claw-core 0.1.2

Embedded local database engine for ClawDB — an agent-native cognitive database
Documentation
//! Session state store.
//!
//! The `session_state` table persists per-session key-value pairs for AI
//! agents. Unlike active/scratchpad memory, session state is typically
//! retained across agent restarts and tied to a logical session identifier.

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

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

/// A single session-state record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRecord {
    /// Unique row identifier.
    pub id: Uuid,
    /// Logical session identifier (e.g. a conversation ID).
    pub session_id: String,
    /// Logical key within the session.
    pub key: String,
    /// Serialized JSON value.
    pub value: serde_json::Value,
    /// Timestamp when this record was last updated.
    pub updated_at: DateTime<Utc>,
}

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

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

    /// Upsert a session-state record.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the SQL execution fails.
    pub async fn upsert(&self, record: &SessionRecord) -> ClawResult<()> {
        sqlx::query(
            r#"
            INSERT INTO session_state (id, session_id, key, value, updated_at)
            VALUES (?, ?, ?, ?, ?)
            ON CONFLICT (session_id, key)
            DO UPDATE SET value = excluded.value,
                          updated_at = excluded.updated_at
            "#,
        )
        .bind(record.id.to_string())
        .bind(&record.session_id)
        .bind(&record.key)
        .bind(serde_json::to_string(&record.value)?)
        .bind(record.updated_at.to_rfc3339())
        .execute(self.pool)
        .await?;

        Ok(())
    }

    /// Fetch all records for `session_id`.
    ///
    /// # Errors
    ///
    /// Returns a [`ClawError`] if the query fails.
    pub async fn get_session(&self, session_id: &str) -> ClawResult<Vec<SessionRecord>> {
        let rows = sqlx::query_as::<_, (String, String, String, String, String)>(
            "SELECT id, session_id, key, value, updated_at \
             FROM session_state WHERE session_id = ? ORDER BY key",
        )
        .bind(session_id)
        .fetch_all(self.pool)
        .await?;

        rows.into_iter()
            .map(|(id, session_id, key, value, updated_at)| {
                Ok(SessionRecord {
                    id: Uuid::parse_str(&id).map_err(|e| ClawError::Store(e.to_string()))?,
                    session_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),
                })
            })
            .collect()
    }

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

        Ok(result.rows_affected())
    }
}