From b2147387560ef3021c7dabbab06e5ae864327954 Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Sat, 18 Jul 2026 11:40:39 -0700 Subject: [PATCH 1/2] fix: report supervised daemon topology accurately --- src/cli/status.rs | 169 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 5 deletions(-) diff --git a/src/cli/status.rs b/src/cli/status.rs index e6789ad..901eced 100644 --- a/src/cli/status.rs +++ b/src/cli/status.rs @@ -1039,6 +1039,110 @@ fn supervisor_fanout_verdict( } } +fn supervisor_worker_count( + supervisor_pid: Option, + daemon_pids: &[u32], + known_session_pids: &[u32], +) -> usize { + let known: std::collections::HashSet = known_session_pids.iter().copied().collect(); + daemon_pids + .iter() + .copied() + .filter(|pid| match supervisor_pid { + Some(supervisor) => *pid != supervisor, + None => known.contains(pid), + }) + .collect::>() + .len() +} + +fn supervisor_daemon_health_verdict( + supervisor_pid: Option, + supervisor_alive: bool, + daemon_pids: &[u32], + unmanaged_pids: &[u32], +) -> Option { + let supervisor_pid = + supervisor_pid.filter(|pid| supervisor_alive && daemon_pids.contains(pid))?; + let workers = supervisor_worker_count(Some(supervisor_pid), daemon_pids, &[]); + if !unmanaged_pids.is_empty() { + let managed_workers = workers.saturating_sub(unmanaged_pids.len()); + return Some(DoctorCheck::fail( + "daemon", + format!( + "supervisor (pid {supervisor_pid}) + {managed_workers} session child daemon(s), but {} unmanaged daemon(s) remain: {}", + unmanaged_pids.len(), + unmanaged_pids + .iter() + .map(u32::to_string) + .collect::>() + .join(", ") + ), + "inspect `wire supervisor` and process ancestry; recover through the existing service manager; do not wildcard-kill processes", + )); + } + Some(DoctorCheck::pass( + "daemon", + format!( + "supervisor (pid {supervisor_pid}) + {workers} session child daemon(s); no unmanaged daemons" + ), + )) +} + +fn supervisor_pid_consistency_verdict( + supervisor_pid: Option, + supervisor_alive: bool, + daemon_pids: &[u32], + unmanaged_pids: &[u32], +) -> Option { + let health = supervisor_daemon_health_verdict( + supervisor_pid, + supervisor_alive, + daemon_pids, + unmanaged_pids, + )?; + (health.status == "PASS").then(|| { + DoctorCheck::pass( + "daemon_pid_consistency", + "supervisor owns session worker lifecycle; retired session pidfiles do not imply an orphan", + ) + }) +} + +fn supervisor_runtime_topology() -> (Option, bool, Vec, Vec, usize) { + let daemon_pids = actual_wire_role_pids("daemon"); + let supervisor_pid = crate::session::sessions_root() + .ok() + .map(|root| root.join("supervisor.pid")) + .and_then(|path| std::fs::read_to_string(path).ok()) + .and_then(|body| body.trim().parse::().ok()); + let supervisor_alive = supervisor_pid.is_some_and(|pid| daemon_pids.contains(&pid)); + let known_session_pids: Vec = crate::session::list_sessions() + .unwrap_or_default() + .iter() + .filter_map(|session| crate::session::session_daemon_pid(&session.home_dir)) + .collect(); + let known_session_pid_set: std::collections::HashSet = + known_session_pids.iter().copied().collect(); + let unmanaged_pids = daemon_pids + .iter() + .copied() + .filter(|pid| Some(*pid) != supervisor_pid && !known_session_pid_set.contains(pid)) + .collect(); + let workers = supervisor_worker_count( + supervisor_pid.filter(|_| supervisor_alive), + &daemon_pids, + &known_session_pids, + ); + ( + supervisor_pid, + supervisor_alive, + daemon_pids, + unmanaged_pids, + workers, + ) +} + fn session_override_collision_verdict(processes: usize, distinct_homes: usize) -> DoctorCheck { if processes > 1 && distinct_homes < processes { DoctorCheck::warn( @@ -1116,11 +1220,7 @@ fn local_relay_verdict(installed: bool, reachable: bool) -> DoctorCheck { fn check_supervisor_fanout() -> DoctorCheck { let sessions = crate::session::list_sessions().unwrap_or_default(); - let workers = sessions - .iter() - .filter_map(|session| crate::session::session_daemon_pid(&session.home_dir)) - .filter(|pid| crate::platform::process_alive(*pid)) - .count(); + let (_, _, _, _, workers) = supervisor_runtime_topology(); let inactive = sessions .iter() .filter(|session| { @@ -1407,6 +1507,17 @@ fn check_sync_freshness() -> DoctorCheck { /// `wire doctor` must catch THIS class: multiple daemons running, OR /// pid-file claims daemon down while a process is actually up. fn check_daemon_health() -> DoctorCheck { + let (supervisor_pid, supervisor_alive, daemon_pids, unmanaged_pids, _) = + supervisor_runtime_topology(); + if let Some(verdict) = supervisor_daemon_health_verdict( + supervisor_pid, + supervisor_alive, + &daemon_pids, + &unmanaged_pids, + ) { + return verdict; + } + // v0.5.13 (issue #2 bug A): doctor PASSed on orphan-only state while // `wire status` reported DOWN, disagreeing for 25 min. v0.5.19 (#2 // hardening): every surface routes through ensure_up::daemon_liveness @@ -1523,6 +1634,17 @@ fn check_daemon_health() -> DoctorCheck { /// content, not liveness), letting `wire status: DOWN` and /// `wire doctor: PASS` disagree for 25 min in incident #2. fn check_daemon_pid_consistency() -> DoctorCheck { + let (supervisor_pid, supervisor_alive, daemon_pids, unmanaged_pids, _) = + supervisor_runtime_topology(); + if let Some(verdict) = supervisor_pid_consistency_verdict( + supervisor_pid, + supervisor_alive, + &daemon_pids, + &unmanaged_pids, + ) { + return verdict; + } + let snap = crate::ensure_up::daemon_liveness(); match &snap.record { crate::ensure_up::PidRecord::Missing => DoctorCheck::pass( @@ -2150,6 +2272,43 @@ mod doctor_tests { assert!(c.detail.contains("566") && c.detail.contains("16")); } + #[test] + fn supervisor_worker_count_uses_actual_daemon_roles_not_stale_pidfiles() { + assert_eq!( + supervisor_worker_count(Some(10), &[10, 11, 12], &[11, 12]), + 2 + ); + assert_eq!(supervisor_worker_count(None, &[11, 12], &[11]), 1); + } + + #[test] + fn doctor_prefers_healthy_supervisor_topology_over_stale_session_pidfile() { + let c = supervisor_daemon_health_verdict(Some(10), true, &[10, 11, 12], &[]) + .expect("healthy supervisor should own the daemon verdict"); + assert_eq!(c.status, "PASS"); + assert!(c.detail.contains("2 session child")); + } + + #[test] + fn supervisor_topology_surfaces_unmanaged_daemon() { + let c = supervisor_daemon_health_verdict(Some(10), true, &[10, 11, 12], &[12]) + .expect("healthy supervisor should own the daemon verdict"); + assert_eq!(c.status, "FAIL"); + assert!(c.detail.contains("1 session child")); + assert!(c.detail.contains("unmanaged")); + } + + #[test] + fn retired_session_pidfile_is_consistent_under_healthy_supervisor() { + let c = supervisor_pid_consistency_verdict(Some(10), true, &[10, 11, 12], &[]) + .expect("healthy supervisor should own pid consistency"); + assert_eq!(c.status, "PASS"); + assert!( + c.detail + .contains("supervisor owns session worker lifecycle") + ); + } + #[test] fn duplicate_literal_override_is_operator_configuration() { let c = session_override_collision_verdict(2, 1); From e3ce0557595c68404334705bef8b43b70911921f Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Sat, 18 Jul 2026 11:40:45 -0700 Subject: [PATCH 2/2] docs: record final supervisor audit --- SESSION_LOG_2026_07_18.md | 44 +++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/SESSION_LOG_2026_07_18.md b/SESSION_LOG_2026_07_18.md index e18aded..abcd59f 100644 --- a/SESSION_LOG_2026_07_18.md +++ b/SESSION_LOG_2026_07_18.md @@ -113,10 +113,11 @@ thread ID. signals. They remain inactive rather than becoming supervisor children. - Local relay TCP healthy; launchd daemon unit loaded; PATH/service binary consistent; no reported MCP version skew. -- Doctor's aggregate `supervisor_fanout` check still counts old MCP-owned - workers and reports 53 live workers. The managed supervisor itself owns only - 12 children. No broad process kill or extra service restart was used; old - MCP-owned workers age out with their Codex sessions. +- Doctor's aggregate `supervisor_fanout` check reported 53 live workers while + process ancestry showed only 12 managed children. That discrepancy was not + old MCP-owned workers; the post-merge audit below traced it to stale daemon + pidfiles whose numeric PIDs had been reused by unrelated live processes. + No broad process kill or extra service restart was used. ## Missed runtime-test caller @@ -127,6 +128,41 @@ and 10 failures with uninitialized identities. Changing that caller to XDG root. This one-line follow-up and this final rollout record are carried on `fix/codex-thread-rollout-record` for a second protected merge. +PR #369 merged that follow-up through 14 green protected checks as +`80b271e90011a44eec3082fb9db1222acfebe91c`. + +## Post-merge doctor topology correction + +The final installed-binary audit found launchd and the local relay healthy but +`wire doctor` emitted two false daemon failures: + +- `supervisor_fanout` counted 50 "live workers" from historical daemon + pidfiles by checking only whether each numeric PID existed. Exact command + roles and ancestry showed one launchd supervisor plus 11 child daemons; old + pidfile numbers had been reused by unrelated processes. +- The current inherited-literal session had been retired by the bounded + supervisor, leaving its per-session pidfile stale. The legacy single-session + doctor path then labeled every healthy supervisor child an orphan, even + though all 11 children had the supervisor as parent and no unmanaged daemon + existed. + +GitNexus classified `check_daemon_health` and +`check_daemon_pid_consistency` LOW risk (doctor-only diagnostics, no indexed +upstream callers). `check_supervisor_fanout` was absent from the graph; direct +source inspection found only `cmd_doctor` as caller, so risk remained confined +to the diagnostic surface. + +TDD added four topology regressions: actual daemon-role counting, isolation +from another WIRE_HOME, healthy-supervisor precedence over a retired session +pidfile, and unmanaged-daemon failure. RED failed on missing helpers/signature; +GREEN passed 27 doctor unit tests. Formatting, clippy, the 655-stale-home +restart test, and a live candidate doctor run passed. Candidate doctor reported +11 workers within cap 16 across 719 homes, supervisor + 11 children, no +unmanaged daemons, consistent retired pidfile handling, and zero FAIL results. +The canonical `test-env/run.sh` gate then passed end-to-end: 657 library tests, +all serial Rust targets, release build, demos, and 11/11 integration scripts. +No service mutation occurred during this audit. + ## Artifacts - `docs/superpowers/specs/2026-07-18-codex-thread-identity-design.md` — approved