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>
This commit is contained in:
parent
18b7fd0535
commit
dd551fe551
4 changed files with 235 additions and 11 deletions
|
|
@ -166,6 +166,63 @@ pub fn reload() -> bool {
|
|||
changed
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Agent config (top-level settings)
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -228,6 +228,9 @@ fn restore_terminal(terminal: &mut ratatui::Terminal<CrosstermBackend<io::Stdout
|
|||
async fn start(cli: crate::user::CliArgs) -> Result<()> {
|
||||
let (config, _figment) = crate::config::load_session(&cli).await?;
|
||||
|
||||
// Pick up external edits (vim, F6 hotkeys, etc.) without restart.
|
||||
crate::config::watch_config(cli.clone());
|
||||
|
||||
if config.app.debug {
|
||||
unsafe { std::env::set_var("POC_DEBUG", "1") };
|
||||
}
|
||||
|
|
@ -599,7 +602,7 @@ async fn run(
|
|||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser, Debug, Default)]
|
||||
#[derive(Parser, Debug, Default, Clone)]
|
||||
#[command(name = "consciousness", about = "Substrate-independent AI agent")]
|
||||
pub struct CliArgs {
|
||||
/// Model override (selects a named entry from `models` in config.json5)
|
||||
|
|
@ -634,7 +637,7 @@ pub struct CliArgs {
|
|||
pub command: Option<SubCmd>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum SubCmd {
|
||||
/// Print new output since last read and exit
|
||||
Read {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue