From 55fdc3dad77ac3df6e1dc00ad106fd04e5e06371 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Sun, 8 Mar 2026 18:31:51 -0400 Subject: [PATCH] idle: afk command, configurable session timeout, fix block_reason Add `poc-daemon afk` to immediately mark Kent as away, allowing the idle timer to fire without waiting for the session active timeout. Add `poc-daemon session-timeout ` to configure how long after the last message Kent counts as "present" (default 15min, persisted). Fix block_reason() to report "kent present" and "in turn" states that were checked in the tick but not in the diagnostic output. --- schema/daemon.capnp | 2 ++ src/bin/poc-daemon/idle.rs | 30 ++++++++++++++++++++++++++++-- src/bin/poc-daemon/main.rs | 17 +++++++++++++++++ src/bin/poc-daemon/rpc.rs | 19 +++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/schema/daemon.capnp b/schema/daemon.capnp index 23ec212..be894c7 100644 --- a/schema/daemon.capnp +++ b/schema/daemon.capnp @@ -73,6 +73,8 @@ interface Daemon { debug @18 () -> (json :Text); ewma @20 (value :Float64) -> (current :Float64); + afk @21 () -> (); + sessionTimeout @22 (seconds :Float64) -> (); # Modules moduleCommand @15 (module :Text, command :Text, args :List(Text)) diff --git a/src/bin/poc-daemon/idle.rs b/src/bin/poc-daemon/idle.rs index bd78f73..7b3cc5c 100644 --- a/src/bin/poc-daemon/idle.rs +++ b/src/bin/poc-daemon/idle.rs @@ -15,7 +15,7 @@ use tracing::info; // Defaults const DEFAULT_IDLE_TIMEOUT: f64 = 5.0 * 60.0; const DEFAULT_NOTIFY_TIMEOUT: f64 = 2.0 * 60.0; -const SESSION_ACTIVE_SECS: f64 = 15.0 * 60.0; +const DEFAULT_SESSION_ACTIVE_SECS: f64 = 15.0 * 60.0; const DREAM_INTERVAL_HOURS: u64 = 18; /// EWMA decay half-life in seconds (5 minutes). @@ -52,6 +52,8 @@ struct Persisted { #[serde(default)] ewma_updated_at: f64, #[serde(default)] + session_active_secs: f64, + #[serde(default)] in_turn: bool, #[serde(default)] turn_start: f64, @@ -110,6 +112,7 @@ pub struct State { pub notify_timeout: f64, pub activity_ewma: f64, pub ewma_updated_at: f64, + pub session_active_secs: f64, pub in_turn: bool, pub turn_start: f64, pub last_nudge: f64, @@ -135,6 +138,7 @@ impl State { fired: false, idle_timeout: DEFAULT_IDLE_TIMEOUT, notify_timeout: DEFAULT_NOTIFY_TIMEOUT, + session_active_secs: DEFAULT_SESSION_ACTIVE_SECS, activity_ewma: 0.0, ewma_updated_at: now(), in_turn: false, @@ -157,6 +161,9 @@ impl State { if p.notify_timeout > 0.0 { self.notify_timeout = p.notify_timeout; } + if p.session_active_secs > 0.0 { + self.session_active_secs = p.session_active_secs; + } // Reset activity timestamps to now — timers count from // restart, not from stale pre-restart state let t = now(); @@ -197,6 +204,7 @@ impl State { fired: self.fired, idle_timeout: self.idle_timeout, notify_timeout: self.notify_timeout, + session_active_secs: self.session_active_secs, activity_ewma: self.activity_ewma, ewma_updated_at: self.ewma_updated_at, in_turn: self.in_turn, @@ -284,6 +292,20 @@ impl State { } } + pub fn handle_afk(&mut self) { + // Push last_user_msg far enough back that kent_present() returns false + self.last_user_msg = now() - self.session_active_secs - 1.0; + self.fired = false; // allow idle timer to fire again + info!("Kent marked AFK"); + self.save(); + } + + pub fn handle_session_timeout(&mut self, secs: f64) { + self.session_active_secs = secs; + info!("session active timeout = {secs}s"); + self.save(); + } + pub fn handle_idle_timeout(&mut self, secs: f64) { self.idle_timeout = secs; self.save(); @@ -331,7 +353,7 @@ impl State { } pub fn kent_present(&self) -> bool { - (now() - self.last_user_msg) < SESSION_ACTIVE_SECS + (now() - self.last_user_msg) < self.session_active_secs } /// Seconds since the most recent of user message or response. @@ -353,6 +375,10 @@ impl State { "consolidating" } else if self.dreaming { "dreaming" + } else if self.kent_present() { + "kent present" + } else if self.in_turn { + "in turn" } else if self.last_response.max(self.last_user_msg) == 0.0 { "no activity yet" } else if self.since_activity() < self.idle_timeout { diff --git a/src/bin/poc-daemon/main.rs b/src/bin/poc-daemon/main.rs index 4e91b34..31bd01a 100644 --- a/src/bin/poc-daemon/main.rs +++ b/src/bin/poc-daemon/main.rs @@ -84,6 +84,13 @@ enum Command { /// Duration in seconds seconds: Option, }, + /// Mark Kent as AFK (immediately allow idle timer to fire) + Afk, + /// Set session active timeout in seconds (how long after last message Kent counts as "present") + SessionTimeout { + /// Timeout in seconds + seconds: f64, + }, /// Set idle timeout in seconds (how long before autonomous prompt) IdleTimeout { /// Timeout in seconds @@ -249,6 +256,16 @@ async fn client_main(cmd: Command) -> Result<(), Box> { req.get().set_seconds(seconds.unwrap_or(300)); req.send().promise.await?; } + Command::Afk => { + daemon.afk_request().send().promise.await?; + println!("marked AFK"); + } + Command::SessionTimeout { seconds } => { + let mut req = daemon.session_timeout_request(); + req.get().set_seconds(seconds); + req.send().promise.await?; + println!("session timeout = {seconds}s"); + } Command::IdleTimeout { seconds } => { let mut req = daemon.idle_timeout_request(); req.get().set_seconds(seconds); diff --git a/src/bin/poc-daemon/rpc.rs b/src/bin/poc-daemon/rpc.rs index 37c70d4..599bdfe 100644 --- a/src/bin/poc-daemon/rpc.rs +++ b/src/bin/poc-daemon/rpc.rs @@ -126,6 +126,25 @@ impl daemon::Server for DaemonImpl { Promise::ok(()) } + fn afk( + &mut self, + _params: daemon::AfkParams, + _results: daemon::AfkResults, + ) -> Promise<(), capnp::Error> { + self.state.borrow_mut().handle_afk(); + Promise::ok(()) + } + + fn session_timeout( + &mut self, + params: daemon::SessionTimeoutParams, + _results: daemon::SessionTimeoutResults, + ) -> Promise<(), capnp::Error> { + let secs = pry!(params.get()).get_seconds(); + self.state.borrow_mut().handle_session_timeout(secs); + Promise::ok(()) + } + fn idle_timeout( &mut self, params: daemon::IdleTimeoutParams,