Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/collector/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
26 changes: 26 additions & 0 deletions src/collector/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down