From 1fb2cb9630965d37adf7e34469fd5185b0c3116b Mon Sep 17 00:00:00 2001 From: Alexander-kniit Date: Tue, 8 Sep 2026 16:08:04 +0200 Subject: [PATCH] fix: hide SDK stream-json sessions from the session list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit abtop surfaces every live `claude` process, including non-interactive sessions driven over the Agent SDK's bidirectional stream-json protocol (`--input-format stream-json`). These are background infrastructure, not interactive agent sessions the user is running — for example claude-mem's observer holds such a session open continuously, cluttering the sessions, projects, and context panels, and even registers its transient CLAUDE_CONFIG_DIR as a scanned config root. This mirrors the existing filter that skips abtop's own `claude --print` summary children: add `process::is_sdk_stream_session`, which matches only the programmatic stream-json *input* protocol, and skip such procs in both `find_claude_pids` (the live row) and the `/proc`-environ config-root discovery loop (the dynamically-discovered historical row). A human `claude --print "q"` never feeds an input stream, so interactive and one-shot `--print` sessions are still surfaced unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/collector/claude.rs | 66 ++++++++++++++++++++++++++++++++++++++++ src/collector/process.rs | 26 ++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/collector/claude.rs b/src/collector/claude.rs index 8f08db5..95e50d1 100644 --- a/src/collector/claude.rs +++ b/src/collector/claude.rs @@ -104,6 +104,11 @@ impl ClaudeCollector { if !process::cmd_has_binary(&info.command, "claude") { continue; } + // Don't let a background SDK session (e.g. claude-mem's observer) + // register its CLAUDE_CONFIG_DIR as a scanned root. + if process::is_sdk_stream_session(&info.command) { + continue; + } if let Some(dir) = read_env_var_from_proc(*pid, "CLAUDE_CONFIG_DIR") { let p = PathBuf::from(dir); if p.is_dir() { @@ -259,6 +264,11 @@ impl ClaudeCollector { if process::is_descendant_of(*pid, self_pid, process_info) { continue; } + // SDK stream-json sessions (Agent SDK, background observers like + // claude-mem) are infrastructure, not interactive agent sessions. + if process::is_sdk_stream_session(&info.command) { + continue; + } pids.push(*pid); } pids @@ -2677,6 +2687,62 @@ n/Users/bob/.claude-alt/projects/-Users-bob-project/session.jsonl assert_eq!(got, vec![10, 13]); } + #[test] + fn test_find_claude_pids_excludes_sdk_stream_sessions() { + // A background SDK session driven over the stream-json protocol + // (`--input-format stream-json`, e.g. claude-mem's observer) is + // infrastructure and must be filtered, while an interactive session + // (PID 10) and a human `claude --print` one-shot (PID 11) are still + // surfaced. + let abtop_pid = 99u32; + let mut process_info = HashMap::new(); + process_info.insert( + abtop_pid, + ProcInfo { + pid: abtop_pid, + ppid: 1, + rss_kb: 1, + cpu_pct: 0.0, + command: "abtop".to_string(), + }, + ); + process_info.insert( + 10, + ProcInfo { + pid: 10, + ppid: 1, + rss_kb: 1, + cpu_pct: 0.0, + command: "claude".to_string(), + }, + ); + process_info.insert( + 11, + ProcInfo { + pid: 11, + ppid: 1, + rss_kb: 1, + cpu_pct: 0.0, + command: "claude --print user-script".to_string(), + }, + ); + process_info.insert( + 12, + ProcInfo { + pid: 12, + ppid: 1, + rss_kb: 1, + cpu_pct: 0.0, + command: "claude --output-format stream-json --verbose --input-format stream-json" + .to_string(), + }, + ); + + let mut got = ClaudeCollector::find_claude_pids(&process_info, abtop_pid); + got.sort_unstable(); + assert_eq!(got, vec![10, 11]); + } + #[test] fn test_resolve_project_dir_uses_worktree_fallback() { let temp = tempfile::tempdir().unwrap(); diff --git a/src/collector/process.rs b/src/collector/process.rs index a700f8f..51f3c06 100644 --- a/src/collector/process.rs +++ b/src/collector/process.rs @@ -432,6 +432,17 @@ fn unix_token_has_binary(tok: &str, name: &str) -> bool { matches!((iter.next(), iter.next()), (Some("versions"), Some(parent)) if parent == name) } +/// True when `cmd` is a non-interactive Claude Code session driven over the +/// Agent SDK's bidirectional stream-json protocol (`--input-format stream-json`). +/// A human `claude --print "q"` never feeds an input *stream*, so this matches +/// only programmatic/background sessions (the Agent SDK, claude-mem's observer), +/// never interactive use nor a one-shot `--print`. Callers skip these for the +/// same reason abtop already skips its own `--print` summary children: they are +/// infrastructure, not interactive agent sessions the user is running. +pub fn is_sdk_stream_session(cmd: &str) -> bool { + cmd.contains("--input-format stream-json") || cmd.contains("--input-format=stream-json") +} + /// Windows variant: checks executable-position tokens, splits on `\`, strips a /// trailing `.exe` and common script extensions (`.js`, `.sh`, `.py`), and /// matches case-insensitively. @@ -577,6 +588,21 @@ mod tests { assert!(!cmd_has_binary("/some/versions/2.1.121", "claude")); } + #[test] + fn is_sdk_stream_session_matches_stream_json_input() { + // Observed claude-mem observer invocation (space form). + assert!(is_sdk_stream_session( + "claude --output-format stream-json --verbose --input-format stream-json --model sonnet" + )); + // `=` form. + assert!(is_sdk_stream_session("claude --input-format=stream-json")); + // Interactive and one-shot `--print` sessions must NOT match. + assert!(!is_sdk_stream_session("claude")); + assert!(!is_sdk_stream_session("claude --print user-script")); + // `--output-format stream-json` alone (scripted one-shot read) must not match. + assert!(!is_sdk_stream_session("claude --output-format stream-json")); + } + #[cfg(windows)] #[test] fn cmd_has_binary_windows_detects_node_wrapped_codex() {