claw-core 0.1.2

Embedded local database engine for ClawDB — an agent-native cognitive database
Documentation
//! Temporary context store.
//!
//! The `context` table holds short-lived contextual data that agents attach to
//! a running session. Entries are intended to expire quickly and are typically
//! pruned by a background job or on engine startup. Unlike active memory,
//! context records carry an explicit TTL (`expires_at`).

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use uuid::Uuid;

use crate::error::{ClawError, ClawResult};

/// A single temporary-context record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextRecord {
    /// Unique row identifier.
    pub id: Uuid,
    /// The session this context entry belongs to.
    pub session_id: String,
    /// Logical namespace / key for the context entry.
    pub key: String,
    /// Serialized JSON payload.
    pub value: serde_json::Value,
    /// Timestamp when this record was created.
    pub created_at: DateTime<Utc>,
    /// Timestamp after which this record should be considered expired.
    pub expires_at: DateTime<Utc>,
}

/// Data-access object for the `context` table.
#[derive(Debug)]
pub struct ContextStore<'a> {
    pool: &'a SqlitePool,
}

impl<'a> ContextStore<'a> {
    /// Create a new store bound to `pool`.
    pub fn new(pool: &'a SqlitePool) -> Self {
        ContextStore { pool }
    }

    /// Insert a new context record.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the SQL execution fails.
    pub async fn insert(&self, record: &ContextRecord) -> ClawResult<()> {
        sqlx::query(
            r#"
            INSERT INTO context (id, session_id, key, value, created_at, expires_at)
            VALUES (?, ?, ?, ?, ?, ?)
            "#,
        )
        .bind(record.id.to_string())
        .bind(&record.session_id)
        .bind(&record.key)
        .bind(serde_json::to_string(&record.value)?)
        .bind(record.created_at.to_rfc3339())
        .bind(record.expires_at.to_rfc3339())
        .execute(self.pool)
        .await?;

        Ok(())
    }

    /// Delete all expired context records (where `expires_at <= now`).
    ///
    /// Returns the number of rows deleted.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the SQL execution fails.
    pub async fn purge_expired(&self) -> ClawResult<u64> {
        let now = Utc::now().to_rfc3339();
        let result = sqlx::query("DELETE FROM context WHERE expires_at <= ?")
            .bind(now)
            .execute(self.pool)
            .await?;

        Ok(result.rows_affected())
    }

    /// Fetch all non-expired context records for `session_id`.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the query fails.
    pub async fn get_active(&self, session_id: &str) -> ClawResult<Vec<ContextRecord>> {
        let now = Utc::now().to_rfc3339();
        let rows = sqlx::query_as::<_, (String, String, String, String, String, String)>(
            "SELECT id, session_id, key, value, created_at, expires_at \
             FROM context WHERE session_id = ? AND expires_at > ? ORDER BY created_at ASC",
        )
        .bind(session_id)
        .bind(now)
        .fetch_all(self.pool)
        .await?;

        rows.into_iter()
            .map(|(id, session_id, key, value, created_at, expires_at)| {
                Ok(ContextRecord {
                    id: Uuid::parse_str(&id).map_err(|e| ClawError::Store(e.to_string()))?,
                    session_id,
                    key,
                    value: serde_json::from_str(&value)?,
                    created_at: DateTime::parse_from_rfc3339(&created_at)
                        .map_err(|e| ClawError::Store(e.to_string()))?
                        .with_timezone(&Utc),
                    expires_at: DateTime::parse_from_rfc3339(&expires_at)
                        .map_err(|e| ClawError::Store(e.to_string()))?
                        .with_timezone(&Utc),
                })
            })
            .collect()
    }
}