claw-core 0.1.1

Embedded local database engine for ClawDB — an agent-native cognitive database
Documentation
//! Active / scratchpad memory store.
//!
//! The `active_memory` table functions as a fast scratchpad for AI agent
//! working memory. Records are keyed by `(agent_id, key)` and hold an
//! arbitrary JSON value. Values can be upserted, fetched, and expired
//! individually or in bulk.

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

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

/// A single active-memory record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveMemoryRecord {
    /// Unique row identifier.
    pub id: Uuid,
    /// The agent that owns this record.
    pub agent_id: String,
    /// Logical key within the agent's scratchpad.
    pub key: String,
    /// Serialized JSON value.
    pub value: serde_json::Value,
    /// Timestamp when this record was created or last updated.
    pub updated_at: DateTime<Utc>,
    /// Optional expiry timestamp. `None` means the record never expires.
    pub expires_at: Option<DateTime<Utc>>,
}

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

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

    /// Upsert a record into `active_memory`.
    ///
    /// If a record with the same `(agent_id, key)` already exists, its value
    /// and `updated_at` timestamp are updated.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the SQL execution fails.
    pub async fn upsert(&self, record: &ActiveMemoryRecord) -> ClawResult<()> {
        sqlx::query(
            r#"
            INSERT INTO active_memory (id, agent_id, key, value, updated_at, expires_at)
            VALUES (?, ?, ?, ?, ?, ?)
            ON CONFLICT (agent_id, key)
            DO UPDATE SET value = excluded.value,
                          updated_at = excluded.updated_at,
                          expires_at = excluded.expires_at
            "#,
        )
        .bind(record.id.to_string())
        .bind(&record.agent_id)
        .bind(&record.key)
        .bind(serde_json::to_string(&record.value)?)
        .bind(record.updated_at.to_rfc3339())
        .bind(record.expires_at.map(|t| t.to_rfc3339()))
        .execute(self.pool)
        .await?;

        Ok(())
    }

    /// Fetch the record for `(agent_id, key)`, if it exists.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the query fails.
    pub async fn get(&self, agent_id: &str, key: &str) -> ClawResult<Option<ActiveMemoryRecord>> {
        let row = sqlx::query_as::<_, (String, String, String, String, String, Option<String>)>(
            "SELECT id, agent_id, key, value, updated_at, expires_at \
             FROM active_memory WHERE agent_id = ? AND key = ?",
        )
        .bind(agent_id)
        .bind(key)
        .fetch_optional(self.pool)
        .await?;

        row.map(|(id, agent_id, key, value, updated_at, expires_at)| {
            Ok(ActiveMemoryRecord {
                id: Uuid::parse_str(&id).map_err(|e| ClawError::Store(e.to_string()))?,
                agent_id,
                key,
                value: serde_json::from_str(&value)?,
                updated_at: DateTime::parse_from_rfc3339(&updated_at)
                    .map_err(|e| ClawError::Store(e.to_string()))?
                    .with_timezone(&Utc),
                expires_at: expires_at
                    .map(|s| {
                        DateTime::parse_from_rfc3339(&s)
                            .map(|dt| dt.with_timezone(&Utc))
                            .map_err(|e| ClawError::Store(e.to_string()))
                    })
                    .transpose()?,
            })
        })
        .transpose()
    }

    /// Delete all records belonging to `agent_id`.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the SQL execution fails.
    pub async fn clear_agent(&self, agent_id: &str) -> ClawResult<u64> {
        let result = sqlx::query("DELETE FROM active_memory WHERE agent_id = ?")
            .bind(agent_id)
            .execute(self.pool)
            .await?;

        Ok(result.rows_affected())
    }
}