Skip to main content
Configuration is realm-scoped. The active realm_id determines which raw config document is read and written.

Realm-scoped config

An ordinary non-global realm keeps these files in its resolved realm directory:
  • config.toml (user settings)
  • config_state.json (generation counter for CAS)
  • realm_manifest.json (pinned backend + metadata)
The reserved global config document and its config-generation sidecar are the exception: they are home-rooted at ~/.rkat/config.toml and ~/.rkat/config_state.json. Global config can be inherited, but a global realm’s runtime state is never inherited. For CLI commands, the normal non-global location is project-local:
Here project-root is the nearest ancestor containing .rkat. Outside an existing project, it falls back to the exact invocation context. The invocation context itself is never walked up for workspace realm identity or MCP trust discovery, so identity and the local storage candidate are intentionally separate facts. An explicit --state-root bypasses candidate probing. For daemon/API surfaces that do not receive a --state-root, the default state root is the platform data directory:
  • macOS: ~/Library/Application Support/meerkat/realms/<realm>/
  • Linux: ~/.local/share/meerkat/realms/<realm>/
  • Windows: %APPDATA%\meerkat\realms\<realm>\
Use realms to choose whether state is shared or isolated.

Realm config inheritance

A realm config doc may declare a parent, and resolution walks a single chain head -> parent -> ... -> global. A terminal non-global realm appends global when a global section is configured. The chain can also end before global when no global config has been materialized.
  • Config inherits along the chain: models, MCP servers, hooks, skills, limits, and auth bindings are composed parent-first with child-wins.
  • State never inherits: sessions, session stores, event stores, and the .rkat/ projection stay realm-local to the consuming (head) realm.
The global realm’s config document is home-rooted at ~/.rkat/config.toml (a single well-known location, not a per-workspace file), which is why a single interactive rkat auth login is inherited by every workspace realm via the chain tail. See Realm inheritance for the full model, including credential read-inherit / write-strict-owner and the add/override-but-not-remove rules for MCP servers and hooks.

Effective precedence

For an agent build in a given realm, effective precedence is:
  1. Built-in defaults
  2. A configured global root and explicit ancestors, folded root-first
  3. The head realm’s own config.toml
  4. Per-request runtime parameters (for example model, system prompt, or tool toggles on create/resume calls)
The agent build path on every surface (CLI, REST, RPC, MCP server) composes this effective config over the head realm, so model/MCP/hook/skill/limit inheritance applies on every build, not just the auth path. config get / config set operate on the raw head doc (a read/write split), so an inherited entry is read through composition but never flattened into the child’s own doc on write. Environment variables still matter, but they are primarily part of runtime credential resolution, not a general config layer that mutates the loaded realm config. In practice:
  • realm config remains the canonical persisted settings surface
  • provider credentials can still come from RKAT_* or provider-native env vars at resolve time
  • bindings and auth profiles decide how those credentials are used at runtime

Config APIs and CAS

RPC, REST, and MCP all expose the same config envelope:
  • config
  • generation
  • realm_id
  • instance_id
  • backend
Some surfaces also expose resolved_paths in diagnostic/admin contexts. Treat that field as optional rather than universal across all public config envelopes. Writes support optimistic concurrency:
  • config/set and config/patch accept optional expected_generation
  • stale writes return generation conflict
This gives deterministic behavior when multiple clients or processes update config concurrently.

Merge semantics

Meerkat uses three update models:
  1. Layered config loading (defaults -> file -> runtime) uses section-specific merge rules:
  • optional settings such as max_tokens override when present; many non-optional scalars override only when nondefault
  • shell, store, comms, compaction, and rest: a nondefault incoming section replaces the whole section, including its omitted fields’ defaults
  • hook entries append; hook scalar settings override only when nondefault
  1. Realm-chain composition reuses those rules, folding each realm doc parent-first:
  • per-provider model defaults: union, child-wins per provider (a child overrides its anthropic default while inheriting the parent’s openai default)
  • MCP servers: map-key union, child-wins per name (add or override, never remove)
  • hook entries and skill sources: append parent-first (child cannot remove inherited entries); hook scalars retain the nondefault-value rule
  • limits: per-field optional child-wins (a child can tighten max_sessions while inheriting other caps)
  • shell, store, comms, compaction, and rest: retain nondefault whole-section replacement, not per-field inheritance
  • filesystem-backed docs additionally use raw TOML presence for tools, retry, and skills overrides, so explicitly setting a supported field to its default can override a parent; this is not a universal rule for all scalar fields or non-filesystem sources
  • model_fallback: a nondefault table replaces the inherited table as a whole; an empty/default-valued table inherits, while enabled = false explicitly replaces and disables
  • the binding/backend/auth maps are not merged across realms: each realm’s binding resolves its backend_profile / auth_profile only within its own section (the owning realm)
  1. Runtime patch APIs (config/patch, PATCH /config) use RFC 7396 JSON merge-patch semantics.
Use config/set when replacing the whole config intentionally, and config/patch for surgical updates.

Presence-based override (Option fields)

Config.max_tokens and agent.max_tokens_per_turn are optional. None means “inherit / use the template default” and is resolved at point-of-use (resolved_max_tokens / resolved_max_tokens_per_turn). Merge for these fields is presence-based (an explicit value wins, regardless of whether it equals the default), so a child realm can override a non-default parent value back down to the default - something a != default heuristic could not express.

CLI behavior

CLI config commands operate on the selected realm:
Without --realm, CLI derives a stable workspace realm (ws-...) from the current directory, or from --context-root when supplied.

What About ~/.rkat/config.toml And .rkat/config.toml?

~/.rkat/config.toml is the home-rooted config document for the reserved global realm. Interactive rkat auth login writes the [realm.global] binding section here; once configured, it is the implicit chain tail for workspace realms. The project-root .rkat/config.toml still exists for rkat init, legacy single-file loading, and explicitly layered hook discovery. Do not confuse it with the active non-global realm document at <state-root>/<realm>/config.toml, which is the canonical raw head store used by CLI/RPC/REST/MCP runtime composition.

Backend and storage settings

Session backend is pinned per realm in realm_manifest.json (sqlite, jsonl, or memory). --realm-backend only matters on first creation of a realm.

Compaction settings

Compaction runtime policy is configurable in realm config:
When session-compaction is enabled, AgentFactory maps these values into the runtime CompactionConfig. If auto_compact_threshold is omitted, cataloged models scale the token trigger to 80 percent of the smaller of their context window and separate input ceiling, when one is declared. For gpt-6-astra, the 922,000-token input ceiling gives a 737,600-token default trigger; its shared input-plus-output context window remains 1,050,000 tokens. Built-in provider clients also attach the current provider’s approximate request-byte cap to each lowered request; the byte trigger fires at 80 percent of that cap. max_request_bytes is an optional operator override, especially useful for a self-hosted gateway whose limit is not in the catalog. Compaction is checked against both token and serialized-request pressure. Inline media can exhaust a provider byte limit long before the token estimate reaches its threshold.

Retry and stream watchdog settings

LLM retry policy (RetryPolicy) is configurable in realm config:
call_timeout is a tri-state override for the hard per-LLM-call deadline: omitted inherits the profile-derived default, "disabled" turns it off, and a duration sets it explicitly. stream_inactivity_timeout is the per-provider-stream inactivity watchdog. It is on by default with a 300-second window: an LLM call whose stream reports no events for the window is aborted with the retryable StreamStalled failure and flows through normal retry handling. Each received stream event re-arms the window, so long-but-alive responses are unaffected. Set stream_inactivity_timeout = "disabled" to opt out, or a duration ("120s") for an explicit window. Unlike call_timeout it never inherits from the model profile; omitted means the built-in 300s default. The watchdog covers all built-in provider clients; a custom AgentLlmClient that does not report stream liveness fails open (no watchdog; only the hard call timeout applies). Unknown provider failures also enter the machine-authorized bounded retry path. Explicit terminal classes such as invalid requests, authentication failures, exhausted quota or credit (OpenAI insufficient_quota, Anthropic spend caps, Gemini daily quota), missing models, content filtering, context overflow, and oversized requests remain non-retryable. With the default max_retries = 3, a persistently unknown failure can make four provider attempts before returning retry_exhausted.

Turn and agent-lifetime bounds

The timeouts above bound one segment of a turn: a single LLM call, a single provider stream, a single tool call. Their sum is a separate fact with a separate owner:
max_turn_duration is the aggregate bound. Its clock is re-armed at every run entry, so it measures one turn end-to-end no matter how many LLM calls, retries, and tool batches that turn contains. Exceeding it takes the existing time terminal, TurnTerminalOutcome::TimeBudgetExceeded, which surfaces as AgentError::TerminalFailure { outcome: TimeBudgetExceeded, .. }. Note the asymmetry with the other budget dimensions, which is deliberate: an exhausted token or tool-call budget is an orderly stop that still answers the caller, while an exhausted deadline is a hard failure. A turn past its deadline can no longer promise what it did or when it will answer, so it fails closed rather than reporting a warning and continuing. max_duration is not a per-turn deadline. Its clock starts when the session’s agent is built and is never re-armed, so it measures the agent’s whole lifetime including idle time between turns. Setting it low to bound a turn will terminalize a later turn that did no work. Both are unset by default: turns are unbounded in aggregate unless a deployment declares a ceiling. The bound is enforced at segment boundaries, so it never tears down a tool call that is already running; the effective ceiling is max_turn_duration plus the longest segment already in flight (at most one per-tool-call timeout, since calls in a batch run their clocks together). limits.max_sessions controls active-session admission; persisted history does not consume the limit. RPC, REST, and MCP snapshot this value when their session service builds its admission semaphore. Changing config does not resize an already-running service; rebuild or restart it to apply the new limit.

HTML Presentation Defaults

HTML rendering is surface-requested. The Config schema accepts a default template and optional custom templates:
This is accepted configuration syntax, but the ordinary CLI realm-composition path currently does not propagate presentation. On that path, neither a configured default nor these named custom templates reaches the HTML resolver. This limitation does not describe direct Rust consumers of a parsed Config. HTML output itself still works. Select the builtin explicitly or read a custom template file directly:
Configuration does not turn ordinary runs into HTML; request --html, --output html, or --browser.

Model fallback settings

Model fallback is disabled by default. Enabling it requires a non-empty, explicit ordered chain. There is no implicit catalog fallback:
A fallback target is identified by model, provider, self-hosted server, and auth_binding. The same model/provider with a different credential binding is a valid target. Cross-provider candidates are skipped before credential lookup unless cross_provider = true (default false). Use enabled = false to disable an inherited policy. A nondefault child fallback table replaces the inherited table as a whole. Omitted tables and empty/default-valued tables inherit; explicit enabled = false is nondefault and therefore replaces the parent. A chain alone never enables fallback. enabled = true with an empty chain is invalid. Unknown fallback keys, including the removed use_catalog_default_chain and unsupported scope/expiry/revert options, are rejected. Explicit brain_swap requests are independent of automatic fallback settings. Target discovery uses the effective model catalog, configured realm routes, account affinity, and installed provider runtimes; it does not resolve foreign credentials or prebuild foreign clients. The active route remains eligible, including an environment-backed active route, but an unconfigured foreign environment-only route is not advertised. Credential and client admission still occur through the canonical runtime realization before the next turn; a staged request is not a committed switch. Separately enabled image generation may resolve its own provider credentials; disabling automatic model fallback does not disable image features. An explicit image-generation disable skips that feature’s automatic executor discovery. Mob definitions also accept [runtime.model_fallback] and [profiles.<name>.model_fallback]. A present profile table wins over mob runtime, which wins over host config; explicit false is preserved through materialization and portable profile revival. Realm-profile references cannot add a fallback override beside realm_profile; put it on the referenced profile or in the mob runtime table instead. Fallback activation is still runtime-governed: the generated recovery authority must classify the LLM failure as recoverable, and the core run loop must decide the retry is pre-stream safe. Defaults consider capacity and provider unavailability only after three failed attempts. Transport and empty-output failures never migrate by default; ordinary bounded retries remain separate. Migration is suppressed after visible text or reasoning. Admission uses the actual hydrated, compacted request, output reserve, target context limits, required tools/modalities, structured output, and credential authority. Unsafe candidates produce typed model_fallback_skipped events, even if every candidate is rejected. Staging is not commitment; only model_fallback_committed confirms the canonical sticky route changed. New fallback commits persist target-bound provenance in the same canonical control transaction. An unsafe fallback-origin resume requires explicit model reconfiguration; explicit identity overrides obsolete that provenance. There is no automatic restoration or migration of pre-0.8.37 sticky routes: historical notice text and brain-swap history are not fallback authority. Scoped overlays, expiry, timed reversion, and doctor/migration redesign remain unsupported.

See also