idle: persist timeout, suppress restart fire, add debug/save/idle-timeout commands

Several idle timer fixes and new inspection capabilities:

- Persist idle_timeout across daemon restarts (was reverting to 5min default)
- Set fired=true on load to suppress immediate fire from stale timestamps
- Add human-readable ISO timestamps to daemon-state.json for debugging
- Use to_string_pretty for readable state file
- Make save() public for RPC access
- Remove kb_idle_minutes() — go purely off message timestamps
- Add maybe_prompt_notification() with idle gate so notifications only
  inject via tmux when truly idle, not during active sessions
- Add debug_json() for full state inspection with computed values
  (would_fire, block_reason, all timers)

New RPC commands (schema @16-18):
  poc-daemon idle-timeout <secs>  — set idle timeout
  poc-daemon save                 — force state persistence
  poc-daemon debug                — dump full internal state as JSON

Also: save state on clean shutdown, route module notifications through
maybe_prompt_notification before submitting to queue.

Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-03-05 21:16:19 -05:00 committed by Kent Overstreet
parent 8662759d53
commit eab656aa64
4 changed files with 207 additions and 30 deletions

View file

@ -134,6 +134,15 @@ async fn client_main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>
req.get().set_seconds(secs);
req.send().promise.await?;
}
"idle-timeout" => {
let secs: f64 = args.get(2)
.and_then(|s| s.parse().ok())
.ok_or("usage: poc-daemon idle-timeout <seconds>")?;
let mut req = daemon.idle_timeout_request();
req.get().set_seconds(secs);
req.send().promise.await?;
println!("idle timeout = {secs}s");
}
"consolidating" => {
daemon.consolidating_request().send().promise.await?;
}
@ -146,6 +155,20 @@ async fn client_main(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>
"dream-end" => {
daemon.dream_end_request().send().promise.await?;
}
"save" => {
daemon.save_request().send().promise.await?;
println!("state saved");
}
"debug" => {
let reply = daemon.debug_request().send().promise.await?;
let json = reply.get()?.get_json()?.to_str()?;
// Pretty-print
if let Ok(v) = serde_json::from_str::<serde_json::Value>(json) {
println!("{}", serde_json::to_string_pretty(&v).unwrap_or_else(|_| json.to_string()));
} else {
println!("{json}");
}
}
"stop" => {
daemon.stop_request().send().promise.await?;
println!("stopping");
@ -350,6 +373,9 @@ async fn server_main() -> Result<(), Box<dyn std::error::Error>> {
// Drain module notifications into state
Some(notif) = notify_rx.recv() => {
state.borrow().maybe_prompt_notification(
&notif.ntype, notif.urgency, &notif.message,
);
state.borrow_mut().notifications.submit(
notif.ntype,
notif.urgency,
@ -400,6 +426,7 @@ async fn server_main() -> Result<(), Box<dyn std::error::Error>> {
}
}
state.borrow().save();
let _ = std::fs::remove_file(sock_path());
let _ = std::fs::remove_file(pid_path());
info!("daemon stopped");
@ -422,8 +449,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
match args[1].as_str() {
"status" | "user" | "response" | "sleep" | "wake" | "quiet"
| "consolidating" | "consolidated" | "dream-start" | "dream-end"
| "stop" | "notify" | "notifications" | "notify-types"
| "notify-threshold" | "irc" | "telegram" => client_main(args).await,
| "stop" | "save" | "notify" | "notifications" | "notify-types"
| "notify-threshold" | "idle-timeout"
| "irc" | "telegram" => client_main(args).await,
_ => {
eprintln!("usage: poc-daemon [command]");
eprintln!(" (no args) Start daemon");
@ -435,6 +463,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!(" notify-types List notification types");
eprintln!(" notify-threshold <type> <level>");
eprintln!(" sleep [timestamp]");
eprintln!(" save Force state persistence");
eprintln!(" wake / quiet / stop / dream-start / dream-end");
eprintln!(" irc <join|leave|send|status|log|nick> [args]");
std::process::exit(1);