Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/adapters/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ apply when working here.

- The `SourceAdapter` trait in `mod.rs` is the authoritative contract, not the
DEVELOPMENT.md example. `id()`, `label()`, `scan()`, and `resume_command()`
are required; `scan_summary()`, `scan_for_sync()`, `prune()`,
`app_command()`, and `usage_parser_version()` are optional overrides.
are required; `scan_for_sync()`, `prune()`, `app_command()`, and
`usage_parser_version()` are optional overrides.
- Register new adapters in `all_adapters()` in `mod.rs`. Registration alone
wires the adapter into sync, search, the TUI source filter, and the CLI
`--source` flag. No schema change is needed — `sessions.source` is a value,
Expand Down
10 changes: 0 additions & 10 deletions src/adapters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ pub(crate) trait SourceAdapter {
fn id(&self) -> &str;
fn label(&self) -> &str;
fn scan(&self) -> anyhow::Result<Vec<RawSession>>;
fn scan_summary(&self) -> anyhow::Result<Option<SourceScanSummary>> {
Ok(None)
}
fn usage_parser_version(&self) -> Option<u32> {
None
}
Expand Down Expand Up @@ -162,13 +159,6 @@ pub(crate) struct SyncScanResult {
pub(crate) stats: SyncScanStats,
}

pub(crate) struct SourceScanSummary {
pub(crate) sessions: usize,
pub(crate) messages: usize,
pub(crate) oldest_started_at: Option<i64>,
pub(crate) newest_started_at: Option<i64>,
}

#[derive(Debug, Clone)]
pub(crate) struct ResumeCommand {
pub(crate) program: String,
Expand Down
59 changes: 1 addition & 58 deletions src/adapters/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use tracing::debug;

use crate::adapters::events;
use crate::adapters::{
RawMessage, RawSession, ResumeCommand, SourceAdapter, SourceScanSummary, SyncScanResult,
SyncScanStats,
RawMessage, RawSession, ResumeCommand, SourceAdapter, SyncScanResult, SyncScanStats,
};
use crate::db::store::Store;
use crate::types::{RawSessionEvent, RawUsageEvent, Role};
Expand Down Expand Up @@ -71,27 +70,6 @@ impl SourceAdapter for OpenCodeAdapter {
scan_session_messages(&conn, sessions, true)
}

fn scan_summary(&self) -> anyhow::Result<Option<SourceScanSummary>> {
let Some(conn) = open_opencode_db()? else {
return Ok(Some(SourceScanSummary {
sessions: 0,
messages: 0,
oldest_started_at: None,
newest_started_at: None,
}));
};

let sessions: usize =
conn.query_row("SELECT COUNT(*) FROM session", [], |row| row.get(0))?;
let oldest_started_at =
conn.query_row("SELECT MIN(time_created) FROM session", [], |row| row.get(0))?;
let newest_started_at =
conn.query_row("SELECT MAX(time_created) FROM session", [], |row| row.get(0))?;
let messages = count_total_parsed_messages(&conn)?;

Ok(Some(SourceScanSummary { sessions, messages, oldest_started_at, newest_started_at }))
}

fn scan_for_sync(
&self,
store: &Store,
Expand Down Expand Up @@ -137,16 +115,6 @@ fn count_filtered_sessions(conn: &Connection, since_ts: Option<i64>) -> anyhow::
.map_err(Into::into)
}

fn count_total_parsed_messages(conn: &Connection) -> anyhow::Result<usize> {
let sql = format!(
"SELECT COUNT(*)
FROM message m
JOIN part p ON p.message_id = m.id
WHERE {PARSED_PART_FILTER_SQL}"
);
conn.query_row(&sql, [], |row| row.get(0)).map_err(Into::into)
}

fn load_session_rows(conn: &Connection, since_ts: Option<i64>) -> anyhow::Result<Vec<SessionRow>> {
let sql = if since_ts.is_some() {
"SELECT id, directory, time_created, time_updated
Expand Down Expand Up @@ -880,31 +848,6 @@ mod tests {
let _ = std::fs::remove_file(path);
}

#[test]
fn summary_reports_counts_without_full_scan() {
let (path, conn) = setup_opencode_db();
insert_session_with_message(&conn, "s1", 220, 100, "hello");
insert_session_with_message(&conn, "s2", 250, 200, "world");

let summary = SourceScanSummary {
sessions: conn.query_row("SELECT COUNT(*) FROM session", [], |row| row.get(0)).unwrap(),
messages: count_total_parsed_messages(&conn).unwrap(),
oldest_started_at: conn
.query_row("SELECT MIN(time_created) FROM session", [], |row| row.get(0))
.unwrap(),
newest_started_at: conn
.query_row("SELECT MAX(time_created) FROM session", [], |row| row.get(0))
.unwrap(),
};

assert_eq!(summary.sessions, 2);
assert_eq!(summary.messages, 2);
assert_eq!(summary.oldest_started_at, Some(100));
assert_eq!(summary.newest_started_at, Some(200));
drop(conn);
let _ = std::fs::remove_file(path);
}

#[test]
fn incremental_scan_tolerates_malformed_json_rows() {
let (path, conn) = setup_opencode_db();
Expand Down
63 changes: 63 additions & 0 deletions src/db/session_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ use crate::types::{
Message, ParentLink, RawSessionEvent, RawUsageEvent, Role, Session, SessionTopology, ThreadRole,
};

pub(crate) struct IndexedSourceStats {
pub(crate) sessions: u64,
pub(crate) messages: u64,
pub(crate) oldest_started_at: Option<i64>,
pub(crate) newest_started_at: Option<i64>,
}

impl Store {
pub(crate) fn session_meta(
&self,
Expand Down Expand Up @@ -45,6 +52,27 @@ impl Store {
rows.collect::<Result<HashMap<_, _>, _>>().map_err(Into::into)
}

pub(crate) fn indexed_source_stats(&self) -> Result<HashMap<String, IndexedSourceStats>> {
let mut stmt = self.conn.prepare(
"SELECT source, COUNT(*), COALESCE(SUM(message_count), 0),
MIN(started_at), MAX(started_at)
FROM sessions
GROUP BY source",
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get(0)?,
IndexedSourceStats {
sessions: row.get(1)?,
messages: row.get(2)?,
oldest_started_at: row.get(3)?,
newest_started_at: row.get(4)?,
},
))
})?;
rows.collect::<Result<HashMap<_, _>, _>>().map_err(Into::into)
}

pub(crate) fn imported_source_ids(&self, source: &str) -> Result<HashSet<String>> {
let mut stmt = self
.conn
Expand Down Expand Up @@ -1226,3 +1254,38 @@ mod topology_tests {
);
}
}

#[cfg(test)]
mod source_stats_tests {
use super::*;
use crate::db::schema;

#[test]
fn indexed_source_stats_use_persisted_session_counts() {
schema::register_sqlite_vec();
let store = Store::open_in_memory().unwrap();
store
.conn
.execute_batch(
"INSERT INTO sessions
(id, source, source_id, title, started_at, message_count)
VALUES
('c1', 'codex', 'raw-c1', 'one', 20, 2),
('c2', 'codex', 'raw-c2', 'two', 10, 3),
('o1', 'opencode', 'raw-o1', 'three', 30, 4);",
)
.unwrap();

let stats = store.indexed_source_stats().unwrap();

let codex = &stats["codex"];
assert_eq!(codex.sessions, 2);
assert_eq!(codex.messages, 5);
assert_eq!(codex.oldest_started_at, Some(10));
assert_eq!(codex.newest_started_at, Some(20));

let opencode = &stats["opencode"];
assert_eq!(opencode.sessions, 1);
assert_eq!(opencode.messages, 4);
}
}
Loading