poc-daemon: fix pane tracking, suppress notifications when user present

Three fixes:

- Persist claude_pane via thalamus extra map so it survives every
  save path (not just explicit Save commands)
- Don't clobber claude_pane with empty string when TMUX_PANE is unset
- signal_response now passes TMUX_PANE like signal_user does
- maybe_prompt_notification returns early when user is present,
  preventing notification spam during active sessions

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-11 14:35:16 -04:00
commit fa3b57fffd
4 changed files with 38 additions and 19 deletions

View file

@ -25,43 +25,57 @@ impl std::ops::DerefMut for State {
impl State { impl State {
pub fn new() -> Self { pub fn new() -> Self {
Self { let mut inner = thalamus_idle::State::new();
inner: thalamus_idle::State::new(), inner.state_path = home().join(".consciousness/daemon-state.json");
claude_pane: None, Self { inner, claude_pane: None }
}
} }
pub fn load(&mut self) { pub fn load(&mut self) {
self.inner.load(); self.inner.load();
// Also load claude_pane from persisted state self.claude_pane = self.inner.extra
let path = home().join(".consciousness/daemon-state.json"); .get("claude_pane")
if let Ok(data) = std::fs::read_to_string(&path) { .and_then(|v| v.as_str())
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&data) { .map(|s| s.to_string());
if let Some(p) = v.get("claude_pane").and_then(|v| v.as_str()) {
self.claude_pane = Some(p.to_string());
}
}
}
} }
pub fn save(&self) { pub fn save(&mut self) {
// Stash claude_pane in extra so inner.save() persists it
self.inner.extra.insert(
"claude_pane".into(),
serde_json::json!(self.claude_pane),
);
self.inner.save(); self.inner.save();
} }
fn set_pane(&mut self, pane: &str) {
if !pane.is_empty() {
self.claude_pane = Some(pane.to_string());
self.inner.extra.insert(
"claude_pane".into(),
serde_json::json!(pane),
);
}
}
/// Record user activity with pane tracking. /// Record user activity with pane tracking.
pub fn handle_user(&mut self, pane: &str) { pub fn handle_user(&mut self, pane: &str) {
self.claude_pane = Some(pane.to_string()); self.set_pane(pane);
self.inner.user_activity(); self.inner.user_activity();
} }
/// Record response activity with pane tracking. /// Record response activity with pane tracking.
pub fn handle_response(&mut self, pane: &str) { pub fn handle_response(&mut self, pane: &str) {
self.claude_pane = Some(pane.to_string()); self.set_pane(pane);
self.inner.response_activity(); self.inner.response_activity();
} }
/// Maybe send a notification as a tmux prompt. /// Maybe send a notification as a tmux prompt.
/// If the user is present, notifications stay queued — the hook
/// picks them up via check_notifications() on the next message.
pub fn maybe_prompt_notification(&mut self, ntype: &str, urgency: u8, _message: &str) { pub fn maybe_prompt_notification(&mut self, ntype: &str, urgency: u8, _message: &str) {
if self.inner.user_present() {
return;
}
let threshold = self.inner.notifications.threshold_for(ntype); let threshold = self.inner.notifications.threshold_for(ntype);
if urgency >= threshold { if urgency >= threshold {
let deliverable = self.inner.notifications.drain_deliverable(); let deliverable = self.inner.notifications.drain_deliverable();

View file

@ -552,7 +552,7 @@ async fn server_main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
state.borrow().save(); state.borrow_mut().save();
let _ = std::fs::remove_file(sock_path()); let _ = std::fs::remove_file(sock_path());
let _ = std::fs::remove_file(pid_path()); let _ = std::fs::remove_file(pid_path());
info!("daemon stopped"); info!("daemon stopped");

View file

@ -66,7 +66,12 @@ fn signal_user() {
} }
fn signal_response() { fn signal_response() {
let pane = std::env::var("TMUX_PANE").unwrap_or_default();
if pane.is_empty() {
daemon_cmd(&["response"]); daemon_cmd(&["response"]);
} else {
daemon_cmd(&["response", &pane]);
}
} }
fn check_notifications() { fn check_notifications() {

View file

@ -181,7 +181,7 @@ impl daemon::Server for DaemonImpl {
_params: daemon::SaveParams, _params: daemon::SaveParams,
_results: daemon::SaveResults, _results: daemon::SaveResults,
) -> impl std::future::Future<Output = Result<(), capnp::Error>> { ) -> impl std::future::Future<Output = Result<(), capnp::Error>> {
self.state.borrow().save(); self.state.borrow_mut().save();
info!("state saved"); info!("state saved");
std::future::ready(Ok(())) std::future::ready(Ok(()))
} }