Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/substrate/audit/config-settings-sync.md
Fimeg e480809c70 docs: rescue the agent-substrate tree out of a gitignored directory
219 files, 2.0 MB, untracked in souveraine/docs and existing nowhere else.
The volume is at 100% with no snapshots.
2026-07-26 12:11:50 -04:00

3.9 KiB

Config → Settings Sync Architecture

How the Settings UI (TOML editor) pushes changes to other data stores. Last updated: 2026-05-14


The two persistence systems

System A — souveraine.toml (config file) User-editable fields across ~20 sections (bifrost, subconscious, compaction, memory, voice, etc.). Edited by Settings UI (src/ui/settings.rs), saved to disk, hot-loaded into Arc<RwLock<ConsciousnessConfig>>.

System B — SQLite agent records (~/.souveraine/server/agents/*/) Primary agent state including llm_model, context_window, temperature, tags, etc. Read by run_turn at src/backend/local.rs:793 for the per-turn Bifrost request.

The model value in System B was written once at agent creation time and never updated.


The sync rule

On every Settings save (TOML write), compute the diff between the original snapshot and the saved config. For each changed field, push the new value to every data store that shadows it:

TOML field                        → SQLite column
─────────────────────────────────────────────────────
bifrost.primary_model             → agents.llm_model
subconscious.model                → subconscious agent's llm_model (future)
reflection.model                  → subconscious agent's reflection config (future)
compaction.*                      → agent config_json (future)
memory.*                          → agent config_json (future)

Current implementation scope (2026-05-14): only primary_model syncs to SQLite. The diff-and-push pattern is extensible; add new field mappings by extending the sync block.


The sync path

SettingsView::handle_key('y')         → SettingsAction::Save
  └─ App::handle_settings_key()
       ├─ view.save(&path)            → writes TOML to disk
       ├─ live = view.config.clone()  → hot-reloads in-memory config
       └─ sync_fields(view)           → pushes changed fields to SQLite
            └─ for each (field, new_val) in changed_fields()
                 └─ backend.update_agent_model(id, val)
                      └─ AgentInventory::update()   → SQLite UPDATE
                                                    → agent.json rewrite
                                                    → cache refresh

The function sync_fields() computes the diff between view.original (snapshot at Settings entry) and view.config (edited state). Only fields whose values actually changed are pushed, which makes the system idempotent — saving the same config twice writes TOML twice but skips redundant SQLite writes.


Multi-field change guard (stub)

When multiple fields with SQLite shadows change in a single save, the system should prompt for confirmation before pushing all of them. This prevents accidental bulk updates (e.g. editing both primary_model and subconscious.model at once without realizing both affect agent records).

Implementation status: stub. The diff machinery enumerates changed fields but the confirmation dialog is not yet wired. Change count is logged but save proceeds without blocking:

tracing::info!(
    changed_fields = %field_list,
    "Settings sync: pushing N fields to SQLite"
)

Relevant TOML fields that will eventually need this guard:

Section Field Synced to
bifrost primary_model agents.llm_model
subconscious model subconscious agent
compaction.per_type.* strategy, thresholds agent config_json
memory base_path, git_enabled agent config_json

One sync per save

Each Settings save triggers exactly one sync pass. The sync is synchronous within the save handler — the TUI shows "saved" only after both TOML write and SQLite writes complete. If a SQLite write fails, the error is logged but the TOML save is not rolled back (the user can retry by saving again).