kaizen-cli 0.1.46

Distributable agent observability: real-time-tailable sessions, agile-style retros, and repo-level improvement (Cursor, Claude Code, Codex). SQLite, redact before any sync you enable.
Documentation
use super::SessionSummaryRead;
use super::session_search_sql;
use crate::store::Store;
use anyhow::{Result, ensure};
use rusqlite::named_params;

pub(crate) struct SessionSearchQuery {
    workspace: String,
    text: String,
    offset: i64,
    limit: i64,
    now_ms: i64,
    with_status: bool,
}

pub(super) struct SessionRowsPage {
    pub(super) rows: Vec<SessionSummaryRead>,
    pub(super) filtered_total: usize,
    pub(super) offset: usize,
    pub(super) limit: usize,
}

impl SessionSearchQuery {
    pub(crate) fn new(
        workspace: &str,
        text: &str,
        offset: usize,
        limit: usize,
        now_ms: u64,
    ) -> Result<Self> {
        validate(offset, limit, now_ms)?;
        Ok(valid_query(workspace, text, offset, limit, now_ms))
    }
}

pub(super) fn read(store: &Store, query: &SessionSearchQuery) -> Result<SessionRowsPage> {
    let filtered_total = count(store, query)?;
    Ok(SessionRowsPage {
        rows: rows(store, query)?,
        filtered_total,
        offset: query.offset as usize,
        limit: query.limit as usize,
    })
}

fn count(store: &Store, query: &SessionSearchQuery) -> Result<usize> {
    let count = if query.with_status {
        count_status(store, query)?
    } else {
        count_ordinary(store, query)?
    };
    Ok(count as usize)
}

fn rows(store: &Store, query: &SessionSearchQuery) -> Result<Vec<SessionSummaryRead>> {
    if query.with_status {
        rows_status(store, query)
    } else {
        rows_ordinary(store, query)
    }
}

fn count_status(store: &Store, query: &SessionSearchQuery) -> Result<i64> {
    let sql = session_search_sql::count(true);
    let values = named_params! {":workspace": query.workspace, ":text": query.text,
    ":now_ms": query.now_ms};
    Ok(store.conn().query_row(&sql, values, |row| row.get(0))?)
}

fn count_ordinary(store: &Store, query: &SessionSearchQuery) -> Result<i64> {
    let sql = session_search_sql::count(false);
    let values = named_params! {":workspace": query.workspace, ":text": query.text};
    Ok(store.conn().query_row(&sql, values, |row| row.get(0))?)
}

fn rows_status(store: &Store, query: &SessionSearchQuery) -> Result<Vec<SessionSummaryRead>> {
    let mut statement = store.conn().prepare(&session_search_sql::page(true))?;
    let values = named_params! {":workspace": query.workspace, ":text": query.text,
    ":now_ms": query.now_ms, ":limit": query.limit, ":offset": query.offset};
    let rows = statement.query_map(values, super::sessions::summary_row)?;
    Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}

fn rows_ordinary(store: &Store, query: &SessionSearchQuery) -> Result<Vec<SessionSummaryRead>> {
    let mut statement = store.conn().prepare(&session_search_sql::page(false))?;
    let values = named_params! {":workspace": query.workspace, ":text": query.text,
    ":limit": query.limit, ":offset": query.offset};
    let rows = statement.query_map(values, super::sessions::summary_row)?;
    Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}

fn searches_status(text: &str) -> bool {
    let text = caseless::default_case_fold_str(text);
    !text.is_empty()
        && ["active", "errored", "orphaned", "idle", "done"]
            .iter()
            .any(|status| status.contains(&text))
}

fn valid_query(
    workspace: &str,
    text: &str,
    offset: usize,
    limit: usize,
    now_ms: u64,
) -> SessionSearchQuery {
    SessionSearchQuery {
        workspace: workspace.into(),
        text: text.into(),
        offset: offset as i64,
        limit: limit as i64,
        now_ms: now_ms as i64,
        with_status: searches_status(text),
    }
}

fn validate(offset: usize, limit: usize, now_ms: u64) -> Result<()> {
    ensure!(limit > 0, "session page limit must be positive");
    ensure!(offset <= i64::MAX as usize, "session page offset too large");
    ensure!(limit <= i64::MAX as usize, "session page limit too large");
    ensure!(
        now_ms <= i64::MAX as u64,
        "session search timestamp too large"
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::searches_status;

    #[test]
    fn status_path_only_for_visible_status_matches() {
        assert!(!searches_status(""));
        assert!(!searches_status("needle"));
        assert!(!searches_status("running"));
        assert!(searches_status("ctiv"));
        assert!(searches_status("IDLE"));
    }
}