remem-ai 0.4.9

Persistent memory for Claude Code and Codex — single binary, automatic context
Documentation
use anyhow::Result;
use serde::Serialize;

use crate::cli::types::PendingAction;
use crate::db::{self, pending::admin::FailedPendingRow};

pub(in crate::cli) fn run_pending(action: PendingAction) -> Result<()> {
    let conn = db::open_db()?;

    match action {
        PendingAction::ListFailed {
            project,
            limit,
            json,
        } => {
            let rows = db::pending::admin::list_failed(&conn, project.as_deref(), limit)?;
            if json {
                let output = PendingListFailedJson {
                    project,
                    limit: limit.max(1),
                    count: rows.len(),
                    failed: rows,
                };
                println!("{}", serde_json::to_string_pretty(&output)?);
                return Ok(());
            }
            if rows.is_empty() {
                println!("No failed pending observations.");
                return Ok(());
            }
            println!("Failed pending observations ({}):", rows.len());
            for row in rows {
                let ts = chrono::DateTime::from_timestamp(row.updated_at_epoch, 0)
                    .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
                    .unwrap_or_default();
                let err = row
                    .last_error
                    .as_deref()
                    .map(|message| db::truncate_str(message, 120).to_string())
                    .unwrap_or_default();
                println!(
                    "  [{}] {} | {} | {} | attempt={} | {}",
                    row.id, row.project, row.session_id, row.tool_name, row.attempt_count, ts
                );
                if !err.is_empty() {
                    println!("      error: {}", err);
                }
            }
        }
        PendingAction::RetryFailed {
            project,
            limit,
            dry_run,
        } => {
            if dry_run {
                let count = db::pending::admin::count_failed_retry_candidates(
                    &conn,
                    project.as_deref(),
                    limit,
                )?;
                println!("Would move {} failed rows back to pending.", count);
            } else {
                let count = db::pending::admin::retry_failed(&conn, project.as_deref(), limit)?;
                println!("Moved {} failed rows back to pending.", count);
            }
        }
        PendingAction::PurgeFailed {
            project,
            older_than_days,
            dry_run,
        } => {
            if dry_run {
                let count = db::pending::admin::count_failed_purge_candidates(
                    &conn,
                    project.as_deref(),
                    older_than_days,
                )?;
                println!(
                    "Would purge {} failed rows older than {} day(s).",
                    count, older_than_days
                );
            } else {
                let count =
                    db::pending::admin::purge_failed(&conn, project.as_deref(), older_than_days)?;
                println!(
                    "Purged {} failed rows older than {} day(s).",
                    count, older_than_days
                );
            }
        }
    }

    Ok(())
}

#[derive(Debug, Clone, Serialize)]
struct PendingListFailedJson {
    project: Option<String>,
    limit: i64,
    count: usize,
    failed: Vec<FailedPendingRow>,
}

#[cfg(test)]
mod tests {
    use serde_json::Value;

    use super::*;

    #[test]
    fn cli_pending_list_failed_json_is_machine_parseable(
    ) -> std::result::Result<(), serde_json::Error> {
        let output = PendingListFailedJson {
            project: Some("proj".to_string()),
            limit: 1,
            count: 1,
            failed: vec![FailedPendingRow {
                id: 1,
                session_id: "session-1".to_string(),
                project: "proj".to_string(),
                tool_name: "Bash".to_string(),
                attempt_count: 3,
                updated_at_epoch: 10,
                last_error: Some("failed".to_string()),
            }],
        };

        let text = serde_json::to_string(&output)?;
        let parsed: Value = serde_json::from_str(&text)?;

        assert_eq!(parsed["project"], "proj");
        assert_eq!(parsed["count"], 1);
        assert_eq!(parsed["failed"][0]["tool_name"], "Bash");
        Ok(())
    }
}