From 31e690cde78fcf52c3d0bd20e7f15e44af77b6cc Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:40:06 +0800 Subject: [PATCH] feat(daemon): report panes the machine tree no longer references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the tree now populated by clients' semantic operations, the daemon can finally see panes nothing references. A periodic sweep reports them — log-only, deliberately: an unreferenced pane is not proof of a leak (a native-SSH pane opened inside a remote workspace's window runs in this daemon while belonging to the other machine's tree), and reclaiming one wrongly kills a session the user is looking at. The sweep's interval doubles as a grace period: a pane is reported only after being unreferenced across two consecutive looks, so an adoption still in flight is never flagged. Reclamation can be layered on once the log has shown the false-positive rate is zero. --- crates/tty7-core/src/core/machine.rs | 6 +++ crates/tty7-core/src/daemon/server.rs | 57 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/crates/tty7-core/src/core/machine.rs b/crates/tty7-core/src/core/machine.rs index 58d729b1..b4f9e1d0 100644 --- a/crates/tty7-core/src/core/machine.rs +++ b/crates/tty7-core/src/core/machine.rs @@ -1449,6 +1449,12 @@ pub fn observe_pane(pane: u64, f: impl FnOnce(&mut PaneRecord)) { } } +/// The installed observation store, if any — for daemon-side code (the orphan +/// sweep) that wants to *read* the tree the pane server publishes into. +pub fn observed_store() -> Option> { + OBSERVED.lock().unwrap_or_else(|e| e.into_inner()).clone() +} + /// Test-only: clear the slot again, so one test's store cannot swallow the /// observations of unrelated tests running later in the same binary. #[cfg(test)] diff --git a/crates/tty7-core/src/daemon/server.rs b/crates/tty7-core/src/daemon/server.rs index efefb3ce..98bb4181 100644 --- a/crates/tty7-core/src/daemon/server.rs +++ b/crates/tty7-core/src/daemon/server.rs @@ -87,6 +87,59 @@ impl Registry { } } +/// How often the orphan sweep looks, which doubles as its grace period: a pane +/// is only reported after it has been unreferenced across two consecutive +/// looks, so a freshly-spawned pane whose adopting operation is still in +/// flight is never flagged. +const ORPHAN_SWEEP_INTERVAL: std::time::Duration = std::time::Duration::from_secs(600); + +/// Periodically report live panes the machine tree does not reference. +/// +/// **Log-only, on purpose.** An unreferenced pane is not proof of a leak: +/// a native-SSH pane opened inside a *remote* workspace's window runs in this +/// (the client's) daemon while belonging to the other machine's tree, so it is +/// unreferenced here by design — and a reclaim would kill a session the user +/// is looking at. Until the tree provably references everything legitimate, +/// the sweep's job is to make leaks observable, not to act on them; killing +/// can be layered on once the log has shown the false-positive rate is zero. +fn spawn_orphan_sweep(registry: Arc) { + let spawned = std::thread::Builder::new() + .name("tty7-orphan-sweep".into()) + .spawn(move || { + let mut previous: std::collections::HashSet = std::collections::HashSet::new(); + loop { + std::thread::sleep(ORPHAN_SWEEP_INTERVAL); + // No tree served (a pane-only daemon) means no opinion. + let Some(store) = crate::core::machine::observed_store() else { + continue; + }; + let machine = store.machine(); + let referenced: std::collections::HashSet = machine + .workspaces + .iter() + .flat_map(|w| w.tabs.iter()) + .flat_map(|t| t.root.pane_ids()) + .collect(); + let orphans: std::collections::HashSet = registry + .list() + .into_iter() + .filter(|p| p.alive && !referenced.contains(&p.pane_id)) + .map(|p| p.pane_id) + .collect(); + for id in orphans.intersection(&previous) { + log::info!( + "pane {id} is running but no workspace tree references it \ + (kept; the sweep only reports — see spawn_orphan_sweep)" + ); + } + previous = orphans; + } + }); + if let Err(e) = spawned { + log::warn!("could not start the orphan-pane sweep: {e}"); + } +} + /// Resolve a pane id to its live native-SSH connection, for the SFTP control /// handlers. Errors (as a client-facing string) when the pane is unknown or isn't /// a native-SSH pane with an established connection (a PTY / compat-`ssh` pane, or @@ -233,6 +286,10 @@ pub fn run() -> anyhow::Result<()> { #[cfg(unix)] serve_sigterm(registry.clone()); + // Now that the tree has an owner filling it, the daemon can *see* panes + // nothing references any more — but it only reports them, deliberately. + spawn_orphan_sweep(registry.clone()); + for stream in listener.incoming() { match stream { Ok(stream) => {