Skip to main content

tact_memory/
model.rs

1//! Domain models shared by local storage, remote storage, and the wire protocol.
2
3use crate::server::protocol::RemoteRole;
4use serde::{Deserialize, Serialize};
5
6const PROBATION_DURATION_MS: i64 = 7 * 24 * 60 * 60 * 1_000;
7
8/// Stable identity and optimistic-concurrency version of a memory.
9#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
10pub struct MemoryKey {
11    /// Positive identifier allocated by the owning store.
12    pub id: i64,
13    /// Positive version required for compare-and-swap mutations.
14    pub version: u64,
15    /// Owning remote namespace, or `None` for a local memory.
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub namespace: Option<String>,
18}
19
20impl MemoryKey {
21    /// Creates a key owned by a local store.
22    pub const fn local(id: i64, version: u64) -> Self {
23        Self {
24            id,
25            version,
26            namespace: None,
27        }
28    }
29
30    /// Creates a key owned by `namespace` on a remote store.
31    pub fn remote(namespace: String, id: i64, version: u64) -> Self {
32        Self {
33            id,
34            version,
35            namespace: Some(namespace),
36        }
37    }
38
39    /// Returns whether this key belongs to a local store.
40    pub const fn is_local(&self) -> bool {
41        self.namespace.is_none()
42    }
43}
44
45/// Complete durable state of a memory.
46#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
47pub struct MemoryRecord {
48    /// Stable identity and current version.
49    pub key: MemoryKey,
50    /// User-authored memory content.
51    pub content: String,
52    /// Unix timestamp in milliseconds when this identity was created.
53    pub created_at_ms: i64,
54    /// Unix timestamp in milliseconds when this version was written.
55    pub updated_at_ms: i64,
56    /// Unix timestamp in milliseconds of the most recent matching scan.
57    pub last_scanned_at_ms: Option<i64>,
58    /// Number of matching scans recorded for this version.
59    pub scan_count: u64,
60    /// Unix timestamp in milliseconds of the most recent read.
61    pub last_used_at_ms: Option<i64>,
62    /// Number of reads recorded for this version.
63    pub use_count: u64,
64    /// Expiry time for an unused probationary record, if it remains on probation.
65    pub probation_until_ms: Option<i64>,
66}
67
68/// Ranked, bounded preview returned by a memory scan.
69#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
70pub struct MemoryCandidate {
71    /// Identity and version of the matching memory.
72    pub key: MemoryKey,
73    /// Bounded excerpt suitable for choosing whether to read the record.
74    pub preview: String,
75    /// Retrieval score; larger values rank ahead of smaller values.
76    pub score: f64,
77}
78
79/// Result of a semantic memory scan.
80#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
81pub struct MemoryScan {
82    /// Whether retrieval intentionally returned no candidates.
83    pub abstained: bool,
84    /// Candidates in descending retrieval rank.
85    pub candidates: Vec<MemoryCandidate>,
86}
87
88impl MemoryScan {
89    /// Ranks an in-memory corpus with Tact's deterministic BM25 retrieval.
90    ///
91    /// Server backends that keep a bounded corpus in memory can use this to match local search
92    /// tokenization, scoring, tie-breaking, and preview behavior.
93    pub fn rank(query: &str, memories: &[MemoryRecord], limit: usize) -> Self {
94        let candidates = crate::retrieval::rank(query, memories, limit);
95        Self {
96            abstained: candidates.is_empty(),
97            candidates,
98        }
99    }
100}
101
102/// Resource limits enforced by memory stores and remote-response validation.
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub struct MemoryLimits {
105    /// Maximum UTF-8 bytes in one memory.
106    pub content_bytes: usize,
107    /// Maximum records owned by one local store or remote namespace.
108    pub records: usize,
109    /// Maximum aggregate content bytes per store or namespace.
110    pub total_content_bytes: usize,
111    /// Maximum local SQLite database size.
112    pub database_bytes: usize,
113    /// Maximum candidates returned by one scan.
114    pub scan_results: usize,
115    /// Maximum UTF-8 bytes in one scan query.
116    pub query_bytes: usize,
117    /// Lifetime of a newly written record that has never been read.
118    pub probation_duration_ms: i64,
119}
120
121impl MemoryLimits {
122    /// Limits used by production local and reference remote stores.
123    pub const PRODUCTION: Self = Self {
124        content_bytes: 1_024,
125        records: 512,
126        total_content_bytes: 256 * 1_024,
127        database_bytes: 4 * 1_024 * 1_024,
128        scan_results: 5,
129        query_bytes: 512,
130        probation_duration_ms: PROBATION_DURATION_MS,
131    };
132}
133
134/// Backend selected by a [`crate::MemoryStore`].
135#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
136#[serde(rename_all = "snake_case")]
137pub enum MemorySource {
138    /// Private local SQLite storage.
139    Local,
140    /// Namespaced remote storage.
141    Remote,
142}
143
144/// Negotiated access information for the active backend.
145#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
146pub struct MemoryAccess {
147    /// Selected backend kind.
148    pub source: MemorySource,
149    /// Configured namespace for a remote backend.
150    pub namespace: Option<String>,
151    /// Server-authorized role after remote session negotiation.
152    pub role: Option<RemoteRole>,
153}
154
155/// Counts produced while importing a remote snapshot into local storage.
156#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
157pub struct MemoryImportReport {
158    /// Records inserted into local storage.
159    pub inserted: usize,
160    /// Records omitted because equivalent content already existed.
161    pub skipped: usize,
162}
163
164#[derive(Clone, Debug)]
165pub(crate) struct StoredMemory {
166    pub(crate) namespace: Option<String>,
167    pub(crate) id: i64,
168    pub(crate) content: String,
169    pub(crate) created_at_ms: i64,
170    pub(crate) updated_at_ms: i64,
171    pub(crate) last_scanned_at_ms: Option<i64>,
172    pub(crate) scan_count: u64,
173    pub(crate) last_used_at_ms: Option<i64>,
174    pub(crate) use_count: u64,
175    pub(crate) probation_until_ms: Option<i64>,
176    pub(crate) version: u64,
177}
178
179impl StoredMemory {
180    pub(crate) fn key(&self) -> MemoryKey {
181        match &self.namespace {
182            Some(namespace) => MemoryKey::remote(namespace.clone(), self.id, self.version),
183            None => MemoryKey::local(self.id, self.version),
184        }
185    }
186}
187
188impl From<StoredMemory> for MemoryRecord {
189    fn from(memory: StoredMemory) -> Self {
190        Self {
191            key: memory.key(),
192            content: memory.content,
193            created_at_ms: memory.created_at_ms,
194            updated_at_ms: memory.updated_at_ms,
195            last_scanned_at_ms: memory.last_scanned_at_ms,
196            scan_count: memory.scan_count,
197            last_used_at_ms: memory.last_used_at_ms,
198            use_count: memory.use_count,
199            probation_until_ms: memory.probation_until_ms,
200        }
201    }
202}
203
204/// Normalizes authored content for backend-enforced duplicate detection.
205pub fn normalize_identity(content: &str) -> String {
206    content
207        .split_whitespace()
208        .map(str::to_lowercase)
209        .collect::<Vec<_>>()
210        .join(" ")
211}