use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use serde::Deserialize;
use super::files::{self, TranscriptFile};
const HEAD_SCAN_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SweptSession {
pub session_id: String,
pub projects_dir: PathBuf,
pub cwd: Option<String>,
pub harness_version: Option<String>,
pub files: Vec<TranscriptFile>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SweepOptions {
pub modified_within: Option<Duration>,
}
impl SweepOptions {
#[must_use]
pub fn modified_within(window: Duration) -> Self {
Self {
modified_within: Some(window),
}
}
}
#[must_use]
pub fn sweep(projects_root: &Path, options: &SweepOptions) -> Vec<SweptSession> {
let cutoff = options
.modified_within
.and_then(|window| SystemTime::now().checked_sub(window));
let Ok(projects) = std::fs::read_dir(projects_root) else {
return Vec::new();
};
let mut out = Vec::new();
for project in projects.flatten() {
let projects_dir = project.path();
if !projects_dir.is_dir() {
continue;
}
let Ok(entries) = std::fs::read_dir(&projects_dir) else {
continue;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
let Some(session_id) = name.strip_suffix(".jsonl") else {
continue;
};
let set = files::session_files(&projects_dir, session_id);
if set.is_empty() {
continue;
}
if let Some(cutoff) = cutoff
&& !touched_since(&set, cutoff)
{
continue;
}
let (cwd, harness_version) = read_session_facts(&entry.path());
out.push(SweptSession {
session_id: session_id.to_owned(),
projects_dir: projects_dir.clone(),
cwd,
harness_version,
files: set,
});
}
}
out.sort_by(|a, b| {
a.session_id
.cmp(&b.session_id)
.then_with(|| a.projects_dir.cmp(&b.projects_dir))
});
out
}
fn touched_since(set: &[TranscriptFile], cutoff: SystemTime) -> bool {
set.iter()
.filter_map(|file| files::fingerprint(&file.path))
.any(|fp| fp.mtime >= cutoff)
}
#[derive(Deserialize)]
struct SessionFacts {
cwd: Option<String>,
version: Option<String>,
}
fn read_session_facts(path: &Path) -> (Option<String>, Option<String>) {
let Some(head) = read_head(path, HEAD_SCAN_BYTES) else {
return (None, None);
};
let mut cwd = None;
let mut version = None;
for line in head.split(|&b| b == b'\n') {
let Ok(facts) = serde_json::from_slice::<SessionFacts>(line) else {
continue;
};
if cwd.is_none() {
cwd = facts.cwd.filter(|value| !value.is_empty());
}
if version.is_none() {
version = facts.version.filter(|value| !value.is_empty());
}
if cwd.is_some() && version.is_some() {
break;
}
}
(cwd, version)
}
fn read_head(path: &Path, cap: usize) -> Option<Vec<u8>> {
use std::io::Read;
let mut file = std::fs::File::open(path).ok()?;
let mut buf = vec![0u8; cap];
let read = file.read(&mut buf).ok()?;
buf.truncate(read);
Some(buf)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn write_session(root: &Path, cwd: &str, sid: &str) -> PathBuf {
let dir = root.join(crate::attribution::claude::fork_parent::encode_cwd(cwd));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(format!("{sid}.jsonl"));
let body = format!(
concat!(
"{{\"type\":\"mode\",\"sessionId\":\"x\"}}\n",
"{{\"type\":\"file-history-snapshot\"}}\n",
"{{\"type\":\"user\",\"sessionId\":\"{sid}\",\"cwd\":\"{cwd}\",",
"\"version\":\"2.1.205\"}}\n",
),
sid = sid,
cwd = cwd,
);
std::fs::write(&path, body).unwrap();
path
}
fn backdate(path: &Path, by: Duration) {
let file = std::fs::File::options().append(true).open(path).unwrap();
file.set_modified(SystemTime::now() - by).unwrap();
}
#[test]
fn sweep_finds_sessions_no_live_registry_would_reveal() {
let root = tempfile::tempdir().unwrap();
write_session(root.path(), "/x/y", "live");
write_session(root.path(), "/a/b", "orphan");
let swept = sweep(root.path(), &SweepOptions::default());
let ids: Vec<&str> = swept.iter().map(|s| s.session_id.as_str()).collect();
assert_eq!(ids, vec!["live", "orphan"], "sorted by session id");
assert!(
ids.contains(&"orphan"),
"a session with no live process must still be swept",
);
}
#[test]
fn sweep_reads_the_true_cwd_and_version_from_the_transcript() {
let root = tempfile::tempdir().unwrap();
write_session(root.path(), "/opt/my-project", "sid-1");
let swept = sweep(root.path(), &SweepOptions::default());
assert_eq!(swept.len(), 1);
assert_eq!(swept[0].cwd.as_deref(), Some("/opt/my-project"));
assert_eq!(swept[0].harness_version.as_deref(), Some("2.1.205"));
assert_eq!(
swept[0].projects_dir.file_name().unwrap().to_string_lossy(),
"-opt-my-project",
"the encoded directory is genuinely ambiguous, hence reading the records",
);
}
#[test]
fn sweep_reports_a_session_whose_records_carry_no_facts() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("-x-y");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("bare.jsonl"), "{\"type\":\"mode\"}\n").unwrap();
let swept = sweep(root.path(), &SweepOptions::default());
assert_eq!(swept.len(), 1);
assert_eq!(swept[0].session_id, "bare");
assert_eq!(swept[0].cwd, None);
assert_eq!(swept[0].harness_version, None);
}
#[test]
fn sweep_carries_the_whole_upload_set() {
let root = tempfile::tempdir().unwrap();
write_session(root.path(), "/x/y", "sid-1");
let sub = root.path().join("-x-y").join("sid-1").join("subagents");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(sub.join("agent-abc.jsonl"), "{}\n").unwrap();
std::fs::write(
sub.join("agent-abc.meta.json"),
r#"{"toolUseId":"toolu_7","agentType":"explore","description":"d"}"#,
)
.unwrap();
let swept = sweep(root.path(), &SweepOptions::default());
assert_eq!(swept.len(), 1);
assert_eq!(swept[0].files.len(), 2);
assert_eq!(swept[0].files[0].agent_id, None, "main sorts first");
assert_eq!(swept[0].files[1].agent_id.as_deref(), Some("abc"));
assert_eq!(swept[0].files[1].meta.tool_use_id, "toolu_7");
}
#[test]
fn sweep_honours_the_modified_within_window() {
let root = tempfile::tempdir().unwrap();
write_session(root.path(), "/x/y", "recent");
let stale = write_session(root.path(), "/a/b", "stale");
backdate(&stale, Duration::from_secs(60 * 60 * 24 * 30));
let all = sweep(root.path(), &SweepOptions::default());
assert_eq!(all.len(), 2, "no window reports everything");
let recent = sweep(
root.path(),
&SweepOptions::modified_within(Duration::from_secs(3600)),
);
assert_eq!(recent.len(), 1);
assert_eq!(recent[0].session_id, "recent");
}
#[test]
fn sweep_window_considers_the_newest_file_in_the_set() {
let root = tempfile::tempdir().unwrap();
let main = write_session(root.path(), "/x/y", "sid-1");
let sub = root.path().join("-x-y").join("sid-1").join("subagents");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(sub.join("agent-abc.jsonl"), "{}\n").unwrap();
backdate(&main, Duration::from_secs(60 * 60 * 24 * 30));
let swept = sweep(
root.path(),
&SweepOptions::modified_within(Duration::from_secs(3600)),
);
assert_eq!(swept.len(), 1, "the fresh subagent keeps the session in");
}
#[test]
fn sweep_ignores_noise_and_a_missing_root() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("-x-y");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("notes.txt"), "junk").unwrap();
std::fs::write(dir.join("archive.jsonl.bak"), "junk").unwrap();
std::fs::create_dir_all(dir.join("weird.jsonl")).unwrap();
std::fs::write(root.path().join("loose.jsonl"), "{}\n").unwrap();
assert!(sweep(root.path(), &SweepOptions::default()).is_empty());
assert!(sweep(&root.path().join("nope"), &SweepOptions::default()).is_empty());
}
}