From 57bd5b6d8b1c03d208f1ec91a5f5f3c23f493838 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Sat, 11 Apr 2026 14:35:08 -0400 Subject: [PATCH] idle: per-instance state path, extensible extra fields Move state_path to a field on State (default thalamus-state.json) so the Claude daemon can use its own file without collision. Add a serde(flatten) extra map to Persisted so callers can round-trip additional fields (e.g. claude_pane) through save/load. save() is now &mut self. Co-Authored-By: Proof of Concept --- src/thalamus/idle.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/thalamus/idle.rs b/src/thalamus/idle.rs index ad16f7e..3af43cf 100644 --- a/src/thalamus/idle.rs +++ b/src/thalamus/idle.rs @@ -53,6 +53,8 @@ struct Persisted { turn_start: f64, #[serde(default)] last_nudge: f64, + #[serde(flatten)] + extra: serde_json::Map, // Human-readable mirrors #[serde(default, skip_deserializing)] last_user_msg_time: String, @@ -66,8 +68,8 @@ struct Persisted { uptime: f64, } -fn state_path() -> std::path::PathBuf { - home().join(".consciousness/daemon-state.json") +pub fn default_state_path() -> std::path::PathBuf { + home().join(".consciousness/thalamus-state.json") } /// Compute EWMA decay factor: 0.5^(elapsed / half_life). @@ -113,6 +115,10 @@ pub struct State { #[serde(skip)] pub start_time: f64, #[serde(skip)] + pub state_path: std::path::PathBuf, + #[serde(skip)] + pub extra: serde_json::Map, + #[serde(skip)] pub notifications: notify::NotifyState, } @@ -137,12 +143,14 @@ impl State { last_nudge: 0.0, running: true, start_time: now(), + state_path: default_state_path(), + extra: serde_json::Map::new(), notifications: notify::NotifyState::new(), } } pub fn load(&mut self) { - if let Ok(data) = fs::read_to_string(state_path()) { + if let Ok(data) = fs::read_to_string(&self.state_path) { if let Ok(p) = serde_json::from_str::(&data) { self.sleep_until = p.sleep_until; if p.idle_timeout > 0.0 { @@ -163,12 +171,17 @@ impl State { self.in_turn = p.in_turn; self.turn_start = p.turn_start; self.last_nudge = p.last_nudge; + // Filter out known Persisted fields that leak into extra via flatten + self.extra = p.extra; + for key in ["last_user_msg_time", "last_response_time", "saved_at", "fired", "uptime"] { + self.extra.remove(key); + } info!("loaded idle state"); } } } - pub fn save(&self) { + pub fn save(&mut self) { let p = Persisted { last_user_msg: self.last_user_msg, last_response: self.last_response, @@ -181,15 +194,15 @@ impl State { in_turn: self.in_turn, turn_start: self.turn_start, last_nudge: self.last_nudge, + extra: self.extra.clone(), last_user_msg_time: epoch_to_iso(self.last_user_msg), last_response_time: epoch_to_iso(self.last_response), saved_at: epoch_to_iso(now()), fired: self.fired, uptime: now() - self.start_time, - ..Default::default() }; if let Ok(json) = serde_json::to_string_pretty(&p) { - let _ = fs::write(state_path(), json); + let _ = fs::write(&self.state_path, json); } }