Skip to main content

claw_core/store/
context.rs

1//! Temporary context store.
2//!
3//! The `context` table holds short-lived contextual data that agents attach to
4//! a running session. Entries are intended to expire quickly and are typically
5//! pruned by a background job or on engine startup. Unlike active memory,
6//! context records carry an explicit TTL (`expires_at`).
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use sqlx::SqlitePool;
11use uuid::Uuid;
12
13use crate::error::{ClawError, ClawResult};
14
15/// A single temporary-context record.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ContextRecord {
18    /// Unique row identifier.
19    pub id: Uuid,
20    /// The session this context entry belongs to.
21    pub session_id: String,
22    /// Logical namespace / key for the context entry.
23    pub key: String,
24    /// Serialized JSON payload.
25    pub value: serde_json::Value,
26    /// Timestamp when this record was created.
27    pub created_at: DateTime<Utc>,
28    /// Timestamp after which this record should be considered expired.
29    pub expires_at: DateTime<Utc>,
30}
31
32/// Data-access object for the `context` table.
33#[derive(Debug)]
34pub struct ContextStore<'a> {
35    pool: &'a SqlitePool,
36}
37
38impl<'a> ContextStore<'a> {
39    /// Create a new store bound to `pool`.
40    pub fn new(pool: &'a SqlitePool) -> Self {
41        ContextStore { pool }
42    }
43
44    /// Insert a new context record.
45    ///
46    /// # Errors
47    ///
48    /// Returns a [`ClawError`] if the SQL execution fails.
49    pub async fn insert(&self, record: &ContextRecord) -> ClawResult<()> {
50        sqlx::query(
51            r#"
52            INSERT INTO context (id, session_id, key, value, created_at, expires_at)
53            VALUES (?, ?, ?, ?, ?, ?)
54            "#,
55        )
56        .bind(record.id.to_string())
57        .bind(&record.session_id)
58        .bind(&record.key)
59        .bind(serde_json::to_string(&record.value)?)
60        .bind(record.created_at.to_rfc3339())
61        .bind(record.expires_at.to_rfc3339())
62        .execute(self.pool)
63        .await?;
64
65        Ok(())
66    }
67
68    /// Delete all expired context records (where `expires_at <= now`).
69    ///
70    /// Returns the number of rows deleted.
71    ///
72    /// # Errors
73    ///
74    /// Returns a [`ClawError`] if the SQL execution fails.
75    pub async fn purge_expired(&self) -> ClawResult<u64> {
76        let now = Utc::now().to_rfc3339();
77        let result = sqlx::query("DELETE FROM context WHERE expires_at <= ?")
78            .bind(now)
79            .execute(self.pool)
80            .await?;
81
82        Ok(result.rows_affected())
83    }
84
85    /// Fetch all non-expired context records for `session_id`.
86    ///
87    /// # Errors
88    ///
89    /// Returns a [`ClawError`] if the query fails.
90    pub async fn get_active(&self, session_id: &str) -> ClawResult<Vec<ContextRecord>> {
91        let now = Utc::now().to_rfc3339();
92        let rows = sqlx::query_as::<_, (String, String, String, String, String, String)>(
93            "SELECT id, session_id, key, value, created_at, expires_at \
94             FROM context WHERE session_id = ? AND expires_at > ? ORDER BY created_at ASC",
95        )
96        .bind(session_id)
97        .bind(now)
98        .fetch_all(self.pool)
99        .await?;
100
101        rows.into_iter()
102            .map(|(id, session_id, key, value, created_at, expires_at)| {
103                Ok(ContextRecord {
104                    id: Uuid::parse_str(&id).map_err(|e| ClawError::Store(e.to_string()))?,
105                    session_id,
106                    key,
107                    value: serde_json::from_str(&value)?,
108                    created_at: DateTime::parse_from_rfc3339(&created_at)
109                        .map_err(|e| ClawError::Store(e.to_string()))?
110                        .with_timezone(&Utc),
111                    expires_at: DateTime::parse_from_rfc3339(&expires_at)
112                        .map_err(|e| ClawError::Store(e.to_string()))?
113                        .with_timezone(&Utc),
114                })
115            })
116            .collect()
117    }
118}