config: hot-reload via RPC, Arc<Config> for cheap sharing

Config is now stored in RwLock<Arc<Config>> instead of OnceLock<Config>.
get() returns Arc<Config> (cheap clone), and reload() re-reads from disk.

New RPC: "reload-config" — reloads config.jsonl without restarting
the daemon. Logs the change to daemon.log. Useful for switching
between API backends and claude accounts without losing in-flight
tasks.

New CLI: poc-memory agent daemon reload-config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-19 13:41:13 -04:00
parent 0944ecc43f
commit af3171d6ec
4 changed files with 43 additions and 7 deletions

View file

@ -1074,6 +1074,18 @@ pub fn run_daemon() -> Result<(), String> {
}); });
} }
daemon.add_rpc_handler(|cmd, _ctx| {
if cmd != "reload-config" { return None; }
let changed = crate::config::reload();
let config = crate::config::get();
let api = config.api_base_url.as_deref().unwrap_or("(none)");
let model = config.api_model.as_deref().unwrap_or("(default)");
log_event("daemon", "config-reload",
&format!("changed={}, api={}, model={}", changed, api, model));
Some(format!("{{\"ok\":true,\"changed\":{},\"api_base_url\":\"{}\",\"api_model\":\"{}\"}}\n",
changed, api, model))
});
daemon.add_rpc_handler(|cmd, _ctx| { daemon.add_rpc_handler(|cmd, _ctx| {
if !cmd.starts_with("record-hits ") { return None; } if !cmd.starts_with("record-hits ") { return None; }
let keys: Vec<&str> = cmd.strip_prefix("record-hits ") let keys: Vec<&str> = cmd.strip_prefix("record-hits ")

View file

@ -268,7 +268,7 @@ pub fn cmd_load_context(stats: bool) -> Result<(), String> {
println!("{}", "-".repeat(42)); println!("{}", "-".repeat(42));
for group in &cfg.context_groups { for group in &cfg.context_groups {
let entries = get_group_content(group, &store, cfg); let entries = get_group_content(group, &store, &cfg);
let words: usize = entries.iter() let words: usize = entries.iter()
.map(|(_, c)| c.split_whitespace().count()) .map(|(_, c)| c.split_whitespace().count())
.sum(); .sum();
@ -287,7 +287,7 @@ pub fn cmd_load_context(stats: bool) -> Result<(), String> {
println!(); println!();
for group in &cfg.context_groups { for group in &cfg.context_groups {
let entries = get_group_content(group, &store, cfg); let entries = get_group_content(group, &store, &cfg);
if !entries.is_empty() && group.source == crate::config::ContextSource::Journal { if !entries.is_empty() && group.source == crate::config::ContextSource::Journal {
println!("--- recent journal entries ({}/{}) ---", println!("--- recent journal entries ({}/{}) ---",
entries.len(), cfg.journal_max); entries.len(), cfg.journal_max);

View file

@ -13,9 +13,9 @@
// {"group": "orientation", "keys": ["where-am-i.md"], "source": "file"} // {"group": "orientation", "keys": ["where-am-i.md"], "source": "file"}
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock; use std::sync::{Arc, OnceLock, RwLock};
static CONFIG: OnceLock<Config> = OnceLock::new(); static CONFIG: OnceLock<RwLock<Arc<Config>>> = OnceLock::new();
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum ContextSource { pub enum ContextSource {
@ -210,7 +210,23 @@ fn expand_home(path: &str) -> PathBuf {
} }
} }
/// Get the global config (loaded once on first access). /// Get the global config (cheap Arc clone).
pub fn get() -> &'static Config { pub fn get() -> Arc<Config> {
CONFIG.get_or_init(Config::load_from_file) 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
} }

View file

@ -469,6 +469,8 @@ enum DaemonCmd {
}, },
/// Interactive TUI /// Interactive TUI
Tui, Tui,
/// Reload config file without restarting
ReloadConfig,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@ -1057,6 +1059,12 @@ fn cmd_daemon(sub: DaemonCmd) -> Result<(), String> {
DaemonCmd::Consolidate => daemon::rpc_consolidate(), DaemonCmd::Consolidate => daemon::rpc_consolidate(),
DaemonCmd::Run { agent, count } => daemon::rpc_run_agent(&agent, count), DaemonCmd::Run { agent, count } => daemon::rpc_run_agent(&agent, count),
DaemonCmd::Tui => tui::run_tui(), DaemonCmd::Tui => tui::run_tui(),
DaemonCmd::ReloadConfig => {
match daemon::send_rpc_pub("reload-config") {
Some(resp) => { eprintln!("{}", resp.trim()); Ok(()) }
None => Err("daemon not running".into()),
}
}
} }
} }