codex-threadripper 0.3.0

Human-first CLI that keeps Codex thread history aligned to one provider bucket.
use anyhow::Result;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use std::time::Instant;

use crate::cli::DEFAULT_BUCKET_PADDING_BYTES;
use crate::codex_config::read_provider_from_config;
use crate::codex_config::resolve_sqlite_path;
use crate::rollout::RolloutProgressConfig;
use crate::rollout::RolloutScope;
use crate::rollout::reconcile_rollout_metadata_from_sqlite_with_progress;
use crate::service;
use crate::service::ServiceStatus as BackgroundServiceStatus;
use crate::state_db::ProviderDistribution;
use crate::state_db::create_sqlite_backup_file;
use crate::state_db::inspect_sqlite_distribution;
use crate::state_db::reconcile_sqlite_in_place;

#[derive(Debug)]
pub(crate) struct ReconcileSummary {
    pub(crate) provider: String,
    pub(crate) changed_rows: u64,
    pub(crate) total_rows: u64,
    pub(crate) changed_rollouts: u64,
    pub(crate) checked_rollouts: u64,
    pub(crate) prepared_rollouts: u64,
    pub(crate) skipped_rollouts: u64,
    pub(crate) elapsed: Duration,
    pub(crate) backup_path: Option<PathBuf>,
    pub(crate) rollout_journal_path: Option<PathBuf>,
}

#[derive(Debug)]
pub(crate) struct StatusSummary {
    pub(crate) codex_home: PathBuf,
    pub(crate) sqlite_path: PathBuf,
    pub(crate) config_path: PathBuf,
    pub(crate) provider: String,
    pub(crate) total_rows: u64,
    pub(crate) mismatched_rows: u64,
    pub(crate) distribution: ProviderDistribution,
    pub(crate) service_status: BackgroundServiceStatus,
}

pub(crate) fn collect_status(
    codex_home: &Path,
    provider_override: Option<&str>,
    profile_override: Option<&str>,
) -> Result<StatusSummary> {
    let config_path = codex_home.join("config.toml");
    let sqlite_path = resolve_sqlite_path(codex_home, profile_override)?;
    let provider = match provider_override {
        Some(provider) => provider.to_string(),
        None => read_provider_from_config(codex_home, profile_override)?,
    };
    let (total_rows, mismatched_rows, distribution) =
        inspect_sqlite_distribution(&sqlite_path, provider.as_str())?;
    let service_status = service::current_service_status()?;

    Ok(StatusSummary {
        codex_home: codex_home.to_path_buf(),
        sqlite_path,
        config_path,
        provider,
        total_rows,
        mismatched_rows,
        distribution,
        service_status,
    })
}

pub(crate) fn reconcile_once(
    codex_home: &Path,
    provider_override: Option<&str>,
    profile_override: Option<&str>,
    rollout_scope: RolloutScope,
) -> Result<ReconcileSummary> {
    reconcile_once_with_progress(
        codex_home,
        provider_override,
        profile_override,
        rollout_scope,
        None,
    )
}

fn reconcile_once_with_progress(
    codex_home: &Path,
    provider_override: Option<&str>,
    profile_override: Option<&str>,
    rollout_scope: RolloutScope,
    progress: Option<RolloutProgressConfig>,
) -> Result<ReconcileSummary> {
    let provider = match provider_override {
        Some(provider) => provider.to_string(),
        None => read_provider_from_config(codex_home, profile_override)?,
    };
    let sqlite_path = resolve_sqlite_path(codex_home, profile_override)?;
    let started = Instant::now();
    let rollout_summary = reconcile_rollout_metadata_from_sqlite_with_progress(
        &sqlite_path,
        codex_home,
        provider.as_str(),
        rollout_scope,
        None,
        DEFAULT_BUCKET_PADDING_BYTES,
        progress,
    )?;
    let (changed_rows, total_rows) = reconcile_sqlite_in_place(&sqlite_path, provider.as_str())?;

    Ok(ReconcileSummary {
        provider,
        changed_rows,
        total_rows,
        changed_rollouts: rollout_summary.changed_files,
        checked_rollouts: rollout_summary.checked_files,
        prepared_rollouts: rollout_summary.prepared_files,
        skipped_rollouts: rollout_summary.skipped_files,
        elapsed: started.elapsed(),
        backup_path: None,
        rollout_journal_path: rollout_summary.journal_path,
    })
}

pub(crate) fn reconcile_once_with_backup_progress(
    codex_home: &Path,
    provider_override: Option<&str>,
    profile_override: Option<&str>,
    rollout_scope: RolloutScope,
    progress: Option<RolloutProgressConfig>,
) -> Result<ReconcileSummary> {
    reconcile_once_with_backup_and_padding(
        codex_home,
        provider_override,
        profile_override,
        rollout_scope,
        DEFAULT_BUCKET_PADDING_BYTES,
        progress,
    )
}

pub(crate) fn reconcile_once_with_backup_and_padding(
    codex_home: &Path,
    provider_override: Option<&str>,
    profile_override: Option<&str>,
    rollout_scope: RolloutScope,
    padding_bytes: usize,
    progress: Option<RolloutProgressConfig>,
) -> Result<ReconcileSummary> {
    let provider = match provider_override {
        Some(provider) => provider.to_string(),
        None => read_provider_from_config(codex_home, profile_override)?,
    };
    let sqlite_path = resolve_sqlite_path(codex_home, profile_override)?;
    let started = Instant::now();
    let backup_path = create_sqlite_backup_file(&sqlite_path)?;
    let rollout_journal_path =
        backup_path
            .parent()
            .unwrap_or_else(|| Path::new("."))
            .join(format!(
                "rollouts.{}.jsonl",
                backup_path
                    .file_name()
                    .and_then(|name| name.to_str())
                    .unwrap_or("state-db.bak")
            ));
    let rollout_summary = reconcile_rollout_metadata_from_sqlite_with_progress(
        &sqlite_path,
        codex_home,
        provider.as_str(),
        rollout_scope,
        Some(rollout_journal_path.as_path()),
        padding_bytes,
        progress,
    )?;
    let (changed_rows, total_rows) = reconcile_sqlite_in_place(&sqlite_path, provider.as_str())?;

    Ok(ReconcileSummary {
        provider,
        changed_rows,
        total_rows,
        changed_rollouts: rollout_summary.changed_files,
        checked_rollouts: rollout_summary.checked_files,
        prepared_rollouts: rollout_summary.prepared_files,
        skipped_rollouts: rollout_summary.skipped_files,
        elapsed: started.elapsed(),
        backup_path: Some(backup_path),
        rollout_journal_path: rollout_summary.journal_path,
    })
}