ai-memory-cli Config::load() Path: Why Single-Pass Configuration Loading Matters

The ai-memory-cli uses a single config::Config::load() call at startup that merges defaults, a TOML config file, and environment variables in one pass, with no second read path to prevent environment desynchronization bugs.

The ai-memory CLI from the akitaonrails/ai-memory repository implements a strict single-read configuration invariant. Every setting is resolved exactly once when the binary starts, producing an immutable Config value that remains unchanged for the program's entire lifetime. This design directly addresses a class of bugs where scattered environment reads caused inconsistent behavior across components.

How Config::load() Resolves the Configuration Path

The Config::load() function in crates/ai-memory-cli/src/config.rs (lines 75-106) follows a deterministic, layered resolution strategy:

Step 1: Determine the Data Directory

The loader prioritizes sources in this order:

  • CLI flag --data-dir (cli_data_dir parameter) takes highest precedence
  • Environment variable AI_MEMORY_DATA_DIR (captured via RuntimeEnv::data_dir) if no flag provided
  • Built-in default from default_data_dir() as final fallback

Step 2: Resolve the Config File Path

With the data directory established, the loader constructs the final path:

  • Explicit --config flag (config_path parameter) overrides everything
  • Otherwise defaults to <data_dir>/config.toml

Step 3: Merge Configuration Layers Using Figment

The implementation uses the Figment library to merge three sources in strict order:

use figment::{Figment, providers::{Serialized, Toml, Env}};

let figment = Figment::from(Serialized::defaults(Self::default()))
    .merge(Toml::file(&resolved_config_path))  // only if file exists
    .merge(Env::prefixed("AI_MEMORY_").split("__"));

This merging order ensures that environment variables override file settings, which in turn override compiled defaults.

Step 4: Inject Runtime-Only Values

After extracting the merged Config, the loader performs exactly one read of process-specific environment variables:

// From config.rs — these values are captured once and never re-read
config.auth.bearer_token = runtime_env.auth_token;      // AI_MEMORY_AUTH_TOKEN
config.server_url = runtime_env.server_url;             // AI_MEMORY_SERVER_URL
config.home_dir = Some(runtime_env.home_dir);           // canonicalized
config.data_dir = data_dir.canonicalize()?;

Step 5: Validate and Return

Finally, Config::load() enforces numeric invariants (decay weights, LLM token limits) and returns either a fully-populated, immutable Config or an early error.

Why No Second Config-Read Path Exists

The comment at the top of config.rs explicitly documents this architectural decision (lines 3-7):

//! All settings are read exactly once at startup, merged into a single
//! immutable [`Config`] value, and passed by reference everywhere. There is
//! no second read path (lesson from agentmemory #456 / #469 — the dimension
//! guard read `process.env` while the rest of the codebase used
//! `getMergedEnv()`, masking the bug for weeks).

The Historical Bug That Motivated This Design

Issues #456 and #469 in the related agentmemory project revealed a critical failure mode: one component read process.env directly while others used a merged configuration view. This desynchronization masked bugs for weeks because tests passed in isolation but production environments exhibited inconsistent behavior.

Four Architectural Guarantees

Guarantee Mechanism Benefit
Consistency RuntimeEnv::from_process() captures all environment variables in one call Every component sees identical values even if the external environment changes
Predictability Single, well-defined merge order (defaults → file → environment) No accidental overrides from late code re-reading configuration
Performance One-time I/O and parsing Fast startup for CLI subcommands
Thread Safety Immutable Config struct Lock-free sharing across threads

Practical Usage Example

Here's how the ai-memory-cli binary invokes the single-pass loader:

use ai_memory_cli::config::Config;
use std::path::Path;

fn main() -> anyhow::Result<()> {
    // Parsed from clap arguments
    let cli_data_dir: Option<&Path> = None;  // from --data-dir
    let config_path: Option<&Path> = None;   // from --config

    // SINGLE LOAD — this is the only configuration read in the entire program
    let cfg = Config::load(config_path, cli_data_dir)?;

    println!("Data directory: {}", cfg.data_dir.display());
    println!("Server URL: {}", cfg.server_url);
    
    // Runtime values injected during load, never re-read
    if let Some(home) = cfg.home_dir.as_deref() {
        println!("Home: {}", home);
    }
    Ok(())
}

Key Source Files

Summary

  • The config::Config::load() path in ai-memory-cli merges defaults → TOML file → environment variables in a single pass using Figment
  • No second read path exists to prevent the environment desynchronization bugs that plagued earlier projects (#456, #469)
  • Runtime-only values (AI_MEMORY_AUTH_TOKEN, AI_MEMORY_SERVER_URL, HOME) are captured exactly once via RuntimeEnv::from_process()
  • The resulting immutable Config eliminates race conditions, ensures testability, and provides predictable behavior across all CLI subcommands

Frequently Asked Questions

What happens if the configuration file doesn't exist?

The loader continues without error. Toml::file() in Figment is non-failing for missing files—the merge simply proceeds with defaults and environment variables. This supports zero-configuration deployments where all settings come from environment variables.

Can I reload configuration without restarting ai-memory-cli?

No. The single-read invariant is architectural, not optional. To change configuration, you must restart the process. This trade-off prioritizes consistency over dynamic reconfiguration, which aligns with the CLI's short-lived process model.

Why use Figment instead of simpler alternatives like envy or config-rs?

Figment provides the precise layering semantics (defaults → file → environment) and prefix-based environment variable parsing (AI_MEMORY_* with __ delimiter for nested keys) that ai-memory-cli requires. It also gracefully handles missing files without custom logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →