2026-03-25 01:23:12 -04:00
|
|
|
// config.rs — Unified configuration
|
2026-03-25 00:52:41 -04:00
|
|
|
//
|
2026-03-27 21:32:28 -04:00
|
|
|
// Single config file: ~/.consciousness/config.json5
|
2026-03-25 01:23:12 -04:00
|
|
|
// Memory settings in the "memory" section (Config)
|
|
|
|
|
// Agent/backend settings at top level (AppConfig)
|
|
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::sync::{Arc, OnceLock, RwLock};
|
2026-03-25 00:52:41 -04:00
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
use anyhow::{Context as _, Result};
|
2026-03-25 00:52:41 -04:00
|
|
|
use figment::providers::Serialized;
|
|
|
|
|
use figment::{Figment, Provider};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
/// Config file path shared by all loaders.
|
|
|
|
|
pub fn config_path() -> PathBuf {
|
|
|
|
|
dirs::home_dir()
|
|
|
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
2026-03-27 21:32:28 -04:00
|
|
|
.join(".consciousness/config.json5")
|
2026-03-25 01:23:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Memory config (the "memory" section)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
static CONFIG: OnceLock<RwLock<Arc<Config>>> = OnceLock::new();
|
|
|
|
|
|
2026-04-02 18:46:27 -04:00
|
|
|
fn default_stream_timeout() -> u64 { 60 }
|
2026-04-04 05:01:49 -04:00
|
|
|
fn default_scoring_interval_secs() -> u64 { 3600 } // 1 hour
|
|
|
|
|
fn default_scoring_response_window() -> usize { 100 }
|
2026-04-13 18:50:21 -04:00
|
|
|
fn default_node_weight() -> f64 { 0.7 }
|
|
|
|
|
fn default_edge_decay() -> f64 { 0.3 }
|
|
|
|
|
fn default_max_hops() -> u32 { 3 }
|
|
|
|
|
fn default_min_activation() -> f64 { 0.05 }
|
2026-03-28 19:49:13 -04:00
|
|
|
fn default_identity_dir() -> PathBuf {
|
2026-04-02 19:57:40 -04:00
|
|
|
dirs::home_dir().unwrap_or_default().join(".consciousness/identity")
|
2026-03-28 19:49:13 -04:00
|
|
|
}
|
2026-03-25 01:23:12 -04:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub struct Config {
|
|
|
|
|
pub user_name: String,
|
|
|
|
|
pub assistant_name: String,
|
|
|
|
|
#[serde(deserialize_with = "deserialize_path")]
|
|
|
|
|
pub data_dir: PathBuf,
|
2026-03-28 19:49:13 -04:00
|
|
|
#[serde(default = "default_identity_dir", deserialize_with = "deserialize_path")]
|
|
|
|
|
pub identity_dir: PathBuf,
|
2026-03-25 01:23:12 -04:00
|
|
|
#[serde(deserialize_with = "deserialize_path")]
|
|
|
|
|
pub projects_dir: PathBuf,
|
2026-04-15 02:37:49 -04:00
|
|
|
/// Nodes that cannot be deleted or renamed
|
2026-04-15 01:40:18 -04:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub protected_nodes: Vec<String>,
|
2026-04-15 02:37:49 -04:00
|
|
|
/// Nodes loaded into main session context
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub personality_nodes: Vec<String>,
|
|
|
|
|
/// Nodes loaded into subconscious agent context
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub agent_nodes: Vec<String>,
|
2026-03-25 01:23:12 -04:00
|
|
|
pub llm_concurrency: usize,
|
2026-04-02 18:46:27 -04:00
|
|
|
/// Stream chunk timeout in seconds (no data = timeout).
|
|
|
|
|
#[serde(default = "default_stream_timeout")]
|
|
|
|
|
pub api_stream_timeout_secs: u64,
|
2026-04-04 05:01:49 -04:00
|
|
|
/// How often to re-score memory nodes (seconds). Default: 3600 (1 hour).
|
|
|
|
|
#[serde(default = "default_scoring_interval_secs")]
|
|
|
|
|
pub scoring_interval_secs: u64,
|
|
|
|
|
/// Number of assistant responses to score per memory. Default: 50.
|
|
|
|
|
#[serde(default = "default_scoring_response_window")]
|
|
|
|
|
pub scoring_response_window: usize,
|
2026-03-25 01:23:12 -04:00
|
|
|
pub agent_types: Vec<String>,
|
2026-04-09 11:45:39 -04:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub mcp_servers: Vec<McpServerConfig>,
|
2026-04-09 12:07:50 -04:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub lsp_servers: Vec<LspServerConfig>,
|
2026-03-26 14:22:29 -04:00
|
|
|
/// Max conversation bytes to include in surface agent context.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub surface_conversation_bytes: Option<usize>,
|
2026-04-13 18:50:21 -04:00
|
|
|
|
|
|
|
|
// Spreading activation parameters
|
|
|
|
|
#[serde(default = "default_node_weight")]
|
|
|
|
|
pub default_node_weight: f64,
|
|
|
|
|
#[serde(default = "default_edge_decay")]
|
|
|
|
|
pub edge_decay: f64,
|
|
|
|
|
#[serde(default = "default_max_hops")]
|
|
|
|
|
pub max_hops: u32,
|
|
|
|
|
#[serde(default = "default_min_activation")]
|
|
|
|
|
pub min_activation: f64,
|
2026-03-25 01:23:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Config {
|
|
|
|
|
fn default() -> Self {
|
2026-04-02 19:57:40 -04:00
|
|
|
let home = dirs::home_dir().unwrap_or_default();
|
2026-03-25 01:23:12 -04:00
|
|
|
Self {
|
|
|
|
|
user_name: "User".to_string(),
|
|
|
|
|
assistant_name: "Assistant".to_string(),
|
2026-03-27 21:26:28 -04:00
|
|
|
data_dir: home.join(".consciousness/memory"),
|
2026-03-28 19:49:13 -04:00
|
|
|
identity_dir: home.join(".consciousness/identity"),
|
2026-03-25 01:23:12 -04:00
|
|
|
projects_dir: home.join(".claude/projects"),
|
2026-04-15 01:40:18 -04:00
|
|
|
protected_nodes: Vec::new(),
|
2026-04-15 02:37:49 -04:00
|
|
|
personality_nodes: vec!["identity".into(), "core-practices".into()],
|
|
|
|
|
agent_nodes: vec!["identity".into(), "core-practices".into()],
|
2026-03-25 01:23:12 -04:00
|
|
|
llm_concurrency: 1,
|
2026-04-02 18:46:27 -04:00
|
|
|
api_stream_timeout_secs: default_stream_timeout(),
|
2026-04-04 05:01:49 -04:00
|
|
|
scoring_interval_secs: default_scoring_interval_secs(),
|
|
|
|
|
scoring_response_window: default_scoring_response_window(),
|
2026-03-25 01:23:12 -04:00
|
|
|
agent_types: vec![
|
|
|
|
|
"linker".into(), "organize".into(), "distill".into(),
|
|
|
|
|
"separator".into(), "split".into(),
|
|
|
|
|
],
|
2026-03-26 14:22:29 -04:00
|
|
|
surface_conversation_bytes: None,
|
2026-04-09 11:45:39 -04:00
|
|
|
mcp_servers: vec![],
|
2026-04-09 12:07:50 -04:00
|
|
|
lsp_servers: vec![],
|
2026-04-13 18:50:21 -04:00
|
|
|
default_node_weight: default_node_weight(),
|
|
|
|
|
edge_decay: default_edge_decay(),
|
|
|
|
|
max_hops: default_max_hops(),
|
|
|
|
|
min_activation: default_min_activation(),
|
2026-03-25 01:23:12 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Config {
|
|
|
|
|
fn load_from_file() -> Self {
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
Self::try_load_shared().unwrap_or_default()
|
2026-03-25 01:23:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load from shared config. Memory settings in the "memory" section;
|
|
|
|
|
/// API settings resolved from models + backend configuration.
|
|
|
|
|
fn try_load_shared() -> Option<Self> {
|
|
|
|
|
let content = std::fs::read_to_string(config_path()).ok()?;
|
config_writer: emit pretty multi-line sections, drop json5 crate
Previously when append_kvp created a new section or added a key, it
stuffed the "\n " separator into the new kvp's wsc.0 (the whitespace
between its own key and colon) instead of the prior kvp's wsc.3 (the
whitespace after the prior trailing comma). Result looked like:
lsp_servers: [...],
learn
: {generate_alternates
: true,},}
The writer also didn't set any interior whitespace on the new section's
JSONObjectContext, so everything crammed onto one line — `{key: val,}`
compact, not `{\n key: val,\n}` multi-line.
Rewrote the appender as append_kvp_pretty(object, key, value,
inner_indent, outer_indent):
- separator between kvps goes in the prior kvp's wsc.3, or if we're the
first kvp in a fresh object, in the object's own wsc.0 (after its
opening `{`)
- new kvp's wsc.3 carries `,\n<outer_indent>` so the parent's closing
`}` lands correctly indented
- interior indent vs outer indent are both explicit, so we don't have
to rewrite this logic every time we add another nesting level
New tests: new_section_exact_multiline_layout asserts byte-exact
output shape; new_section_and_key_format_cleanly verifies no key wraps
to the next line. Prior tests just substring-matched and happily passed
on the broken output — that's why this shipped in the first place.
Also: dropped the json5 crate dependency. json-five's serde feature
(default) provides the same from_str / to_string API. One fewer
dependency, and the two were doing the same job.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 13:08:19 -04:00
|
|
|
let root: serde_json::Value = json_five::from_str(&content).ok()?;
|
2026-03-25 01:23:12 -04:00
|
|
|
let mem_value = root.get("memory")?;
|
|
|
|
|
|
|
|
|
|
let mut config: Config = serde_json::from_value(mem_value.clone()).ok()?;
|
|
|
|
|
config.llm_concurrency = config.llm_concurrency.max(1);
|
|
|
|
|
|
config: unify subconscious API resolution with the main chat path
Two parallel backend-resolution paths had drifted apart:
- Main chat: AppConfig::resolve_model() → a named BackendConfig in
AppConfig.backends
- Subconscious / oneshot / context_window(): four skip-serde
"cache" fields on Config (memory section) — api_base_url, api_key,
api_model, api_context_window — that used to be populated at
Config::try_load_shared time by walking memory.agent_model →
root.models[name] → root[backend_name]
When we renamed `models` to `backends` and collapsed ModelConfig into
BackendConfig, the latter chain started silently dereferencing
`root.get("models")` → None → no population. Subconscious agents fell
through the "API not configured" guard; context_window() started
returning 0 (since api_context_window default is u64's 0 now that we
don't populate it). It was only visibly working for the main chat.
Collapse to one path:
- Drop Config.agent_model (duplicate of AppConfig.default_backend)
- Drop Config.{api_base_url, api_key, api_model, api_context_window}
— no longer populated, no longer needed
- Drop default_context_window() — nobody reads the field anymore
- Drop the memory-side resolution block in try_load_shared()
- Subconscious (mind/unconscious.rs) and oneshot (agent/oneshot.rs)
now call load_app() + resolve_model(&app.default_backend) just like
the main chat does
- context_window() reads from config::app().backends[default_backend]
.context_window, defaulting to 128k only if the backend doesn't
specify one
Side effect: Kent's config file drops agent_model, api_reasoning,
journal_days, journal_max — all fields whose Rust counterparts are
now gone. (Figment tolerates unknown fields, so leaving them wouldn't
have broken anything, but they were lying about what's configurable.)
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 16:02:43 -04:00
|
|
|
// Top-level sections (not inside "memory").
|
2026-04-09 13:25:33 -04:00
|
|
|
if let Some(servers) = root.get("lsp_servers") {
|
|
|
|
|
config.lsp_servers = serde_json::from_value(servers.clone()).unwrap_or_default();
|
|
|
|
|
}
|
|
|
|
|
if let Some(servers) = root.get("mcp_servers") {
|
|
|
|
|
config.mcp_servers = serde_json::from_value(servers.clone()).unwrap_or_default();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
Some(config)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the global memory config (cheap Arc clone).
|
|
|
|
|
pub fn get() -> Arc<Config> {
|
|
|
|
|
CONFIG
|
|
|
|
|
.get_or_init(|| RwLock::new(Arc::new(Config::load_from_file())))
|
|
|
|
|
.read()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reload the config from disk. Returns true if changed.
|
|
|
|
|
pub fn reload() -> bool {
|
|
|
|
|
let lock = CONFIG.get_or_init(|| RwLock::new(Arc::new(Config::load_from_file())));
|
|
|
|
|
let new = Config::load_from_file();
|
|
|
|
|
let mut current = lock.write().unwrap();
|
|
|
|
|
let changed = format!("{:?}", **current) != format!("{:?}", new);
|
|
|
|
|
if changed {
|
|
|
|
|
*current = Arc::new(new);
|
|
|
|
|
}
|
|
|
|
|
changed
|
|
|
|
|
}
|
|
|
|
|
|
config: watch config.json5 with inotify, reload live on change
Both config halves (Config for the memory section, AppConfig globally)
are now reloaded whenever ~/.consciousness/config.json5 changes on
disk. So edits from vim, manual tweaks, or F6's own config_writer
calls all land without a restart. No more "reload the daemon to pick
up a config change."
Wires up the previously-unused Config::reload() (Kent flagged it as
"not dead, just not wired"). Pairs it with an AppConfig reload via
install_app(). Both run on the same file-change event.
Implementation:
- notify-debouncer-mini watches the config file's parent directory
(editors usually replace-via-rename, so watching the file itself
misses the new inode). Debounced at 200ms to coalesce the flurry
of events editors produce around a single save.
- Filter for events whose path is the actual config file.
- On match: call reload() for Config, run build_figment + extract for
AppConfig. If AppConfig parsing fails (editor mid-save with partial
content), log and keep the old cached value.
- Watcher runs in its own named thread, fire-and-forget. If startup
fails we just log and move on — worst case is no live reload, not
a crash.
CliArgs + SubCmd both get Clone derives so the watcher can own a
snapshot of the startup args for future reloads. Watcher is kicked
off in user/mod.rs:start() right after load_session.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 16:14:43 -04:00
|
|
|
/// Spawn a background thread that watches `~/.consciousness/config.json5`
|
|
|
|
|
/// and reloads both the memory Config and the global AppConfig whenever
|
|
|
|
|
/// the file changes on disk. Lets edits from vim / F6 hotkeys / manual
|
|
|
|
|
/// tweaks land live without restarting the process.
|
|
|
|
|
pub fn watch_config(cli: crate::user::CliArgs) {
|
|
|
|
|
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
|
|
|
|
|
|
|
|
|
|
let path = config_path();
|
|
|
|
|
// Watch the parent directory — editors often replace-via-rename, so
|
|
|
|
|
// watching the file itself misses the new inode.
|
|
|
|
|
let Some(parent) = path.parent().map(|p| p.to_path_buf()) else {
|
|
|
|
|
crate::dbglog!("[config] no parent for {}, skipping watch", path.display());
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::thread::Builder::new()
|
|
|
|
|
.name("config-watcher".into())
|
|
|
|
|
.spawn(move || {
|
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
|
let mut debouncer = match new_debouncer(std::time::Duration::from_millis(200), tx) {
|
|
|
|
|
Ok(d) => d,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
crate::dbglog!("[config] watcher setup failed: {}", e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if let Err(e) = debouncer.watcher()
|
|
|
|
|
.watch(&parent, RecursiveMode::NonRecursive)
|
|
|
|
|
{
|
|
|
|
|
crate::dbglog!("[config] watch({}) failed: {}", parent.display(), e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
crate::dbglog!("[config] watching {}", path.display());
|
|
|
|
|
|
|
|
|
|
while let Ok(res) = rx.recv() {
|
|
|
|
|
let Ok(events) = res else { continue; };
|
|
|
|
|
if !events.iter().any(|e| e.path == path) { continue; }
|
|
|
|
|
|
|
|
|
|
// Reload both halves.
|
|
|
|
|
let mem_changed = reload();
|
|
|
|
|
let app_changed = match build_figment(&cli).extract::<AppConfig>() {
|
|
|
|
|
Ok(app) => {
|
|
|
|
|
install_app(app);
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
crate::dbglog!("[config] reload: AppConfig parse failed: {}", e);
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
crate::dbglog!("[config] reloaded (memory_changed={}, app_changed={})",
|
|
|
|
|
mem_changed, app_changed);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.ok();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
// ============================================================
|
|
|
|
|
// Agent config (top-level settings)
|
|
|
|
|
// ============================================================
|
2026-03-25 00:52:41 -04:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AppConfig {
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
/// Named model endpoints — credentials, base URL, and model id bundled
|
|
|
|
|
/// into one entry per backend. Keyed by name, selected by
|
|
|
|
|
/// `default_backend` or by `--model <name>` on the CLI.
|
2026-03-25 00:52:41 -04:00
|
|
|
#[serde(default)]
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
pub backends: HashMap<String, BackendConfig>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub default_backend: String,
|
2026-03-25 00:52:41 -04:00
|
|
|
pub debug: bool,
|
|
|
|
|
pub compaction: CompactionConfig,
|
|
|
|
|
pub dmn: DmnConfig,
|
learn: F6 screen — scoring stats, ActivityGuard, configurable threshold
Three changes that together reshape the F6 fine-tune-review screen:
1. Finetune scoring reports through the standard agent activity system
instead of a separate finetune_progress String. The previous design
ran an independent progress field that forced a cross-lock dance and
bespoke UI plumbing. start_finetune_scoring now uses start_activity
+ activity.update, so the usual status line and notifications
capture scoring progress uniformly with other background work.
2. MindState gains a FinetuneScoringStats snapshot (responses seen,
above threshold, max divergence, error). The F6 empty screen shows
this instead of a loading message — so after a scoring run that
produced zero candidates, you can see *why* (e.g., max_divergence
below threshold).
3. The divergence threshold is configurable from F6 via +/- hotkeys
(scales by 10×) and persisted to ~/.consciousness/config.json5 via
config_writer::set_learn_threshold. AppConfig grows a learn section
with a threshold field (default 1e-7).
Also: user/mod.rs no longer uses try_lock() for the per-tick
unconscious/mind state sync — we fixed the locking hot paths that
made try_lock necessary, so lock().await is now the right choice.
And subconscious::learn::score_finetune_candidates now returns
(candidates, max_divergence) so the stats can be populated.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 11:49:26 -04:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub learn: LearnConfig,
|
2026-03-25 00:52:41 -04:00
|
|
|
#[serde(default)]
|
2026-04-09 11:45:39 -04:00
|
|
|
pub mcp_servers: Vec<McpServerConfig>,
|
2026-04-09 12:07:50 -04:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub lsp_servers: Vec<LspServerConfig>,
|
2026-04-09 11:45:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct McpServerConfig {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub command: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub args: Vec<String>,
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 12:07:50 -04:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct LspServerConfig {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub command: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub args: Vec<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub languages: Vec<String>, // e.g. ["rust"], ["c", "cpp"]. Empty = auto-detect
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 00:52:41 -04:00
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
|
|
|
pub struct BackendConfig {
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
/// API key for the backend.
|
2026-03-25 00:52:41 -04:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub api_key: String,
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
/// Base URL for the backend's OpenAI-compatible endpoint.
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2026-03-25 00:52:41 -04:00
|
|
|
pub base_url: Option<String>,
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
/// Model identifier sent to the API.
|
|
|
|
|
pub model_id: String,
|
|
|
|
|
/// Context window size in tokens.
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub context_window: Option<usize>,
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct CompactionConfig {
|
|
|
|
|
pub hard_threshold_pct: u32,
|
|
|
|
|
pub soft_threshold_pct: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct DmnConfig {
|
|
|
|
|
pub max_turns: u32,
|
|
|
|
|
}
|
|
|
|
|
|
learn: F6 screen — scoring stats, ActivityGuard, configurable threshold
Three changes that together reshape the F6 fine-tune-review screen:
1. Finetune scoring reports through the standard agent activity system
instead of a separate finetune_progress String. The previous design
ran an independent progress field that forced a cross-lock dance and
bespoke UI plumbing. start_finetune_scoring now uses start_activity
+ activity.update, so the usual status line and notifications
capture scoring progress uniformly with other background work.
2. MindState gains a FinetuneScoringStats snapshot (responses seen,
above threshold, max divergence, error). The F6 empty screen shows
this instead of a loading message — so after a scoring run that
produced zero candidates, you can see *why* (e.g., max_divergence
below threshold).
3. The divergence threshold is configurable from F6 via +/- hotkeys
(scales by 10×) and persisted to ~/.consciousness/config.json5 via
config_writer::set_learn_threshold. AppConfig grows a learn section
with a threshold field (default 1e-7).
Also: user/mod.rs no longer uses try_lock() for the per-tick
unconscious/mind state sync — we fixed the locking hot paths that
made try_lock necessary, so lock().await is now the right choice.
And subconscious::learn::score_finetune_candidates now returns
(candidates, max_divergence) so the stats can be populated.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 11:49:26 -04:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct LearnConfig {
|
|
|
|
|
/// Divergence threshold — responses scoring above this become
|
|
|
|
|
/// fine-tuning candidates. Lower = more sensitive.
|
|
|
|
|
#[serde(default = "default_learn_threshold")]
|
|
|
|
|
pub threshold: f64,
|
2026-04-16 12:53:22 -04:00
|
|
|
/// Whether to generate "what would the model have said without
|
|
|
|
|
/// memories" alternates alongside each scoring run. Expensive —
|
|
|
|
|
/// one full streaming generation per candidate.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub generate_alternates: bool,
|
learn: F6 screen — scoring stats, ActivityGuard, configurable threshold
Three changes that together reshape the F6 fine-tune-review screen:
1. Finetune scoring reports through the standard agent activity system
instead of a separate finetune_progress String. The previous design
ran an independent progress field that forced a cross-lock dance and
bespoke UI plumbing. start_finetune_scoring now uses start_activity
+ activity.update, so the usual status line and notifications
capture scoring progress uniformly with other background work.
2. MindState gains a FinetuneScoringStats snapshot (responses seen,
above threshold, max divergence, error). The F6 empty screen shows
this instead of a loading message — so after a scoring run that
produced zero candidates, you can see *why* (e.g., max_divergence
below threshold).
3. The divergence threshold is configurable from F6 via +/- hotkeys
(scales by 10×) and persisted to ~/.consciousness/config.json5 via
config_writer::set_learn_threshold. AppConfig grows a learn section
with a threshold field (default 1e-7).
Also: user/mod.rs no longer uses try_lock() for the per-tick
unconscious/mind state sync — we fixed the locking hot paths that
made try_lock necessary, so lock().await is now the right choice.
And subconscious::learn::score_finetune_candidates now returns
(candidates, max_divergence) so the stats can be populated.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 11:49:26 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 12:53:22 -04:00
|
|
|
fn default_learn_threshold() -> f64 { 1.0 }
|
learn: F6 screen — scoring stats, ActivityGuard, configurable threshold
Three changes that together reshape the F6 fine-tune-review screen:
1. Finetune scoring reports through the standard agent activity system
instead of a separate finetune_progress String. The previous design
ran an independent progress field that forced a cross-lock dance and
bespoke UI plumbing. start_finetune_scoring now uses start_activity
+ activity.update, so the usual status line and notifications
capture scoring progress uniformly with other background work.
2. MindState gains a FinetuneScoringStats snapshot (responses seen,
above threshold, max divergence, error). The F6 empty screen shows
this instead of a loading message — so after a scoring run that
produced zero candidates, you can see *why* (e.g., max_divergence
below threshold).
3. The divergence threshold is configurable from F6 via +/- hotkeys
(scales by 10×) and persisted to ~/.consciousness/config.json5 via
config_writer::set_learn_threshold. AppConfig grows a learn section
with a threshold field (default 1e-7).
Also: user/mod.rs no longer uses try_lock() for the per-tick
unconscious/mind state sync — we fixed the locking hot paths that
made try_lock necessary, so lock().await is now the right choice.
And subconscious::learn::score_finetune_candidates now returns
(candidates, max_divergence) so the stats can be populated.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 11:49:26 -04:00
|
|
|
|
|
|
|
|
impl Default for LearnConfig {
|
|
|
|
|
fn default() -> Self {
|
2026-04-16 12:53:22 -04:00
|
|
|
Self {
|
|
|
|
|
threshold: default_learn_threshold(),
|
|
|
|
|
generate_alternates: false,
|
|
|
|
|
}
|
learn: F6 screen — scoring stats, ActivityGuard, configurable threshold
Three changes that together reshape the F6 fine-tune-review screen:
1. Finetune scoring reports through the standard agent activity system
instead of a separate finetune_progress String. The previous design
ran an independent progress field that forced a cross-lock dance and
bespoke UI plumbing. start_finetune_scoring now uses start_activity
+ activity.update, so the usual status line and notifications
capture scoring progress uniformly with other background work.
2. MindState gains a FinetuneScoringStats snapshot (responses seen,
above threshold, max divergence, error). The F6 empty screen shows
this instead of a loading message — so after a scoring run that
produced zero candidates, you can see *why* (e.g., max_divergence
below threshold).
3. The divergence threshold is configurable from F6 via +/- hotkeys
(scales by 10×) and persisted to ~/.consciousness/config.json5 via
config_writer::set_learn_threshold. AppConfig grows a learn section
with a threshold field (default 1e-7).
Also: user/mod.rs no longer uses try_lock() for the per-tick
unconscious/mind state sync — we fixed the locking hot paths that
made try_lock necessary, so lock().await is now the right choice.
And subconscious::learn::score_finetune_candidates now returns
(candidates, max_divergence) so the stats can be populated.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 11:49:26 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 00:52:41 -04:00
|
|
|
impl Default for AppConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
backends: HashMap::new(),
|
|
|
|
|
default_backend: String::new(),
|
2026-03-25 00:52:41 -04:00
|
|
|
debug: false,
|
|
|
|
|
compaction: CompactionConfig {
|
|
|
|
|
hard_threshold_pct: 90,
|
|
|
|
|
soft_threshold_pct: 80,
|
|
|
|
|
},
|
|
|
|
|
dmn: DmnConfig { max_turns: 20 },
|
learn: F6 screen — scoring stats, ActivityGuard, configurable threshold
Three changes that together reshape the F6 fine-tune-review screen:
1. Finetune scoring reports through the standard agent activity system
instead of a separate finetune_progress String. The previous design
ran an independent progress field that forced a cross-lock dance and
bespoke UI plumbing. start_finetune_scoring now uses start_activity
+ activity.update, so the usual status line and notifications
capture scoring progress uniformly with other background work.
2. MindState gains a FinetuneScoringStats snapshot (responses seen,
above threshold, max divergence, error). The F6 empty screen shows
this instead of a loading message — so after a scoring run that
produced zero candidates, you can see *why* (e.g., max_divergence
below threshold).
3. The divergence threshold is configurable from F6 via +/- hotkeys
(scales by 10×) and persisted to ~/.consciousness/config.json5 via
config_writer::set_learn_threshold. AppConfig grows a learn section
with a threshold field (default 1e-7).
Also: user/mod.rs no longer uses try_lock() for the per-tick
unconscious/mind state sync — we fixed the locking hot paths that
made try_lock necessary, so lock().await is now the right choice.
And subconscious::learn::score_finetune_candidates now returns
(candidates, max_divergence) so the stats can be populated.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 11:49:26 -04:00
|
|
|
learn: LearnConfig::default(),
|
2026-04-09 11:45:39 -04:00
|
|
|
mcp_servers: Vec::new(),
|
2026-04-09 12:07:50 -04:00
|
|
|
lsp_servers: Vec::new(),
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
/// Resolved, ready-to-use agent session config.
|
|
|
|
|
pub struct SessionConfig {
|
2026-03-25 00:52:41 -04:00
|
|
|
pub api_base: String,
|
|
|
|
|
pub api_key: String,
|
|
|
|
|
pub model: String,
|
2026-04-15 03:06:23 -04:00
|
|
|
/// Identity/personality nodes as (name, content) pairs.
|
2026-03-25 00:52:41 -04:00
|
|
|
pub context_parts: Vec<(String, String)>,
|
|
|
|
|
pub session_dir: PathBuf,
|
|
|
|
|
pub app: AppConfig,
|
2026-04-04 23:06:25 -04:00
|
|
|
/// Disable background agents (surface, observe, scoring)
|
|
|
|
|
pub no_agents: bool,
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A fully resolved model ready to construct an ApiClient.
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub struct ResolvedModel {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub api_base: String,
|
|
|
|
|
pub api_key: String,
|
|
|
|
|
pub model_id: String,
|
|
|
|
|
pub context_window: Option<usize>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AppConfig {
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
/// Resolve the active backend and assemble prompts into a SessionConfig.
|
2026-04-13 14:55:41 -04:00
|
|
|
pub async fn resolve(&self, cli: &crate::user::CliArgs) -> Result<SessionConfig> {
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
if self.backends.is_empty() {
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
anyhow::bail!(
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
"no backends configured in {}. Add a `backends` section with at least one entry.",
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
config_path().display()
|
|
|
|
|
);
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
let name = cli.model.as_deref().unwrap_or(&self.default_backend);
|
|
|
|
|
let resolved = self.resolve_model(name)?;
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
|
2026-04-15 02:37:49 -04:00
|
|
|
let personality_nodes = get().personality_nodes.clone();
|
2026-04-15 03:06:23 -04:00
|
|
|
let context_parts = crate::mind::identity::personality_nodes(&personality_nodes).await;
|
2026-03-25 00:52:41 -04:00
|
|
|
|
|
|
|
|
let session_dir = dirs::home_dir()
|
|
|
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
2026-03-27 21:32:28 -04:00
|
|
|
.join(".consciousness/agent-sessions");
|
2026-03-25 00:52:41 -04:00
|
|
|
std::fs::create_dir_all(&session_dir).ok();
|
|
|
|
|
|
2026-03-29 20:58:53 -04:00
|
|
|
// CLI --api-base and --api-key override everything
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
let api_base = cli.api_base.clone().unwrap_or(resolved.api_base);
|
|
|
|
|
let api_key = cli.api_key.clone().unwrap_or(resolved.api_key);
|
2026-03-29 20:58:53 -04:00
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
Ok(SessionConfig {
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
api_base,
|
|
|
|
|
api_key,
|
|
|
|
|
model: resolved.model_id,
|
2026-04-12 01:23:50 -04:00
|
|
|
context_parts,
|
2026-03-25 00:52:41 -04:00
|
|
|
session_dir,
|
|
|
|
|
app: self.clone(),
|
2026-04-04 23:06:25 -04:00
|
|
|
no_agents: cli.no_agents,
|
2026-03-25 00:52:41 -04:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
/// Look up a named backend and resolve its credentials.
|
2026-03-25 00:52:41 -04:00
|
|
|
pub fn resolve_model(&self, name: &str) -> Result<ResolvedModel> {
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
let b = self.backends.get(name)
|
2026-03-25 00:52:41 -04:00
|
|
|
.ok_or_else(|| anyhow::anyhow!(
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
"Unknown backend '{}'. Available: {}",
|
2026-03-25 00:52:41 -04:00
|
|
|
name,
|
|
|
|
|
self.model_names().join(", "),
|
|
|
|
|
))?;
|
|
|
|
|
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
let api_base = b.base_url.clone()
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
.ok_or_else(|| anyhow::anyhow!(
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
"backends.{}.base_url not set in {}",
|
|
|
|
|
name, config_path().display()
|
config: drop dead code and collapse to a single backend
Config had accumulated several obsolete fields, a legacy load path
that was just returning defaults, and multi-backend infrastructure
that's no longer used.
Removed from Config (memory section):
- load_legacy_jsonl() — just returned Config::default(), no callers
- The legacy-fallback branch in load_from_file
- surface_hooks, surface_timeout_secs — zero external readers
- scoring_chunk_tokens + default fn — zero external readers
- The POC_MEMORY_CONFIG env override note in the header comment
(not actually wired up anywhere)
Collapsed multi-backend to single-backend:
- AppConfig used to carry `anthropic: BackendConfig` and
`openrouter: BackendConfig` as required fields plus an optional
`deepinfra`, picked between at runtime by name. Only one is ever
actually used in any deployment. Collapse to a single
`backend: BackendConfig` on AppConfig, drop the multi-backend
match logic in resolve_model, drop the top-level `backend: String`
selector field, drop the `BackendConfig::resolve` fallback path.
- Also drop BackendConfig.model (redundant with ModelConfig.model_id
once multi-backend is gone).
- ModelConfig.backend field goes — there's only one backend now, no
choice to make.
Dead prompt_file machinery:
- ModelConfig.prompt_file, ResolvedModel.prompt_file, SessionConfig
.prompt_file, Agent.prompt_file — nothing in the codebase actually
reads the file these strings name. Just passed around and compared.
Delete the whole string through every struct.
- The "if prompt_file changed on model switch, recompact" branch in
user/chat.rs goes too (never fired usefully).
Dead memory_project plumbing:
- AppConfig.memory_project field, CliArgs.memory_project, the
--memory-project CLI flag, the figment merge target, the show_config
display line. Nothing reads it anywhere.
Dead ContextInfo struct:
- `struct ContextInfo` was never constructed — context_info: None
was the only initializer. The conditional display blocks in
user/context.rs that dereferenced it were dead.
Behavior change: AppConfig::resolve() now requires a non-empty
`models` map and bails with a helpful message if it's missing. The
old fallback ("no models? use top-level backend + PromptConfig to
build a default") path is gone — it was only kept for symmetry with
a mode nobody used.
Config file shape: `deepinfra: {...}` → `backend: {...}`, and
model entries no longer need `backend:` or `prompt_file:`. Updated
~/.consciousness/config.json5 to match.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:41:55 -04:00
|
|
|
))?;
|
2026-03-25 00:52:41 -04:00
|
|
|
|
|
|
|
|
Ok(ResolvedModel {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
api_base,
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
api_key: b.api_key.clone(),
|
|
|
|
|
model_id: b.model_id.clone(),
|
|
|
|
|
context_window: b.context_window,
|
2026-03-25 00:52:41 -04:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
/// List available backend names, sorted.
|
2026-03-25 00:52:41 -04:00
|
|
|
pub fn model_names(&self) -> Vec<String> {
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
let mut names: Vec<_> = self.backends.keys().cloned().collect();
|
2026-03-25 00:52:41 -04:00
|
|
|
names.sort();
|
|
|
|
|
names
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
// ============================================================
|
|
|
|
|
// Figment-based agent config loading
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
struct Json5File(PathBuf);
|
|
|
|
|
|
|
|
|
|
impl Provider for Json5File {
|
|
|
|
|
fn metadata(&self) -> figment::Metadata {
|
|
|
|
|
figment::Metadata::named(format!("JSON5 file ({})", self.0.display()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn data(&self) -> figment::Result<figment::value::Map<figment::Profile, figment::value::Dict>> {
|
|
|
|
|
match std::fs::read_to_string(&self.0) {
|
|
|
|
|
Ok(content) => {
|
config_writer: emit pretty multi-line sections, drop json5 crate
Previously when append_kvp created a new section or added a key, it
stuffed the "\n " separator into the new kvp's wsc.0 (the whitespace
between its own key and colon) instead of the prior kvp's wsc.3 (the
whitespace after the prior trailing comma). Result looked like:
lsp_servers: [...],
learn
: {generate_alternates
: true,},}
The writer also didn't set any interior whitespace on the new section's
JSONObjectContext, so everything crammed onto one line — `{key: val,}`
compact, not `{\n key: val,\n}` multi-line.
Rewrote the appender as append_kvp_pretty(object, key, value,
inner_indent, outer_indent):
- separator between kvps goes in the prior kvp's wsc.3, or if we're the
first kvp in a fresh object, in the object's own wsc.0 (after its
opening `{`)
- new kvp's wsc.3 carries `,\n<outer_indent>` so the parent's closing
`}` lands correctly indented
- interior indent vs outer indent are both explicit, so we don't have
to rewrite this logic every time we add another nesting level
New tests: new_section_exact_multiline_layout asserts byte-exact
output shape; new_section_and_key_format_cleanly verifies no key wraps
to the next line. Prior tests just substring-matched and happily passed
on the broken output — that's why this shipped in the first place.
Also: dropped the json5 crate dependency. json-five's serde feature
(default) provides the same from_str / to_string API. One fewer
dependency, and the two were doing the same job.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 13:08:19 -04:00
|
|
|
let value: figment::value::Value = json_five::from_str(&content)
|
2026-03-25 01:23:12 -04:00
|
|
|
.map_err(|e| figment::Error::from(format!("{}: {}", self.0.display(), e)))?;
|
|
|
|
|
Serialized::defaults(value).data()
|
|
|
|
|
}
|
|
|
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(figment::value::Map::new()),
|
|
|
|
|
Err(e) => Err(figment::Error::from(format!("{}: {}", self.0.display(), e))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macro_rules! merge_opt {
|
|
|
|
|
($fig:expr, $val:expr, $($key:expr),+) => {
|
|
|
|
|
if let Some(ref v) = $val {
|
|
|
|
|
$( $fig = $fig.merge(Serialized::default($key, v)); )+
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 02:46:32 -04:00
|
|
|
fn build_figment(cli: &crate::user::CliArgs) -> Figment {
|
2026-03-25 01:23:12 -04:00
|
|
|
let mut f = Figment::from(Serialized::defaults(AppConfig::default()))
|
|
|
|
|
.merge(Json5File(config_path()));
|
|
|
|
|
|
|
|
|
|
merge_opt!(f, cli.dmn_max_turns, "dmn.max_turns");
|
|
|
|
|
if cli.debug {
|
|
|
|
|
f = f.merge(Serialized::default("debug", true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 00:52:41 -04:00
|
|
|
/// Load just the AppConfig — no validation, no prompt assembly.
|
2026-04-16 12:53:22 -04:00
|
|
|
/// Also installs the loaded AppConfig into the global cache so
|
|
|
|
|
/// `config::app()` is available everywhere.
|
2026-04-04 02:46:32 -04:00
|
|
|
pub fn load_app(cli: &crate::user::CliArgs) -> Result<(AppConfig, Figment)> {
|
2026-03-25 00:52:41 -04:00
|
|
|
let figment = build_figment(cli);
|
|
|
|
|
let app: AppConfig = figment.extract().context("Failed to load configuration")?;
|
2026-04-16 12:53:22 -04:00
|
|
|
install_app(app.clone());
|
2026-03-25 00:52:41 -04:00
|
|
|
Ok((app, figment))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 12:53:22 -04:00
|
|
|
// ============================================================
|
|
|
|
|
// Global AppConfig cache (writable, for runtime-mutable settings
|
|
|
|
|
// like learn.threshold that F6 edits via config_writer).
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
static APP_CONFIG: OnceLock<RwLock<AppConfig>> = OnceLock::new();
|
|
|
|
|
|
|
|
|
|
fn install_app(app: AppConfig) {
|
|
|
|
|
let slot = APP_CONFIG.get_or_init(|| RwLock::new(app.clone()));
|
|
|
|
|
*slot.write().unwrap() = app;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Current AppConfig, held under a read lock. Reads should be brief
|
|
|
|
|
/// (no holding across await / long work) to avoid starving writers.
|
|
|
|
|
/// Panics if called before load_app — which runs once at startup.
|
|
|
|
|
pub fn app() -> std::sync::RwLockReadGuard<'static, AppConfig> {
|
|
|
|
|
APP_CONFIG
|
|
|
|
|
.get()
|
|
|
|
|
.expect("config::app() called before load_app()")
|
|
|
|
|
.read()
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Mutate the cached AppConfig in place. Used by config_writer to keep
|
|
|
|
|
/// the in-memory view in sync with disk after surgical edits to
|
|
|
|
|
/// ~/.consciousness/config.json5.
|
|
|
|
|
pub fn update_app(f: impl FnOnce(&mut AppConfig)) {
|
|
|
|
|
let slot = APP_CONFIG.get().expect("update_app before load_app");
|
|
|
|
|
f(&mut *slot.write().unwrap());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 00:52:41 -04:00
|
|
|
/// Load the full config: figment → AppConfig → resolve backend → assemble prompts.
|
2026-04-13 14:55:41 -04:00
|
|
|
pub async fn load_session(cli: &crate::user::CliArgs) -> Result<(SessionConfig, Figment)> {
|
2026-03-25 00:52:41 -04:00
|
|
|
let (app, figment) = load_app(cli)?;
|
2026-04-13 14:55:41 -04:00
|
|
|
let config = app.resolve(cli).await?;
|
2026-03-25 00:52:41 -04:00
|
|
|
Ok((config, figment))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 03:06:23 -04:00
|
|
|
/// Re-assemble context (reload personality nodes).
|
|
|
|
|
pub async fn reload_context() -> Result<Vec<(String, String)>> {
|
2026-04-15 02:37:49 -04:00
|
|
|
let personality_nodes = get().personality_nodes.clone();
|
2026-04-15 03:06:23 -04:00
|
|
|
let context_parts = crate::mind::identity::personality_nodes(&personality_nodes).await;
|
2026-04-12 01:23:50 -04:00
|
|
|
Ok(context_parts)
|
2026-03-25 00:52:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn show_config(app: &AppConfig, figment: &Figment) {
|
|
|
|
|
fn mask(key: &str) -> String {
|
|
|
|
|
if key.is_empty() { "(not set)".into() }
|
|
|
|
|
else if key.len() <= 8 { "****".into() }
|
|
|
|
|
else { format!("{}...{}", &key[..4], &key[key.len() - 4..]) }
|
|
|
|
|
}
|
|
|
|
|
fn src(figment: &Figment, key: &str) -> String {
|
|
|
|
|
figment.find_metadata(key).map_or("default".into(), |m| m.name.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("# Effective configuration\n");
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
println!("debug: {} ({})", app.debug, src(figment, "debug"));
|
2026-03-25 00:52:41 -04:00
|
|
|
println!("\ncompaction:");
|
|
|
|
|
println!(" hard_threshold_pct: {} ({})", app.compaction.hard_threshold_pct, src(figment, "compaction.hard_threshold_pct"));
|
|
|
|
|
println!(" soft_threshold_pct: {} ({})", app.compaction.soft_threshold_pct, src(figment, "compaction.soft_threshold_pct"));
|
|
|
|
|
println!("\ndmn:");
|
|
|
|
|
println!(" max_turns: {} ({})", app.dmn.max_turns, src(figment, "dmn.max_turns"));
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
println!("\ndefault_backend: {:?} ({})", app.default_backend, src(figment, "default_backend"));
|
|
|
|
|
if !app.backends.is_empty() {
|
|
|
|
|
println!("\nbackends:");
|
|
|
|
|
let mut names: Vec<_> = app.backends.keys().cloned().collect();
|
|
|
|
|
names.sort();
|
|
|
|
|
for name in names {
|
|
|
|
|
let b = &app.backends[&name];
|
2026-03-25 00:52:41 -04:00
|
|
|
println!(" {}:", name);
|
config: merge ModelConfig into BackendConfig, keyed by name
AppConfig had one BackendConfig for credentials and a separate
HashMap<String, ModelConfig> for named model entries. In practice each
named model was always paired with exactly one backend's credentials
— the split bought nothing except an extra struct and the awkward
two-lookup shape in resolve_model (find model → get backend creds →
combine).
Merge them: BackendConfig now carries api_key, base_url, model_id,
and context_window. AppConfig has a single
HashMap<String, BackendConfig> backends map and a default_backend
name. resolve_model is one lookup.
ModelConfig struct deleted. default_model renamed to default_backend.
Config shape changes from
backend: { api_key, base_url }
models: { "27b": { model_id, context_window } }
default_model: "27b"
to
backends: { "27b": { api_key, base_url, model_id, context_window } }
default_backend: "27b"
Updated ~/.consciousness/config.json5 to match.
One small side effect: dropped the --api-key / --api-base figment
merge-opts for "backend.*" targets — those would need to know which
backend to target now and there's no sensible default. The CLI flags
still function as post-resolution overrides on the eventual
SessionConfig.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-16 15:49:53 -04:00
|
|
|
println!(" api_key: {} ({})", mask(&b.api_key), src(figment, &format!("backends.{name}.api_key")));
|
|
|
|
|
if let Some(ref url) = b.base_url {
|
|
|
|
|
println!(" base_url: {:?} ({})", url, src(figment, &format!("backends.{name}.base_url")));
|
|
|
|
|
}
|
|
|
|
|
println!(" model_id: {:?}", b.model_id);
|
|
|
|
|
if let Some(cw) = b.context_window {
|
2026-03-25 00:52:41 -04:00
|
|
|
println!(" context_window: {}", cw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 01:23:12 -04:00
|
|
|
// ============================================================
|
|
|
|
|
// Helpers
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
fn deserialize_path<'de, D: serde::Deserializer<'de>>(d: D) -> Result<PathBuf, D::Error> {
|
|
|
|
|
let s: String = serde::Deserialize::deserialize(d)?;
|
|
|
|
|
Ok(expand_home(&s))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn expand_home(path: &str) -> PathBuf {
|
|
|
|
|
if let Some(rest) = path.strip_prefix("~/") {
|
2026-04-02 19:57:40 -04:00
|
|
|
dirs::home_dir().unwrap_or_default().join(rest)
|
2026-03-25 01:23:12 -04:00
|
|
|
} else {
|
|
|
|
|
PathBuf::from(path)
|
|
|
|
|
}
|
|
|
|
|
}
|