Codex Handbook
cli/src/doctor/thread_inventory.rs 921 lines
//! Doctor check that compares rollout files against the SQLite thread inventory.use super::CheckStatus;use super::Config;use super::DoctorCheck;use super::DoctorIssue;use codex_protocol::protocol::InternalSessionSource;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;use codex_rollout::RolloutRecorder;use codex_state::ThreadStateAuditRow;use codex_utils_path::normalize_for_path_comparison;use std::collections::BTreeMap;use std::collections::HashMap;use std::collections::HashSet;use std::ffi::OsStr;use std::path::Path;use std::path::PathBuf;const MAX_PARITY_SCAN_FILES: usize = 10_000;const SAMPLE_LIMIT: usize = 5;const SUMMARY_LIMIT: usize = 8;const CHECK_ID: &str = "state.rollout_db_parity";const CHECK_CATEGORY: &str = "threads";#[derive(Clone, Debug)]struct RolloutAuditFile {    path: PathBuf,    key: PathBuf,    archived: bool,    thread_id: String,}#[derive(Default)]struct RolloutScan {    files: Vec<RolloutAuditFile>,    scan_errors: Vec<String>,    malformed_names: Vec<PathBuf>,    reached_scan_cap: bool,}enum RolloutThreadId {    Id(String),    MalformedName,    Unusable(String),}impl RolloutScan {    fn candidate_count(&self) -> usize {        self.files.len() + self.malformed_names.len() + self.scan_errors.len()    }    fn reached_candidate_cap(&self) -> bool {        self.candidate_count() >= MAX_PARITY_SCAN_FILES    }    fn record_malformed_name(&mut self, path: PathBuf) {        if self.reached_candidate_cap() {            self.reached_scan_cap = true;            return;        }        self.malformed_names.push(path);        self.reached_scan_cap = self.reached_candidate_cap();    }    fn record_scan_error(&mut self, message: String) {        if self.reached_candidate_cap() {            self.reached_scan_cap = true;            return;        }        self.scan_errors.push(message);        self.reached_scan_cap = self.reached_candidate_cap();    }    fn active_count(&self) -> usize {        self.files.iter().filter(|file| !file.archived).count()    }    fn archived_count(&self) -> usize {        self.files.iter().filter(|file| file.archived).count()    }}pub(super) async fn thread_inventory_check(config: &Config) -> DoctorCheck {    thread_inventory_check_for_roots(        config.codex_home.as_path(),        config.sqlite_home.as_path(),        config.model_provider_id.as_str(),    )    .await}async fn thread_inventory_check_for_roots(    codex_home: &Path,    sqlite_home: &Path,    default_provider: &str,) -> DoctorCheck {    let scan = scan_rollout_files(codex_home).await;    let state_db_path = codex_state::state_db_path(sqlite_home);    let mut details = vec![        format!("default model provider: {default_provider}"),        format!("rollout DB active files: {}", scan.active_count()),        format!("rollout DB archived files: {}", scan.archived_count()),        format!("rollout DB scan errors: {}", scan.scan_errors.len()),        format!(            "rollout DB malformed file names: {}",            scan.malformed_names.len()        ),        format!("rollout DB scan cap reached: {}", scan.reached_scan_cap),    ];    push_samples(        &mut details,        "rollout DB scan error sample",        scan.scan_errors.iter().map(String::as_str),    );    push_samples(        &mut details,        "rollout DB malformed file sample",        scan.malformed_names            .iter()            .map(|path| path.display().to_string()),    );    if !state_db_path.is_file() {        details.push("rollout DB rows: skipped (state DB missing)".to_string());        return missing_state_db_check(scan, details);    }    let rows = match codex_state::read_thread_state_audit_rows(&state_db_path).await {        Ok(rows) => rows,        Err(err) => {            details.push(format!("rollout DB read error: {err}"));            return DoctorCheck::new(                CHECK_ID,                CHECK_CATEGORY,                CheckStatus::Warning,                "state database thread inventory could not be read",            )            .details(details)            .issue(                DoctorIssue::new(                    CheckStatus::Warning,                    "state DB thread rows could not be queried",                )                .measured(err.to_string())                .expected("readable threads table"),            );        }    };    parity_check_from_scan_and_rows(codex_home, scan, rows, details)}fn missing_state_db_check(scan: RolloutScan, details: Vec<String>) -> DoctorCheck {    if scan.files.is_empty()        && scan.scan_errors.is_empty()        && scan.malformed_names.is_empty()        && !scan.reached_scan_cap    {        return DoctorCheck::new(            CHECK_ID,            CHECK_CATEGORY,            CheckStatus::Ok,            "no rollout/state DB inventory to compare",        )        .details(details);    }    let summary = if scan.files.is_empty() {        "rollout scan was incomplete or found bad files"    } else {        "state DB is missing while rollout files exist"    };    let mut check =        DoctorCheck::new(CHECK_ID, CHECK_CATEGORY, CheckStatus::Warning, summary).details(details);    if !scan.files.is_empty() {        check = check            .issue(                DoctorIssue::new(                    CheckStatus::Warning,                    "rollout files exist but the state DB is missing",                )                .measured(format!("{} rollout files", scan.files.len()))                .expected("state DB contains matching thread rows")                .remedy("Start Codex with no state DB present so startup backfill can create it from rollout files."),        )            .remediation(                "Start Codex with no state DB present so startup backfill can create it from rollout files.",            );    }    if !scan.scan_errors.is_empty() || !scan.malformed_names.is_empty() || scan.reached_scan_cap {        check = check.issue(            DoctorIssue::new(                CheckStatus::Warning,                "rollout scan was incomplete or found bad files",            )            .measured(format!(                "{} scan errors, {} malformed names, scan cap reached: {}",                scan.scan_errors.len(),                scan.malformed_names.len(),                scan.reached_scan_cap            ))            .expected("rollout directories are fully scannable")            .remedy("Check file permissions and unexpected files under CODEX_HOME sessions."),        );    }    check}fn parity_check_from_scan_and_rows(    codex_home: &Path,    scan: RolloutScan,    rows: Vec<ThreadStateAuditRow>,    mut details: Vec<String>,) -> DoctorCheck {    let rollout_by_key = scan        .files        .iter()        .map(|file| (file.key.clone(), file))        .collect::<HashMap<_, _>>();    let mut rows_by_key: HashMap<PathBuf, Vec<&ThreadStateAuditRow>> = HashMap::new();    for row in &rows {        rows_by_key            .entry(path_key(&row.rollout_path))            .or_default()            .push(row);    }    let missing_active = missing_rollout_paths(&scan.files, &rows_by_key, /*archived*/ false);    let missing_archived = missing_rollout_paths(&scan.files, &rows_by_key, /*archived*/ true);    let scan_complete = !scan.reached_scan_cap;    let stale_rows = if scan_complete {        rows.iter()            .filter(|row| !row.rollout_path.is_file())            .collect::<Vec<_>>()    } else {        Vec::new()    };    let archive_mismatches = if scan_complete {        rows.iter()            .filter_map(|row| {                let expected_archived = rollout_by_key                    .get(&path_key(&row.rollout_path))                    .map(|file| file.archived)                    .or_else(|| {                        row.rollout_path                            .is_file()                            .then(|| archived_from_rollout_path(codex_home, &row.rollout_path))                            .flatten()                    })?;                (expected_archived != row.archived).then_some(row)            })            .collect::<Vec<_>>()    } else {        Vec::new()    };    let duplicate_rollout_thread_ids = duplicate_rollout_thread_ids(&scan.files);    let duplicate_db_paths = duplicate_db_paths(&rows_by_key);    let archived_rows = rows.iter().filter(|row| row.archived).count();    let active_rows = rows.len() - archived_rows;    details.extend([        format!("rollout DB rows: {}", rows.len()),        format!("rollout DB active rows: {active_rows}"),        format!("rollout DB archived rows: {archived_rows}"),        format!("rollout DB missing active rows: {}", missing_active.len()),        format!(            "rollout DB missing archived rows: {}",            missing_archived.len()        ),        format!(            "rollout DB stale rows: {}",            count_or_skipped(stale_rows.len(), scan_complete)        ),        format!(            "rollout DB archive mismatches: {}",            count_or_skipped(archive_mismatches.len(), scan_complete)        ),        format!(            "rollout DB duplicate rollout thread ids: {}",            duplicate_rollout_thread_ids.len()        ),        format!(            "rollout DB duplicate DB paths: {}",            duplicate_db_paths.len()        ),        format!(            "rollout DB model providers: {}",            count_summary(rows.iter().map(|row| row.model_provider.as_str()))        ),        format!(            "rollout DB sources: {}",            count_summary(rows.iter().map(|row| source_category(&row.source)))        ),    ]);    push_path_samples(        &mut details,        "rollout DB missing active sample",        missing_active.iter().copied(),    );    push_path_samples(        &mut details,        "rollout DB missing archived sample",        missing_archived.iter().copied(),    );    push_path_samples(        &mut details,        "rollout DB stale row sample",        stale_rows.iter().map(|row| row.rollout_path.as_path()),    );    push_path_samples(        &mut details,        "rollout DB archive mismatch sample",        archive_mismatches            .iter()            .map(|row| row.rollout_path.as_path()),    );    push_samples(        &mut details,        "rollout DB duplicate rollout thread id sample",        duplicate_rollout_thread_ids.iter().map(String::as_str),    );    push_path_samples(        &mut details,        "rollout DB duplicate DB path sample",        duplicate_db_paths.iter().map(PathBuf::as_path),    );    let status = if scan.scan_errors.is_empty()        && scan.malformed_names.is_empty()        && !scan.reached_scan_cap        && missing_active.is_empty()        && missing_archived.is_empty()        && stale_rows.is_empty()        && archive_mismatches.is_empty()        && duplicate_rollout_thread_ids.is_empty()        && duplicate_db_paths.is_empty()    {        CheckStatus::Ok    } else {        CheckStatus::Warning    };    let summary = if status == CheckStatus::Ok {        "rollout files and state DB thread inventory agree"    } else {        "rollout files and state DB thread inventory differ"    };    let mut check = DoctorCheck::new(CHECK_ID, CHECK_CATEGORY, status, summary).details(details);    if !missing_active.is_empty() || !missing_archived.is_empty() {        check = check.issue(            DoctorIssue::new(                CheckStatus::Warning,                "rollout files are missing from the state DB",            )            .measured(format!(                "{} active, {} archived",                missing_active.len(),                missing_archived.len()            ))            .expected("every rollout file has a matching threads row"),        );    }    if !stale_rows.is_empty() {        check = check.issue(            DoctorIssue::new(                CheckStatus::Warning,                "state DB rows point at missing or unusable rollout files",            )            .measured(format!("{} stale rows", stale_rows.len()))            .expected("every state DB rollout path is a file on disk"),        );    }    if !archive_mismatches.is_empty() {        check = check.issue(            DoctorIssue::new(                CheckStatus::Warning,                "state DB archive flags disagree with rollout file locations",            )            .measured(format!("{} mismatched rows", archive_mismatches.len()))            .expected(                "rows under archived_sessions are archived and rows under sessions are active",            ),        );    }    if !duplicate_rollout_thread_ids.is_empty() || !duplicate_db_paths.is_empty() {        check = check.issue(            DoctorIssue::new(                CheckStatus::Warning,                "duplicate thread inventory entries found",            )            .measured(format!(                "{} duplicate rollout thread ids, {} duplicate DB paths",                duplicate_rollout_thread_ids.len(),                duplicate_db_paths.len()            ))            .expected("one rollout path and thread id per thread")            .remedy("Attach the doctor report to a bug report so support can inspect samples."),        );    }    if !scan.scan_errors.is_empty() || !scan.malformed_names.is_empty() || scan.reached_scan_cap {        check = check.issue(            DoctorIssue::new(                CheckStatus::Warning,                "rollout scan was incomplete or found bad files",            )            .measured(format!(                "{} scan errors, {} malformed names, scan cap reached: {}",                scan.scan_errors.len(),                scan.malformed_names.len(),                scan.reached_scan_cap            ))            .expected("rollout directories are fully scannable")            .remedy("Check file permissions and unexpected files under CODEX_HOME sessions."),        );    }    check}async fn scan_rollout_files(codex_home: &Path) -> RolloutScan {    let mut scan = RolloutScan::default();    scan_rollout_root(        &codex_home.join("sessions"),        /*archived*/ false,        &mut scan,    )    .await;    scan_rollout_root(        &codex_home.join("archived_sessions"),        /*archived*/ true,        &mut scan,    )    .await;    scan}async fn scan_rollout_root(root: &Path, archived: bool, scan: &mut RolloutScan) {    let mut dirs = vec![root.to_path_buf()];    while let Some(dir) = dirs.pop() {        if scan.reached_scan_cap {            return;        }        let entries = match std::fs::read_dir(&dir) {            Ok(entries) => entries,            Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,            Err(err) => {                scan.record_scan_error(format!("{} ({err})", dir.display()));                continue;            }        };        for entry in entries {            if scan.reached_scan_cap {                return;            }            let entry = match entry {                Ok(entry) => entry,                Err(err) => {                    scan.record_scan_error(format!("{} ({err})", dir.display()));                    continue;                }            };            let path = entry.path();            let file_type = match entry.file_type() {                Ok(file_type) => file_type,                Err(err) => {                    scan.record_scan_error(format!("{} ({err})", path.display()));                    continue;                }            };            if file_type.is_dir() {                dirs.push(path);                continue;            }            if !file_type.is_file() || !is_rollout_file(&path) {                continue;            }            if scan.candidate_count() >= MAX_PARITY_SCAN_FILES {                scan.reached_scan_cap = true;                return;            }            let thread_id = match thread_id_from_rollout(&path).await {                RolloutThreadId::Id(thread_id) => thread_id,                RolloutThreadId::MalformedName => {                    scan.record_malformed_name(path.clone());                    continue;                }                RolloutThreadId::Unusable(reason) => {                    scan.record_scan_error(format!("{} ({reason})", path.display()));                    continue;                }            };            scan.files.push(RolloutAuditFile {                key: path_key(&path),                path,                archived,                thread_id,            });        }    }}async fn thread_id_from_rollout(path: &Path) -> RolloutThreadId {    let items = match RolloutRecorder::load_rollout_items(path).await {        Ok((items, _, _)) => items,        Err(err) => return RolloutThreadId::Unusable(err.to_string()),    };    if items.is_empty() {        return RolloutThreadId::Unusable("no parseable rollout items".to_string());    }    codex_rollout::builder_from_items(items.as_slice(), path)        .map(|builder| RolloutThreadId::Id(builder.id.to_string()))        .unwrap_or(RolloutThreadId::MalformedName)}fn is_rollout_file(path: &Path) -> bool {    path.extension() == Some(OsStr::new("jsonl"))        && path            .file_name()            .and_then(OsStr::to_str)            .is_some_and(|name| name.starts_with("rollout-"))}fn count_or_skipped(count: usize, complete: bool) -> String {    if complete {        count.to_string()    } else {        "skipped (scan cap reached)".to_string()    }}fn path_key(path: &Path) -> PathBuf {    normalize_for_path_comparison(path).unwrap_or_else(|_| path.to_path_buf())}fn archived_from_rollout_path(codex_home: &Path, path: &Path) -> Option<bool> {    let key = path_key(path);    if key.starts_with(path_key(&codex_home.join("archived_sessions"))) {        return Some(true);    }    if key.starts_with(path_key(&codex_home.join("sessions"))) {        return Some(false);    }    None}fn missing_rollout_paths<'a>(    files: &'a [RolloutAuditFile],    rows_by_key: &HashMap<PathBuf, Vec<&ThreadStateAuditRow>>,    archived: bool,) -> Vec<&'a Path> {    files        .iter()        .filter(|file| file.archived == archived && !has_matching_thread_row(file, rows_by_key))        .map(|file| file.path.as_path())        .collect()}fn has_matching_thread_row(    file: &RolloutAuditFile,    rows_by_key: &HashMap<PathBuf, Vec<&ThreadStateAuditRow>>,) -> bool {    let Some(rows) = rows_by_key.get(&file.key) else {        return false;    };    rows.iter().any(|row| row.id == file.thread_id.as_str())}fn duplicate_rollout_thread_ids(files: &[RolloutAuditFile]) -> Vec<String> {    let mut seen = HashSet::new();    let mut duplicates = HashSet::new();    for thread_id in files.iter().map(|file| file.thread_id.as_str()) {        if !seen.insert(thread_id) {            duplicates.insert(thread_id.to_string());        }    }    let mut duplicates = duplicates.into_iter().collect::<Vec<_>>();    duplicates.sort();    duplicates}fn duplicate_db_paths(rows_by_key: &HashMap<PathBuf, Vec<&ThreadStateAuditRow>>) -> Vec<PathBuf> {    let mut paths = rows_by_key        .iter()        .filter(|(_, rows)| rows.len() > 1)        .map(|(path, _)| path.clone())        .collect::<Vec<_>>();    paths.sort();    paths}fn source_category(source: &str) -> &'static str {    let parsed = serde_json::from_str::<SessionSource>(source)        .or_else(|_| serde_json::from_value(serde_json::Value::String(source.to_string())));    let Ok(source) = parsed else {        return "unparsable";    };    match source {        SessionSource::Cli => "cli",        SessionSource::VSCode => "vscode",        SessionSource::Exec => "exec",        SessionSource::Mcp => "mcp",        SessionSource::Custom(_) => "custom",        SessionSource::Internal(InternalSessionSource::MemoryConsolidation) => {            "internal:memory_consolidation"        }        SessionSource::SubAgent(SubAgentSource::Review) => "subagent:review",        SessionSource::SubAgent(SubAgentSource::Compact) => "subagent:compact",        SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => "subagent:thread_spawn",        SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => {            "subagent:memory_consolidation"        }        SessionSource::SubAgent(SubAgentSource::Other(_)) => "subagent:other",        SessionSource::Unknown => "unknown",    }}fn count_summary<I, V>(values: I) -> Stringwhere    I: Iterator<Item = V>,    V: Into<String>,{    let mut counts = BTreeMap::<String, usize>::new();    for value in values {        *counts.entry(value.into()).or_default() += 1;    }    if counts.is_empty() {        return "none".to_string();    }    let mut entries = counts.into_iter().collect::<Vec<_>>();    entries.sort_by(|(left_value, left_count), (right_value, right_count)| {        right_count            .cmp(left_count)            .then_with(|| left_value.cmp(right_value))    });    let omitted_categories = entries.len().saturating_sub(SUMMARY_LIMIT);    let omitted_rows = entries        .iter()        .skip(SUMMARY_LIMIT)        .map(|(_, count)| count)        .sum::<usize>();    let mut parts = entries        .into_iter()        .take(SUMMARY_LIMIT)        .map(|(value, count)| format!("{value}={count}"))        .collect::<Vec<_>>();    if omitted_categories > 0 {        parts.push(format!(            "other={omitted_rows} across {omitted_categories} categories"        ));    }    parts.join(", ")}fn push_path_samples<'a>(    details: &mut Vec<String>,    label: &str,    paths: impl Iterator<Item = &'a Path>,) {    push_samples(details, label, paths.map(|path| path.display().to_string()));}fn push_samples<I, V>(details: &mut Vec<String>, label: &str, values: I)where    I: Iterator<Item = V>,    V: ToString,{    for value in values.take(SAMPLE_LIMIT) {        details.push(format!("{label}: {}", value.to_string()));    }}#[cfg(test)]mod tests {    use super::*;    use codex_protocol::ThreadId;    use codex_protocol::protocol::RolloutItem;    use codex_protocol::protocol::RolloutLine;    use pretty_assertions::assert_eq;    use sqlx::sqlite::SqliteConnectOptions;    use sqlx::sqlite::SqlitePoolOptions;    use tempfile::TempDir;    #[tokio::test]    async fn thread_inventory_check_ok_when_rollouts_match_db() {        let fixture = Fixture::new().await;        let active_path = fixture.write_rollout(            /*archived*/ false,            "2025-01-02T10-00-00",            "00000000-0000-0000-0000-000000000001",        );        let archived_path = fixture.write_rollout(            /*archived*/ true,            "2025-01-02T11-00-00",            "00000000-0000-0000-0000-000000000002",        );        fixture            .insert_thread_row(                "00000000-0000-0000-0000-000000000001",                active_path.as_path(),                /*archived*/ false,            )            .await;        fixture            .insert_thread_row(                "00000000-0000-0000-0000-000000000002",                archived_path.as_path(),                /*archived*/ true,            )            .await;        let check = thread_inventory_check_for_roots(            fixture.codex_home.path(),            fixture.sqlite_home.path(),            "test-provider",        )        .await;        assert_eq!(check.status, CheckStatus::Ok);        assert_eq!(check.category, CHECK_CATEGORY);        assert_detail(&check, "rollout DB missing active rows", "0");        assert_detail(&check, "rollout DB missing archived rows", "0");        assert_detail(&check, "rollout DB stale rows", "0");        assert_detail(&check, "rollout DB archive mismatches", "0");    }    #[tokio::test]    async fn thread_inventory_check_warns_for_missing_stale_and_mismatched_rows() {        let fixture = Fixture::new().await;        let missing_path = fixture.write_rollout(            /*archived*/ false,            "2025-01-02T10-00-00",            "00000000-0000-0000-0000-000000000001",        );        let mismatched_path = fixture.write_rollout(            /*archived*/ true,            "2025-01-02T11-00-00",            "00000000-0000-0000-0000-000000000002",        );        let stale_path = fixture            .codex_home            .path()            .join("sessions/2025/01/02/rollout-2025-01-02T12-00-00-00000000-0000-0000-0000-000000000003.jsonl");        fixture            .insert_thread_row(                "00000000-0000-0000-0000-000000000002",                mismatched_path.as_path(),                /*archived*/ false,            )            .await;        fixture            .insert_thread_row(                "00000000-0000-0000-0000-000000000003",                stale_path.as_path(),                /*archived*/ false,            )            .await;        let check = thread_inventory_check_for_roots(            fixture.codex_home.path(),            fixture.sqlite_home.path(),            "test-provider",        )        .await;        assert_eq!(check.status, CheckStatus::Warning);        assert_eq!(check.issues.len(), 3);        assert_detail(&check, "rollout DB missing active rows", "1");        assert_detail(&check, "rollout DB stale rows", "1");        assert_detail(&check, "rollout DB archive mismatches", "1");        assert_eq!(check.remediation, None);        assert!(check.issues.iter().all(|issue| {            !issue                .remedy                .as_deref()                .is_some_and(|remedy| remedy.starts_with("Restart Codex"))        }));        assert!(            check                .details                .iter()                .any(|detail| detail.contains(missing_path.to_string_lossy().as_ref()))        );    }    struct Fixture {        codex_home: TempDir,        sqlite_home: TempDir,    }    impl Fixture {        async fn new() -> Self {            let codex_home = TempDir::new().expect("codex home");            let sqlite_home = TempDir::new().expect("sqlite home");            let _runtime = codex_state::StateRuntime::init(                sqlite_home.path().to_path_buf(),                "test-provider".to_string(),            )            .await            .expect("state runtime");            Self {                codex_home,                sqlite_home,            }        }        fn write_rollout(&self, archived: bool, timestamp: &str, thread_id: &str) -> PathBuf {            let root = if archived {                self.codex_home.path().join("archived_sessions")            } else {                self.codex_home.path().join("sessions/2025/01/02")            };            std::fs::create_dir_all(&root).expect("rollout dir");            let path = root.join(format!("rollout-{timestamp}-{thread_id}.jsonl"));            let rollout_line = RolloutLine {                timestamp: timestamp.to_string(),                item: RolloutItem::SessionMeta(codex_protocol::protocol::SessionMetaLine {                    meta: codex_protocol::protocol::SessionMeta {                        id: ThreadId::from_string(thread_id).expect("thread id"),                        timestamp: timestamp.to_string(),                        cwd: self.codex_home.path().to_path_buf(),                        originator: "test".to_string(),                        cli_version: "test".to_string(),                        source: SessionSource::Cli,                        model_provider: Some("test-provider".to_string()),                        ..Default::default()                    },                    git: None,                }),            };            let contents = serde_json::to_string(&rollout_line).expect("rollout line");            std::fs::write(&path, format!("{contents}\n")).expect("rollout file");            path        }        async fn insert_thread_row(&self, id: &str, rollout_path: &Path, archived: bool) {            let state_db_path = codex_state::state_db_path(self.sqlite_home.path());            let options = SqliteConnectOptions::new()                .filename(state_db_path)                .create_if_missing(false);            let pool = SqlitePoolOptions::new()                .max_connections(1)                .connect_with(options)                .await                .expect("sqlite pool");            sqlx::query(                r#"INSERT INTO threads (    id,    rollout_path,    created_at,    updated_at,    source,    model_provider,    cwd,    title,    sandbox_policy,    approval_mode,    archived,    archived_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)                "#,            )            .bind(id)            .bind(rollout_path.display().to_string())            .bind(1_i64)            .bind(1_i64)            .bind("cli")            .bind("test-provider")            .bind(self.codex_home.path().display().to_string())            .bind("test title")            .bind("read-only")            .bind("on-request")            .bind(if archived { 1_i64 } else { 0_i64 })            .bind(archived.then_some(1_i64))            .execute(&pool)            .await            .expect("insert thread row");            pool.close().await;        }    }    fn assert_detail(check: &DoctorCheck, label: &str, expected: &str) {        let prefix = format!("{label}: ");        let actual = check            .details            .iter()            .find_map(|detail| detail.strip_prefix(&prefix))            .expect("detail should exist");        assert_eq!(actual, expected);    }    #[test]    fn source_category_coarsens_structured_sources() {        assert_eq!(source_category("cli"), "cli");        assert_eq!(            source_category(r#"{"subagent":"memory_consolidation"}"#),            "subagent:memory_consolidation"        );        assert_eq!(            source_category(                r#"{"subagent":{"thread_spawn":{"parent_thread_id":"00000000-0000-0000-0000-000000000001","depth":2}}}"#,            ),            "subagent:thread_spawn"        );    }    #[test]    fn count_summary_caps_distinct_values() {        let summary = count_summary(["a", "b", "c", "d", "e", "f", "g", "h", "i"].into_iter());        assert_eq!(            summary,            "a=1, b=1, c=1, d=1, e=1, f=1, g=1, h=1, other=1 across 1 categories"        );    }}