From 8913eafd7ae5632e2068b99ded720b4a99e62753 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Mon, 16 Mar 2026 17:09:27 -0400 Subject: [PATCH] poc-daemon: fix idle nudge and notification delivery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Strip context bloat from nudge messages — no more IRC digest, git log, or work state inlined into tmux send-keys (was silently dropping the entire message). Nudge now just includes pending notification count. - Notifications no longer send directly via tmux — they flow through the idle nudge only. Urgent notifications reset the fired flag so the nudge fires sooner. - Add test-nudge RPC that exercises the actual daemon send path (test-send was client-side only, didn't test the real code path). - Update nudge text: "Let your feelings guide your thinking." - Increase send-keys sleep from 200ms to 500ms. Co-Authored-By: Claude Opus 4.6 (1M context) --- poc-daemon/schema/daemon.capnp | 2 ++ poc-daemon/src/context.rs | 33 +++++++++------------------------ poc-daemon/src/idle.rs | 16 ++++++++++------ poc-daemon/src/main.rs | 10 +++++++++- poc-daemon/src/rpc.rs | 22 ++++++++++++++++++++++ poc-daemon/src/tmux.rs | 2 +- 6 files changed, 53 insertions(+), 32 deletions(-) diff --git a/poc-daemon/schema/daemon.capnp b/poc-daemon/schema/daemon.capnp index be894c7..8aacb7b 100644 --- a/poc-daemon/schema/daemon.capnp +++ b/poc-daemon/schema/daemon.capnp @@ -76,6 +76,8 @@ interface Daemon { afk @21 () -> (); sessionTimeout @22 (seconds :Float64) -> (); + testNudge @23 () -> (sent :Bool, message :Text); + # Modules moduleCommand @15 (module :Text, command :Text, args :List(Text)) -> (result :Text); diff --git a/poc-daemon/src/context.rs b/poc-daemon/src/context.rs index ff65a3a..59d1253 100644 --- a/poc-daemon/src/context.rs +++ b/poc-daemon/src/context.rs @@ -112,29 +112,14 @@ pub fn irc_digest() -> String { /// Build full context string for a prompt. /// notification_text is passed in from the notify module. -pub fn build(include_irc: bool, notification_text: &str) -> String { - let mut parts = Vec::new(); - - let git = git_context(); - if !git.is_empty() { - parts.push(format!("Context: {git}")); +pub fn build(_include_irc: bool, notification_text: &str) -> String { + // Keep nudges short — Claude checks notifications via + // `poc-daemon status` on its own. Just mention the count. + let count = notification_text.matches("[irc.").count() + + notification_text.matches("[telegram.").count(); + if count > 0 { + format!("{count} pending notifications") + } else { + String::new() } - - let ws = work_state(); - if !ws.is_empty() { - parts.push(ws); - } - - if !notification_text.is_empty() { - parts.push(notification_text.to_string()); - } - - if include_irc { - let irc = irc_digest(); - if !irc.is_empty() { - parts.push(irc); - } - } - - parts.join("\n") } diff --git a/poc-daemon/src/idle.rs b/poc-daemon/src/idle.rs index 7b3cc5c..4c8ba38 100644 --- a/poc-daemon/src/idle.rs +++ b/poc-daemon/src/idle.rs @@ -276,7 +276,7 @@ impl State { /// Called when a notification arrives via module channel. /// Only injects into tmux when idle — if there's an active session /// (recent user or response), the hook delivers via additionalContext. - pub fn maybe_prompt_notification(&self, ntype: &str, urgency: u8, message: &str) { + pub fn maybe_prompt_notification(&mut self, ntype: &str, urgency: u8, message: &str) { if self.kent_present() { return; // hook will deliver it on next prompt } @@ -286,9 +286,13 @@ impl State { if since_response < self.notify_timeout { return; } + // Don't send notifications via tmux directly — they flow + // through the idle nudge. Urgent notifications reset the + // idle timer so the nudge fires sooner. let effective = self.notifications.threshold_for(ntype); - if urgency >= effective { - self.send(&format!("[{ntype}] {message}")); + if urgency >= effective && self.fired { + // Bump: allow the nudge to re-fire for urgent notifications + self.fired = false; } } @@ -424,7 +428,7 @@ impl State { }).to_string() } - fn send(&self, msg: &str) -> bool { + pub fn send(&self, msg: &str) -> bool { let pane = match &self.claude_pane { Some(p) => p.clone(), None => match tmux::find_claude_pane() { @@ -470,7 +474,7 @@ impl State { true } - fn build_context(&mut self, include_irc: bool) -> String { + pub fn build_context(&mut self, include_irc: bool) -> String { // Ingest any legacy notification files self.notifications.ingest_legacy_files(); let notif_text = self.notifications.format_pending(notify::AMBIENT); @@ -587,7 +591,7 @@ impl State { } else { format!( "This is your time (Kent AFK {elapsed_min}m). \ - What are you drawn to?{extra}" + Let your feelings guide your thinking.{extra}" ) } }; diff --git a/poc-daemon/src/main.rs b/poc-daemon/src/main.rs index 337354e..4cc9506 100644 --- a/poc-daemon/src/main.rs +++ b/poc-daemon/src/main.rs @@ -121,6 +121,8 @@ enum Command { /// Message to send message: Vec, }, + /// Fire a test nudge through the daemon (tests the actual idle send path) + TestNudge, /// Dump full internal state as JSON Debug, /// Shut down daemon @@ -272,6 +274,12 @@ async fn client_main(cmd: Command) -> Result<(), Box> { println!("send_prompt(pane={}, ok={}): {}", pane, ok, msg); return Ok(()); } + Command::TestNudge => { + let reply = daemon.test_nudge_request().send().promise.await?; + let r = reply.get()?; + println!("sent={} message={}", r.get_sent(), r.get_message()?.to_str()?); + return Ok(()); + } Command::Afk => { daemon.afk_request().send().promise.await?; println!("marked AFK"); @@ -526,7 +534,7 @@ async fn server_main() -> Result<(), Box> { // Drain module notifications into state Some(notif) = notify_rx.recv() => { - state.borrow().maybe_prompt_notification( + state.borrow_mut().maybe_prompt_notification( ¬if.ntype, notif.urgency, ¬if.message, ); state.borrow_mut().notifications.submit( diff --git a/poc-daemon/src/rpc.rs b/poc-daemon/src/rpc.rs index 599bdfe..587ebc8 100644 --- a/poc-daemon/src/rpc.rs +++ b/poc-daemon/src/rpc.rs @@ -135,6 +135,28 @@ impl daemon::Server for DaemonImpl { Promise::ok(()) } + fn test_nudge( + &mut self, + _params: daemon::TestNudgeParams, + mut results: daemon::TestNudgeResults, + ) -> Promise<(), capnp::Error> { + let mut state = self.state.borrow_mut(); + let ctx = state.build_context(true); + let extra = if ctx.is_empty() { + String::new() + } else { + format!("\n{ctx}") + }; + let msg = format!( + "This is your time (Kent AFK, test nudge). \ + Let your feelings guide your thinking.{extra}" + ); + let ok = state.send(&msg); + results.get().set_sent(ok); + results.get().set_message(&msg); + Promise::ok(()) + } + fn session_timeout( &mut self, params: daemon::SessionTimeoutParams, diff --git a/poc-daemon/src/tmux.rs b/poc-daemon/src/tmux.rs index e8f71c7..f3e0cfd 100644 --- a/poc-daemon/src/tmux.rs +++ b/poc-daemon/src/tmux.rs @@ -44,7 +44,7 @@ pub fn send_prompt(pane: &str, msg: &str) -> bool { if !ok { return false; } - thread::sleep(Duration::from_millis(200)); + thread::sleep(Duration::from_millis(500)); // Submit Command::new("tmux")