use anyhow::Result;use anyhow::anyhow;use chrono::DateTime;use chrono::Utc;use codex_protocol::ThreadId;use serde::Serialize;use sqlx::Row;use sqlx::sqlite::SqliteRow;use super::epoch_millis_to_datetime;#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]#[serde(rename_all = "snake_case")]pub enum ThreadGoalStatus { Active, Paused, Blocked, UsageLimited, BudgetLimited, Complete,}impl ThreadGoalStatus { pub fn as_str(self) -> &'static str { match self { Self::Active => "active", Self::Paused => "paused", Self::Blocked => "blocked", Self::UsageLimited => "usage_limited", Self::BudgetLimited => "budget_limited", Self::Complete => "complete", } } pub fn is_active(self) -> bool { self == Self::Active } pub fn is_terminal(self) -> bool { matches!(self, Self::BudgetLimited | Self::Complete) }}impl TryFrom<&str> for ThreadGoalStatus { type Error = anyhow::Error; fn try_from(value: &str) -> Result<Self> { match value { "active" => Ok(Self::Active), "paused" => Ok(Self::Paused), "blocked" => Ok(Self::Blocked), "usage_limited" => Ok(Self::UsageLimited), "budget_limited" => Ok(Self::BudgetLimited), "complete" => Ok(Self::Complete), other => Err(anyhow!("unknown thread goal status `{other}`")), } }}#[derive(Debug, Clone, PartialEq, Eq)]pub struct ThreadGoal { pub thread_id: ThreadId, pub goal_id: String, pub objective: String, pub status: ThreadGoalStatus, pub token_budget: Option<i64>, pub tokens_used: i64, pub time_used_seconds: i64, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,}pub(crate) struct ThreadGoalRow { pub thread_id: String, pub goal_id: String, pub objective: String, pub status: String, pub token_budget: Option<i64>, pub tokens_used: i64, pub time_used_seconds: i64, pub created_at_ms: i64, pub updated_at_ms: i64,}impl ThreadGoalRow { pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> { Ok(Self { thread_id: row.try_get("thread_id")?, goal_id: row.try_get("goal_id")?, objective: row.try_get("objective")?, status: row.try_get("status")?, token_budget: row.try_get("token_budget")?, tokens_used: row.try_get("tokens_used")?, time_used_seconds: row.try_get("time_used_seconds")?, created_at_ms: row.try_get("created_at_ms")?, updated_at_ms: row.try_get("updated_at_ms")?, }) }}impl TryFrom<ThreadGoalRow> for ThreadGoal { type Error = anyhow::Error; fn try_from(row: ThreadGoalRow) -> Result<Self> { Ok(Self { thread_id: ThreadId::try_from(row.thread_id)?, goal_id: row.goal_id, objective: row.objective, status: ThreadGoalStatus::try_from(row.status.as_str())?, token_budget: row.token_budget, tokens_used: row.tokens_used, time_used_seconds: row.time_used_seconds, created_at: epoch_millis_to_datetime(row.created_at_ms)?, updated_at: epoch_millis_to_datetime(row.updated_at_ms)?, }) }}