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 <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-11 14:35:08 -04:00
parent 193a85bc05
commit 57bd5b6d8b

View file

@ -53,6 +53,8 @@ struct Persisted {
turn_start: f64,
#[serde(default)]
last_nudge: f64,
#[serde(flatten)]
extra: serde_json::Map<String, serde_json::Value>,
// 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<String, serde_json::Value>,
#[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::<Persisted>(&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);
}
}