Files
codex/codex-rs/state/src/audit.rs
T
Eric Traut a7836744cc Add doctor thread inventory audit (#24305)
## Why

Users have been reporting missing sessions in the app. The app server
thread listing is backed by the SQLite state DB, but the durable source
of truth for a thread still exists on disk as rollout JSONL. When the
state DB is incomplete, doctor should be able to show the mismatch
directly instead of leaving users with a generic state health result.

## What changed

This adds a `threads` doctor check that compares active and archived
rollout files under `CODEX_HOME` with rows in the SQLite `threads`
table. The check reports missing rollout rows, stale DB rows, archive
flag mismatches, duplicate rollout thread IDs, duplicate DB paths,
source/provider summaries, and bounded samples of affected rollout
paths.

It also adds a read-only state audit helper in `codex-rs/state` so
doctor can inspect thread rows without creating, migrating, or repairing
the database.

## Sample output

```text
  ⚠ threads      rollout files are missing from the state DB
      default model provider   openai
      rollout DB active files  3910
      rollout DB archived files 2037
      rollout DB scan errors   0
      rollout DB malformed file names 0
      rollout DB scan cap reached false
      rollout DB rows          5499
      rollout DB active rows   3462
      rollout DB archived rows 2037
      rollout DB missing active rows 448
      rollout DB missing archived rows 0
      rollout DB stale rows    0
      rollout DB archive mismatches 0
      rollout DB duplicate rollout thread ids 0
      rollout DB duplicate DB paths 0
      rollout DB model providers openai=5359, lmstudio=35, mock_provider=33, lite_llm=26, proxy=26, ollama=15, lms=4, local-usage-limit=1
      rollout DB sources       vscode=2587, cli=1494, subagent:thread_spawn=577, subagent:other=502, exec=281, subagent:memory_consolidation=46, subagent:review=9, unknown=3
      rollout DB missing active sample ~/.codex/sessions/2026/0…857e-a923c712e066.jsonl
      rollout DB missing active sample ~/.codex/sessions/2025/0…877a-766dff25c68d.jsonl
      rollout DB missing active sample ~/.codex/sessions/2025/0…a8b1-7bbadc836f6e.jsonl
      rollout DB missing active sample ~/.codex/sessions/2025/0…a218-e6197f3f62f8.jsonl
      rollout DB missing active sample ~/.codex/sessions/2025/0…9011-7e30784f9932.jsonl
```
2026-05-25 10:29:06 -07:00

56 lines
1.6 KiB
Rust

//! Read-only state database queries for diagnostics.
use anyhow::Result;
use log::LevelFilter;
use sqlx::ConnectOptions;
use sqlx::Row;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::sqlite::SqlitePoolOptions;
use std::path::Path;
use std::path::PathBuf;
/// Minimal thread metadata used by read-only state database audits.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThreadStateAuditRow {
pub id: String,
pub rollout_path: PathBuf,
pub archived: bool,
pub source: String,
pub model_provider: String,
}
/// Read persisted thread rows from a state DB without creating, migrating, or repairing it.
pub async fn read_thread_state_audit_rows(path: &Path) -> Result<Vec<ThreadStateAuditRow>> {
let options = SqliteConnectOptions::new()
.filename(path)
.create_if_missing(false)
.read_only(true)
.log_statements(LevelFilter::Off);
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(options)
.await?;
let rows = sqlx::query(
r#"
SELECT id, rollout_path, archived, source, model_provider
FROM threads
"#,
)
.fetch_all(&pool)
.await?;
pool.close().await;
rows.into_iter()
.map(|row| {
let archived: i64 = row.try_get("archived")?;
Ok(ThreadStateAuditRow {
id: row.try_get("id")?,
rollout_path: PathBuf::from(row.try_get::<String, _>("rollout_path")?),
archived: archived != 0,
source: row.try_get("source")?,
model_provider: row.try_get("model_provider")?,
})
})
.collect()
}