loopd: [heartbeat] table and summarise name check

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-03 14:00:57 -07:00
co-authored by ornith-1.5-35b-a3b
parent ab4fb65ccc
commit 6b7912399c
6 changed files with 162 additions and 1 deletions
+36
View File
@@ -177,6 +177,8 @@ pub struct Config {
pub memory: Memory,
#[serde(default)]
pub compact: Compact,
#[serde(default)]
pub heartbeat: Heartbeat,
}
/// `[memory]`: `memoryd`'s socket for `loopd` (M5a spec, section 9). An empty path means the
@@ -207,6 +209,24 @@ impl Default for Compact {
}
}
/// `[heartbeat]`: how often the standing `heartbeat` session runs (M5d spec, section 4.1).
/// `every_s` 0 disables it.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Heartbeat {
pub every_s: u64,
pub prompt: std::path::PathBuf,
}
impl Default for Heartbeat {
fn default() -> Self {
Self {
every_s: 0,
prompt: std::path::PathBuf::from("heartbeat.md"),
}
}
}
#[derive(Debug)]
pub enum ConfigError {
Read(std::path::PathBuf, std::io::Error),
@@ -235,6 +255,7 @@ impl Config {
toml::from_str(&text).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))?;
if let Some(parent) = path.parent() {
config.baseline.system = parent.join(&config.baseline.system);
config.heartbeat.prompt = parent.join(&config.heartbeat.prompt);
}
Ok(config)
}
@@ -259,4 +280,19 @@ impl Config {
pub fn compact_prompt(&self) -> std::path::PathBuf {
self.baseline.system.with_file_name("compact.md")
}
/// The heartbeat prompt, already joined to the config file's directory by `load`.
pub fn heartbeat_prompt(&self) -> std::path::PathBuf {
self.heartbeat.prompt.clone()
}
/// Unknown `[compact] summarise` names are a config error (M5d spec, section 4.3).
pub fn check_summarise(&self, registry: &crate::tools::Registry) -> Result<(), String> {
for name in &self.compact.summarise {
if registry.get(name).is_none() {
return Err(format!("[compact] summarise: unknown tool {name}"));
}
}
Ok(())
}
}
+10
View File
@@ -58,6 +58,11 @@ fn run_selftest(path: &Path) -> ExitCode {
}
};
if let Err(e) = cfg.check_summarise(&loopd::tools::Registry::m5c()) {
eprintln!("loopd: {e}");
return ExitCode::from(1);
}
let client = Client::new(cfg);
match run_selftest_check(&client) {
Ok(()) => ExitCode::SUCCESS,
@@ -74,6 +79,11 @@ fn run_serve(path: &Path) -> ExitCode {
}
};
if let Err(e) = cfg.check_summarise(&loopd::tools::Registry::m5c()) {
eprintln!("loopd: {e}");
return ExitCode::from(1);
}
// One loopd per home (M5b spec, section 9a). The lock comes before the socket is touched and
// before the self-test, so a second copy neither takes the socket of the one that runs nor
// sends its slot a prompt. Held until the process ends.