diff --git a/src/ensure_up.rs b/src/ensure_up.rs index fd80d5d..d6b6969 100644 --- a/src/ensure_up.rs +++ b/src/ensure_up.rs @@ -148,6 +148,23 @@ pub fn pid_is_alive(pid: u32) -> bool { crate::platform::process_alive(pid) } +/// Cheap "is THIS session's daemon alive" — reads our own `daemon.pid` and +/// checks that one pid. Unlike [`daemon_liveness`] it does NOT run the +/// machine-wide process-enumeration scan (`find_processes_by_cmdline`) or +/// `list_sessions` (which reads every by-key home's agent-card) or a +/// per-session `pid_is_alive` sweep — work that only the orphan/`wire status` +/// path needs. On Windows those each shell out (PowerShell CIM, `tasklist` ×N, +/// hundreds of NTFS dir reads), so calling full `daemon_liveness` purely to read +/// `pidfile_alive` made every `wire_send` cost seconds (#350). One pidfile read +/// plus one `pid_is_alive` (one `tasklist` on Windows) yields the identical +/// `daemon_seen` boolean for a fraction of the cost. +pub fn daemon_pidfile_alive() -> bool { + read_pid_record("daemon") + .pid() + .map(pid_is_alive) + .unwrap_or(false) +} + /// Read the daemon pid file + pgrep in one shot, producing a snapshot /// every caller can interpret identically. The point of this helper /// is that three independent callers used to compute liveness three @@ -750,4 +767,14 @@ mod tests { assert_eq!(daemon_version_mismatch(), None); }); } + + #[test] + fn daemon_pidfile_alive_false_without_pidfile() { + // No daemon.pid → false, with no process-enumeration shell-out. (The + // send-path annotation reads only this, not the full daemon_liveness + // scan — the #350 Windows hot-path fix.) + crate::config::test_support::with_temp_home(|| { + assert!(!daemon_pidfile_alive()); + }); + } } diff --git a/src/mcp.rs b/src/mcp.rs index 6f03aef..daa44f0 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -1362,10 +1362,14 @@ fn tool_send(args: &Value) -> Result { // these are diagnostic-only (the verdict in `status` is the // authoritative answer), but they're cheap to compute and // existing consumers may key on them. - let snap = crate::ensure_up::daemon_liveness(); + // Cheap pidfile-only liveness: the annotation needs just `daemon_seen`, + // NOT the machine-wide orphan scan `daemon_liveness` runs (PowerShell + // CIM + list_sessions over every by-key home + tasklist ×N on Windows — + // seconds per send, #350). One pidfile check gives the same boolean. + let daemon_seen = crate::ensure_up::daemon_pidfile_alive(); let last_sync_age = crate::ensure_up::last_sync_age_seconds(); if let Some(obj) = v.as_object_mut() { - obj.insert("daemon_seen".into(), json!(snap.pidfile_alive)); + obj.insert("daemon_seen".into(), json!(daemon_seen)); obj.insert("last_sync_age_seconds".into(), json!(last_sync_age)); obj.insert( "stale_sync".into(), @@ -1378,7 +1382,9 @@ fn tool_send(args: &Value) -> Result { // Legacy --queue path. Outbox-write, daemon push loop drains. let line = serde_json::to_vec(&signed).map_err(|e| e.to_string())?; let outbox = config::append_outbox_record(peer, &line).map_err(|e| e.to_string())?; - let snap = crate::ensure_up::daemon_liveness(); + // Cheap pidfile-only liveness (see the sync branch above) — avoids the + // machine-wide orphan scan on the hot send path (#350). + let daemon_seen = crate::ensure_up::daemon_pidfile_alive(); let last_sync_age = crate::ensure_up::last_sync_age_seconds(); // Honesty check mirror of the CLI: if the peer is BOTH // unpinned in trust AND has no pending pair (outbound or @@ -1406,7 +1412,7 @@ fn tool_send(args: &Value) -> Result { "status": "queued", "peer": peer, "outbox": outbox.to_string_lossy(), - "daemon_seen": snap.pidfile_alive, + "daemon_seen": daemon_seen, "last_sync_age_seconds": last_sync_age, "stale_sync": config::stale_sync(last_sync_age), });