# Configuration Reference

All configuration knobs for embeddings and RAG.

## embeddings

Controls the embedding provider and model used for vector search.

```yaml title="~/.anteroom/config.yaml"
embeddings:
  enabled: true                          # null=auto-detect, true=force on, false=disable
  provider: "local"                      # "local" (fastembed) or "api" (OpenAI-compatible)
  model: "text-embedding-3-small"        # Model name for API provider
  dimensions: 0                          # 0=auto-detect from model
  local_model: "BAAI/bge-small-en-v1.5"  # Model name for local provider
  base_url: ""                           # API endpoint (for API provider)
  api_key: ""                            # API key (for API provider)
  api_key_command: ""                    # Shell command to fetch API key dynamically
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool/null | `null` | Tri-state: `null` = auto-detect (enable if provider works), `true` = force on, `false` = disable |
| `provider` | string | `"local"` | `"local"` for fastembed (offline, no API calls) or `"api"` for OpenAI-compatible endpoint |
| `model` | string | `"text-embedding-3-small"` | Model name when using API provider |
| `dimensions` | integer | `0` | Embedding dimensions; `0` = auto-detect from model (384 for local, 1536 for OpenAI) |
| `local_model` | string | `"BAAI/bge-small-en-v1.5"` | Fastembed model name; downloaded automatically on first use |
| `base_url` | string | `""` | API endpoint URL (uses main `ai.base_url` if empty) |
| `api_key` | string | `""` | API key (uses main `ai.api_key` if empty) |
| `api_key_command` | string | `""` | Shell command to fetch API key dynamically (runs on each request) |
| `cache_dir` | string | `""` | Custom fastembed model cache directory; when set, also enables `local_files_only` mode to prevent network requests. Useful for air-gapped environments |

**Environment variables:** `AI_CHAT_EMBEDDINGS_ENABLED`, `AI_CHAT_EMBEDDINGS_PROVIDER`, `AI_CHAT_EMBEDDINGS_MODEL`, `AI_CHAT_EMBEDDINGS_DIMENSIONS`, `AI_CHAT_EMBEDDINGS_LOCAL_MODEL`, `AI_CHAT_EMBEDDINGS_BASE_URL`, `AI_CHAT_EMBEDDINGS_API_KEY`, `AI_CHAT_EMBEDDINGS_API_KEY_COMMAND`, `AI_CHAT_EMBEDDINGS_CACHE_DIR`

### Auto-detection

When `enabled` is `null` (the default), Anteroom auto-detects the embedding provider:

- **Local provider**: creates the service without loading the model at startup; the fastembed model loads lazily on the first dense embedding request
- **API provider**: sends a test embedding request at startup

If the API probe fails, embeddings are silently disabled. If local model loading fails at first use, RAG degrades gracefully; pure `dense` mode returns no results, while `hybrid` can continue with keyword results.

### Dimension Auto-detection

When `dimensions` is `0`:

| Provider | Model | Dimensions |
|----------|-------|-----------|
| local | `BAAI/bge-small-en-v1.5` | 384 |
| local | `BAAI/bge-base-en-v1.5` | 768 |
| local | `BAAI/bge-large-en-v1.5` | 1024 |
| api | (any) | 1536 |

---

## rag

Controls the RAG retrieval pipeline --- what gets searched, how many results, and filtering thresholds.

```yaml title="~/.anteroom/config.yaml"
rag:
  enabled: true                  # Master toggle for RAG
  max_chunks: 10                 # Maximum chunks to retrieve per query
  max_tokens: 2000               # Token budget for RAG context
  similarity_threshold: 0.5      # Maximum cosine distance (lower = stricter)
  include_sources: true          # Search knowledge source chunks
  include_conversations: true    # Search past conversation messages
  exclude_current: true          # Exclude current conversation from results
  retrieval_mode: "dense"        # "dense", "keyword", or "hybrid"
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `true` | Master toggle; `false` disables RAG entirely |
| `max_chunks` | integer | `10` | Maximum number of chunks to retrieve per query |
| `max_tokens` | integer | `2000` | Token budget for injected RAG context (estimated as chars / 4) |
| `similarity_threshold` | float | `0.5` | Maximum cosine distance; results above this threshold are dropped. Lower values = stricter matching. Only applies in `dense` mode |
| `include_sources` | bool | `true` | Whether to search knowledge source chunks |
| `include_conversations` | bool | `true` | Whether to search past conversation messages |
| `exclude_current` | bool | `true` | Whether to exclude the current conversation from message search results |
| `retrieval_mode` | string | `"dense"` | Retrieval strategy: `"dense"` (vector similarity), `"keyword"` (FTS5 text search), or `"hybrid"` (both, merged via Reciprocal Rank Fusion) |

**Environment variables:** `AI_CHAT_RAG_ENABLED`, `AI_CHAT_RAG_MAX_CHUNKS`, `AI_CHAT_RAG_MAX_TOKENS`, `AI_CHAT_RAG_SIMILARITY_THRESHOLD`, `AI_CHAT_RAG_RETRIEVAL_MODE`

### Tuning the Threshold

The `similarity_threshold` is a cosine distance (not cosine similarity). Lower values mean stricter matching:

| Value | Effect |
|-------|--------|
| `0.3` | Very strict --- only highly relevant content surfaces |
| `0.5` | Default --- good balance of relevance and recall |
| `0.7` | Loose --- more content surfaces, may include less relevant results |
| `1.0` | Everything matches (not recommended) |

### Token Budget

The `max_tokens` setting controls how much RAG context is injected into the system prompt. The estimate uses `characters / 4` as a rough token approximation.

If retrieved chunks exceed the budget, the least relevant chunks (highest distance) are dropped until the budget is met.

---

## reranker

Controls the optional cross-encoder reranking stage. When enabled, retrieved chunks are re-scored by a cross-encoder model for improved relevance before being injected into the prompt. Uses fastembed `TextCrossEncoder` locally --- no external API needed.

```yaml title="~/.anteroom/config.yaml"
reranker:
  enabled: null                        # null=auto-detect, true=force, false=disable
  provider: "local"                    # Only "local" (fastembed) is supported
  model: "cross-encoder/ms-marco-MiniLM-L-6-v2"
  top_k: 5                            # Keep top-K after reranking
  score_threshold: 0.0                 # Minimum relevance score (0 = no threshold)
  candidate_multiplier: 3             # Widen initial retrieval by this factor
  cache_dir: ""                        # Custom model cache directory for offline use
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool/null | `null` | Tri-state: `null` = auto-detect (enable if fastembed available), `true` = force on, `false` = disable |
| `provider` | string | `"local"` | Provider: only `"local"` (fastembed TextCrossEncoder) is currently supported |
| `model` | string | `"cross-encoder/ms-marco-MiniLM-L-6-v2"` | Cross-encoder model name (~23MB, downloaded on first use) |
| `top_k` | integer | `5` | Keep top-K chunks after reranking; capped to `rag.max_chunks` at runtime |
| `score_threshold` | float | `0.0` | Minimum relevance score; cross-encoder logits can be negative, so `0.0` means no threshold |
| `candidate_multiplier` | integer | `3` | Fetch `top_k * candidate_multiplier` candidates before reranking; wider pool gives the reranker more to choose from |
| `cache_dir` | string | `""` | Custom fastembed model cache directory; when set, also enables `local_files_only` mode. Useful for air-gapped environments |

**Environment variables:** `AI_CHAT_RERANKER_ENABLED`, `AI_CHAT_RERANKER_PROVIDER`, `AI_CHAT_RERANKER_MODEL`, `AI_CHAT_RERANKER_TOP_K`, `AI_CHAT_RERANKER_SCORE_THRESHOLD`, `AI_CHAT_RERANKER_CANDIDATE_MULTIPLIER`, `AI_CHAT_RERANKER_CACHE_DIR`

### Auto-detection

When `enabled` is `null` (the default), Anteroom creates the local reranker service without loading the cross-encoder at startup. The model is loaded lazily the first time retrieved chunks need reranking, using local-files-only mode unless a cache directory or explicit enablement changes that behavior. If reranking fails, retrieval results are returned in their original order.

### How It Interacts with RAG

The reranker sits between retrieval and token trimming in the pipeline:

1. RAG retrieves `max_chunks * candidate_multiplier` candidates (widened pool)
2. The cross-encoder scores each candidate against the original query
3. Candidates below `score_threshold` are dropped
4. The top `top_k` results are kept (capped to `rag.max_chunks`)
5. Token trimming applies to the reranked results

If reranking fails at runtime, the original retrieval order is used as a fallback.

---

## memory_recall

Controls per-turn recall of typed memory artifacts (#921). Memory recall runs alongside RAG with its own independent token budget — tuning this section does not affect source or conversation retrieval.

```yaml title="~/.anteroom/config.yaml"
memory_recall:
  enabled: null                        # null=auto-detect, true=force, false=disable
  max_memories: 5                      # Top-K memories to inject per turn
  max_tokens: 800                      # Token budget for injected memory block
  similarity_threshold: 0.5            # Max cosine distance; lower = stricter
  show_status: true                    # Show recall status line in CLI renderer
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool/null | `null` | Tri-state: `null` = auto-detect (use if embeddings are available), `true` = force on, `false` = disable |
| `max_memories` | integer | `5` | Maximum memories to inject per turn |
| `max_tokens` | integer | `800` | Token budget (chars/4 estimate) — independent of `rag.max_tokens` |
| `similarity_threshold` | float | `0.5` | Max cosine distance; memories with a higher distance are dropped |
| `show_status` | bool | `true` | When `true`, the CLI renders a status line per turn describing recall outcome |

**Environment variables:** `AI_CHAT_MEMORY_RECALL_ENABLED`, `AI_CHAT_MEMORY_RECALL_MAX_MEMORIES`, `AI_CHAT_MEMORY_RECALL_MAX_TOKENS`, `AI_CHAT_MEMORY_RECALL_SIMILARITY_THRESHOLD`, `AI_CHAT_MEMORY_RECALL_SHOW_STATUS`

See [How Memory Recall Works](how-memory-recall-works.md) for the full pipeline, scope-visibility rules, and untrusted-content framing.

---

## memory.promotion

Controls the governed memory promotion & review pipeline (#920). This is where proposals from users and agents land, how the review queue is rate-limited, and how lineage + rejection reasons are bounded. Defaults are conservative: agent proposals are allowed but must be reviewed, `local_auto_approve` is off, and both the per-conversation candidate cap and the lineage cap prevent runaway writers.

```yaml title="~/.anteroom/config.yaml"
memory:
  promotion:
    default_review_state: candidate         # "candidate" or "pending_review"
    local_auto_approve: false               # escape hatch for solo/local mode
    agent_proposals_enabled: true           # allow proposer="agent" calls
    max_lineage_entries: 50                 # FIFO cap on decision history per memory
    max_candidates_per_conversation: 10     # per-conversation proposal cap (429 on exceed)
    max_reject_reason_chars: 500            # server-side bound on rejection reason
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_review_state` | string | `"candidate"` | Status a fresh proposal lands in. Only `"candidate"` or `"pending_review"` are accepted. Both block recall; the distinction is a product decision about whether your install surfaces two queues. |
| `local_auto_approve` | bool | `false` | When `true`, proposals skip review and land directly in `active` with a `{event: "auto_approved", actor: "system"}` lineage entry. Off by default so durable memory is never an invisible side effect of shared/governed chat. `aroom init` may write `true` for a solo local config; team and server deployments should keep this `false` unless an operator explicitly opts in. |
| `agent_proposals_enabled` | bool | `true` | When `false`, proposals with `proposer="agent"` are rejected with `PromotionAgentDisabledError` → HTTP 403. User proposals are unaffected. |
| `max_lineage_entries` | integer | `50` | FIFO cap on the `lineage` list per memory. When exceeded, the oldest non-creation entry is dropped and a `{event: "truncated"}` marker is appended. |
| `max_candidates_per_conversation` | integer | `10` | Per-conversation rate limit for new candidates. Overflow returns `429` with `conversation_id`/`cap`/`current`/`retry_hint` in the error body. Proposals without a `conversation_id` in provenance bypass the counter (e.g. CLI flows). |
| `max_reject_reason_chars` | integer | `500` | Server-side truncation of the reject reason before it's stored in metadata. The router also enforces a Pydantic `max_length=2000`; the effective cap is the minimum of the two, so raising this value above 2000 has no effect unless you also raise the router bound. |

**Environment variables:** `AI_CHAT_MEMORY_PROMOTION_DEFAULT_REVIEW_STATE`, `AI_CHAT_MEMORY_PROMOTION_LOCAL_AUTO_APPROVE`, `AI_CHAT_MEMORY_PROMOTION_AGENT_PROPOSALS_ENABLED`, `AI_CHAT_MEMORY_PROMOTION_MAX_LINEAGE_ENTRIES`, `AI_CHAT_MEMORY_PROMOTION_MAX_CANDIDATES_PER_CONVERSATION`, `AI_CHAT_MEMORY_PROMOTION_MAX_REJECT_REASON_CHARS`.

See [Memory Promotion & Review](memory-promotion.md) for the full lifecycle, reviewer identity semantics, and agent-proposal contract.

---

## memory.retention

Controls the memory-artifact retention & eviction policy (#625). The retention worker is shared with conversation retention — it's started whenever either `storage.retention_days > 0` or `memory.retention.enabled` is true. Defaults keep eviction off: zero-config installs never delete memories.

```yaml title="~/.anteroom/config.yaml"
memory:
  retention:
    enabled: false                # master switch (default: false — no eviction)
    max_age_days: null            # purge memories older than N days; null = off
    idle_days: null               # purge memories not recalled in N days; null = off
    min_age_days: 1               # grace floor against idle_days for freshly-approved memories
    purge_statuses: ["rejected"]  # statuses eligible for status-based eviction
    respect_pins: true            # pinned memories (metadata.pinned=true) are skipped by default
```

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | bool | `false` | Master switch. When false, the retention worker performs no memory eviction regardless of other fields. |
| `max_age_days` | int or null | `null` | Age cap in days, evaluated against `created_at`. `null` or `0` disables the rule. |
| `idle_days` | int or null | `null` | Idle cap in days, evaluated against `metadata.last_recalled_at` (falling back to `created_at` when recall telemetry hasn't fired). `null` or `0` disables. |
| `min_age_days` | int | `1` | Grace floor against `idle_days` — memories younger than this survive the idle rule even if they've never been recalled. Prevents immediate eviction of freshly-approved memories. |
| `purge_statuses` | list of strings | `["rejected"]` | Statuses eligible for status-based eviction. Invalid statuses are filtered out at load time; an empty list falls back to the default. |
| `respect_pins` | bool | `true` | When true, any memory with `metadata.pinned == true` is skipped regardless of other rules. Set to `false` as an explicit escape hatch. |

**Environment variables:** `AI_CHAT_MEMORY_RETENTION_ENABLED`, `AI_CHAT_MEMORY_RETENTION_MAX_AGE_DAYS`, `AI_CHAT_MEMORY_RETENTION_IDLE_DAYS`, `AI_CHAT_MEMORY_RETENTION_MIN_AGE_DAYS`, `AI_CHAT_MEMORY_RETENTION_PURGE_STATUSES` (comma-separated), `AI_CHAT_MEMORY_RETENTION_RESPECT_PINS`.

See [Data Retention](../security/data-retention.md) for the dry-run preview flow, governed purge endpoint, pinning semantics, and the `memory.purge` audit-log contract.

---

## memory.auto_propose

Selective auto-propose memory candidate pipeline (#1454). When enabled, after each final assistant turn an LLM-driven extractor scans the response for durable facts and proposes them through the existing governed promotion pipeline as `candidate` memories — **never silently active**. Off by default; opt-in.

```yaml title="~/.anteroom/config.yaml"
memory:
  auto_propose:
    enabled: false                       # master switch (default: false — no extraction)
    max_candidates_per_turn: 1           # hard cap on proposals per assistant turn
    categories:                          # subset of MEMORY_CATEGORIES the extractor may propose
      - preference
      - project_fact
      - decision
      - workflow_hint
    min_confidence: 0.8                  # 0.0-1.0; LLM-reported confidence floor
    notify_inline: true                  # render inline notice in CLI/web after extraction
    cooldown_turns: 5                    # suppress repeats of the same content within N turns
```

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | bool | `false` | Master switch. When false, no extraction runs and no LLM round-trip is spent. |
| `max_candidates_per_turn` | int | `1` | Hard upper bound on proposals per final assistant turn. Clamped to `>=1`. |
| `categories` | list of strings | `["preference", "project_fact", "decision", "workflow_hint"]` | Subset of `MEMORY_CATEGORIES` the extractor may propose. Invalid entries are silently dropped at load time; an empty list falls back to the default. |
| `min_confidence` | float | `0.8` | Confidence floor (0.0-1.0). Candidates the model reports below this confidence are filtered out before any DB write. Clamped to `[0.0, 1.0]`. |
| `notify_inline` | bool | `true` | When true, the CLI prints a one-line notice and the web UI renders an inline banner after a turn that produced candidates. When false, candidates land silently and only show up in `aroom memory candidates` / the Memory Review tab. |
| `cooldown_turns` | int | `5` | Suppresses re-proposal of the same content (by normalized hash) within the same conversation for the next N turns. The per-conversation cap from `memory.promotion.max_candidates_per_conversation` still applies on top. |

**Environment variables:** `AI_CHAT_MEMORY_AUTO_PROPOSE_ENABLED`, `AI_CHAT_MEMORY_AUTO_PROPOSE_MAX_CANDIDATES_PER_TURN`, `AI_CHAT_MEMORY_AUTO_PROPOSE_CATEGORIES` (comma-separated), `AI_CHAT_MEMORY_AUTO_PROPOSE_MIN_CONFIDENCE`, `AI_CHAT_MEMORY_AUTO_PROPOSE_NOTIFY_INLINE`, `AI_CHAT_MEMORY_AUTO_PROPOSE_COOLDOWN_TURNS`.

Auto-propose runs in two paths only: web `_stream_chat_events` and the interactive CLI REPL. Non-interactive `aroom exec`, sub-agents, and workflow runners do **not** auto-propose. Every successful proposal emits one `memory.proposed` lineage entry with `details.proposer="agent"` and `details.auto_propose=true` so the audit trail distinguishes auto-propose from manual `proposer="agent"` calls.

See [Memory Promotion & Review](memory-promotion.md) for the full review flow that auto-proposed candidates feed into.

---

## safety.subagent

Runtime limits and governance for sub-agent runs (foreground and detached).

| Key | Type | Default | Env var | Description |
|-----|------|---------|---------|-------------|
| `safety.subagent.max_concurrent` | int | `5` | — | Concurrent detached sub-agents per conversation. Also used as the foreground concurrency cap via `SubagentLimiter`. |
| `safety.subagent.max_total` | int | `10` | — | Total detached sub-agents allowed in a single process (global). |
| `safety.subagent.max_depth` | int | `3` | — | Maximum nesting depth for foreground sub-agents. |
| `safety.subagent.max_iterations` | int | `15` | — | Per-run agent-loop iteration cap for sub-agents. |
| `safety.subagent.timeout` | int | `120` | — | Wall-clock timeout (seconds) for a single sub-agent run. |
| `safety.subagent.max_output_chars` | int | `4000` | — | Truncation bound on sub-agent output returned to the parent. |
| `safety.subagent.max_prompt_chars` | int | `32000` | — | Maximum prompt length accepted by `run_agent`. |

Detached sub-agent lifecycle emits `subagent.*` audit events (#1459) when `audit.enabled: true` — see [`docs/security/audit-log.md`](../security/audit-log.md) for the event taxonomy.

Example:

```yaml
safety:
  subagent:
    max_concurrent: 3
    max_total: 8
    timeout: 90
```

---

## Pack-sourced policies (#924)

Packs can ship `policy` artifacts that set config values through the
same overlay pipeline that `config_overlay` artifacts use — **but**
with a restricted allowlist of keys. A `policy` artifact lives at
`{pack_dir}/policies/{name}.yaml` and may only set keys under:

- `memory.promotion.*`
- `memory.retention.*`

Any key outside those prefixes is **silently dropped with a WARNING**
log that identifies the pack and artifact name. This prevents a
(mis)configured pack from flipping `safety.approval_mode`, disabling
audit logging, or rewriting `ai.*` settings. Priority and merge order
are identical to `config_overlay` artifacts: pack overlay values
compose with team/personal overlays through the same pipeline.

Example `policies/review-defaults.yaml` shipped in a pack:

```yaml
memory:
  promotion:
    agent_proposals_enabled: false
    max_candidates_per_conversation: 10
  retention:
    max_age_days: 180
    respect_pins: true
```

After install, `rebuild_effective_config` will merge these values into
the running config; uninstalling the pack reverts them on the next
rebuild.

---

## storage (RAG-related fields)

```yaml title="~/.anteroom/config.yaml"
storage:
  purge_embeddings: true    # Delete embeddings when conversations are purged
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `purge_embeddings` | bool | `true` | When `retention_days` is set and conversations are purged, also delete their embeddings from the vector index |

---

## Example Configurations

### Offline / Air-gapped

```yaml
embeddings:
  provider: "local"
  cache_dir: "/opt/anteroom/models"  # Pre-downloaded models, no network access
reranker:
  cache_dir: "/opt/anteroom/models"  # Same or separate directory
```

### Hybrid Retrieval with Reranking

```yaml
rag:
  retrieval_mode: "hybrid"       # Dense + keyword, merged via RRF
  max_chunks: 10
reranker:
  enabled: true
  top_k: 5
  candidate_multiplier: 3       # Retrieve 30 candidates, rerank to top 5
```

### OpenAI Embeddings

```yaml
embeddings:
  provider: "api"
  model: "text-embedding-3-small"
  base_url: "https://api.openai.com/v1"
  api_key: "sk-..."
```

### Disable RAG

```yaml
rag:
  enabled: false
```

### Strict Matching with Small Context

```yaml
rag:
  similarity_threshold: 0.3
  max_chunks: 5
  max_tokens: 1000
```

### Sources Only (No Conversation History)

```yaml
rag:
  include_conversations: false
  include_sources: true
```
