kaizen-cli 0.1.38

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::rows::*;
use super::*;

impl Store {
    pub fn upsert_feedback(&self, r: &crate::feedback::types::FeedbackRecord) -> Result<()> {
        use crate::feedback::types::FeedbackLabel;
        self.conn.execute(
            "INSERT OR REPLACE INTO session_feedback
             (id, session_id, score, label, note, created_at_ms)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            rusqlite::params![
                r.id,
                r.session_id,
                r.score.as_ref().map(|s| s.0 as i64),
                r.label.as_ref().map(FeedbackLabel::to_db_str),
                r.note,
                r.created_at_ms as i64,
            ],
        )?;
        let payload = serde_json::to_string(r).unwrap_or_default();
        self.conn.execute(
            "INSERT INTO sync_outbox (session_id, kind, payload, sent)
             VALUES (?1, 'session_feedback', ?2, 0)",
            rusqlite::params![r.session_id, payload],
        )?;
        Ok(())
    }

    pub fn list_feedback_in_window(
        &self,
        start_ms: u64,
        end_ms: u64,
    ) -> Result<Vec<crate::feedback::types::FeedbackRecord>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, session_id, score, label, note, created_at_ms
             FROM session_feedback
             WHERE created_at_ms >= ?1 AND created_at_ms < ?2
             ORDER BY created_at_ms ASC",
        )?;
        let rows = stmt.query_map(
            rusqlite::params![start_ms as i64, end_ms as i64],
            feedback_row,
        )?;
        rows.map(|r| r.map_err(anyhow::Error::from)).collect()
    }

    pub fn feedback_for_sessions(
        &self,
        ids: &[String],
    ) -> Result<std::collections::HashMap<String, crate::feedback::types::FeedbackRecord>> {
        if ids.is_empty() {
            return Ok(std::collections::HashMap::new());
        }
        let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
        let sql = format!(
            "SELECT id, session_id, score, label, note, created_at_ms
             FROM session_feedback WHERE session_id IN ({placeholders})
             ORDER BY created_at_ms DESC"
        );
        let mut stmt = self.conn.prepare(&sql)?;
        let params: Vec<&dyn rusqlite::ToSql> =
            ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
        let rows = stmt.query_map(params.as_slice(), feedback_row)?;
        let mut map = std::collections::HashMap::new();
        for row in rows {
            let r = row?;
            map.entry(r.session_id.clone()).or_insert(r);
        }
        Ok(map)
    }
}