chaotic_semantic_memory 0.3.8

AI memory systems with hyperdimensional vectors and chaotic reservoirs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
use crate::persistence::Persistence;
use csm_core_lib::error::{MemoryError, Result};
use libsql::params;
use tracing::info;

impl Persistence {
    pub(crate) async fn table_exists(
        &self,
        conn: &libsql::Connection,
        table_name: &str,
    ) -> Result<bool> {
        let mut rows = conn
            .query(
                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
                params![table_name],
            )
            .await
            .map_err(|e| MemoryError::database(format!("Failed to check table existence: {e}")))?;

        if let Some(row) = rows
            .next()
            .await
            .map_err(|e| MemoryError::database(format!("Failed to fetch table existence: {e}")))?
        {
            let count: i64 = row
                .get(0)
                .map_err(|e| MemoryError::database(format!("Failed to parse table count: {e}")))?;
            Ok(count > 0)
        } else {
            Ok(false)
        }
    }

    pub(crate) async fn column_exists(
        &self,
        conn: &libsql::Connection,
        table_name: &str,
        column_name: &str,
    ) -> Result<bool> {
        // Validate table_name to prevent potential issues in pragma_table_info (CWE-89)
        if table_name.is_empty() {
            return Err(MemoryError::InvalidInput {
                field: "table_name".to_string(),
                reason: "Table name cannot be empty".to_string(),
            });
        }

        // Use parameter binding for both the table name and the column name.
        // pragma_table_info supports parameter binding for its argument.
        let sql = "SELECT COUNT(*) FROM pragma_table_info(?1) WHERE name = ?2";

        let mut rows = conn
            .query(sql, params![table_name, column_name])
            .await
            .map_err(|e| MemoryError::database(format!("Failed to check column existence: {e}")))?;

        if let Some(row) = rows
            .next()
            .await
            .map_err(|e| MemoryError::database(format!("Failed to fetch column existence: {e}")))?
        {
            let count: i64 = row
                .get(0)
                .map_err(|e| MemoryError::database(format!("Failed to parse column count: {e}")))?;
            Ok(count > 0)
        } else {
            Ok(false)
        }
    }

    pub(crate) async fn apply_v5_namespace_migration(
        &self,
        conn: &libsql::Connection,
    ) -> Result<()> {
        if self.table_exists(conn, "concepts").await? {
            if self.table_exists(conn, "csm_concepts").await? {
                conn.execute_batch(
                    "INSERT OR IGNORE INTO csm_concepts (id, vector, metadata, created_at, modified_at)
                     SELECT id, vector, metadata, created_at, modified_at FROM concepts;
                     DROP TABLE concepts;",
                )
                .await
                .map_err(|e| {
                    MemoryError::database(format!("Failed migration v5 concepts merge: {e}"))
                })?;
            } else {
                conn.execute_batch("ALTER TABLE concepts RENAME TO csm_concepts;")
                    .await
                    .map_err(|e| {
                        MemoryError::database(format!("Failed migration v5 concepts rename: {e}"))
                    })?;
            }
        }

        if self.table_exists(conn, "associations").await? {
            if self.table_exists(conn, "csm_associations").await? {
                conn.execute_batch(
                    "INSERT OR IGNORE INTO csm_associations (from_id, to_id, strength)
                     SELECT from_id, to_id, strength FROM associations;
                     DROP TABLE associations;",
                )
                .await
                .map_err(|e| {
                    MemoryError::database(format!("Failed migration v5 associations merge: {e}"))
                })?;
            } else {
                conn.execute_batch("ALTER TABLE associations RENAME TO csm_associations;")
                    .await
                    .map_err(|e| {
                        MemoryError::database(format!(
                            "Failed migration v5 associations rename: {e}"
                        ))
                    })?;
            }
        }

        if self.table_exists(conn, "concept_versions").await? {
            if self.table_exists(conn, "csm_versions").await? {
                conn.execute_batch(
                    "INSERT OR IGNORE INTO csm_versions (concept_id, version, vector, metadata, modified_at)
                     SELECT concept_id, version, vector, metadata, modified_at FROM concept_versions;
                     DROP TABLE concept_versions;",
                )
                .await
                .map_err(|e| {
                    MemoryError::database(format!("Failed migration v5 versions merge: {e}"))
                })?;
            } else {
                conn.execute_batch("ALTER TABLE concept_versions RENAME TO csm_versions;")
                    .await
                    .map_err(|e| {
                        MemoryError::database(format!("Failed migration v5 versions rename: {e}"))
                    })?;
            }
        }

        if self.table_exists(conn, "canonical_concepts").await? {
            if self.table_exists(conn, "csm_canonical").await? {
                conn.execute_batch(
                    "INSERT OR IGNORE INTO csm_canonical (id, version, labels_json, related_json)
                     SELECT id, version, labels_json, related_json FROM canonical_concepts;
                     DROP TABLE canonical_concepts;",
                )
                .await
                .map_err(|e| {
                    MemoryError::database(format!("Failed migration v5 canonical merge: {e}"))
                })?;
            } else {
                conn.execute_batch("ALTER TABLE canonical_concepts RENAME TO csm_canonical;")
                    .await
                    .map_err(|e| {
                        MemoryError::database(format!("Failed migration v5 canonical rename: {e}"))
                    })?;
            }
        }

        if self.table_exists(conn, "__schema_version").await? {
            conn.execute_batch("DROP TABLE __schema_version;")
                .await
                .map_err(|e| {
                    MemoryError::database(format!("Failed migration v5 schema cleanup: {e}"))
                })?;
        }

        Ok(())
    }

    pub(crate) async fn apply_v8_namespace_migration(
        &self,
        conn: &libsql::Connection,
    ) -> Result<()> {
        // v8: Namespace isolation. Update PKs and FKs to include namespace.
        conn.execute_batch(
            "-- 1. Concepts
             ALTER TABLE csm_concepts RENAME TO csm_concepts_old;
             CREATE TABLE csm_concepts (
                 namespace TEXT NOT NULL DEFAULT '_default',
                 id TEXT NOT NULL,
                 vector BLOB NOT NULL,
                 metadata TEXT NOT NULL,
                 created_at INTEGER NOT NULL,
                 modified_at INTEGER NOT NULL,
                 expires_at INTEGER,
                 canonical_concept_ids_json TEXT,
                 PRIMARY KEY (namespace, id)
             );
             INSERT INTO csm_concepts (id, vector, metadata, created_at, modified_at, expires_at, canonical_concept_ids_json)
             SELECT id, vector, metadata, created_at, modified_at, expires_at, canonical_concept_ids_json FROM csm_concepts_old;
             DROP TABLE csm_concepts_old;
             CREATE INDEX idx_csm_concepts_namespace ON csm_concepts(namespace);

             -- 2. Associations
             ALTER TABLE csm_associations RENAME TO csm_associations_old;
             CREATE TABLE csm_associations (
                 namespace TEXT NOT NULL DEFAULT '_default',
                 from_id TEXT NOT NULL,
                 to_id TEXT NOT NULL,
                 strength REAL NOT NULL,
                 PRIMARY KEY (namespace, from_id, to_id),
                 FOREIGN KEY (namespace, from_id) REFERENCES csm_concepts(namespace, id),
                 FOREIGN KEY (namespace, to_id) REFERENCES csm_concepts(namespace, id)
             );
             INSERT INTO csm_associations (from_id, to_id, strength)
             SELECT from_id, to_id, strength FROM csm_associations_old;
             DROP TABLE csm_associations_old;
             CREATE INDEX idx_csm_associations_namespace ON csm_associations(namespace);
             CREATE INDEX idx_csm_associations_from ON csm_associations(namespace, from_id);

             -- 3. Versions
             ALTER TABLE csm_versions RENAME TO csm_versions_old;
             CREATE TABLE csm_versions (
                 namespace TEXT NOT NULL DEFAULT '_default',
                 concept_id TEXT NOT NULL,
                 version INTEGER NOT NULL,
                 vector BLOB NOT NULL,
                 metadata TEXT NOT NULL,
                 modified_at INTEGER NOT NULL,
                 PRIMARY KEY (namespace, concept_id, version),
                 FOREIGN KEY (namespace, concept_id) REFERENCES csm_concepts(namespace, id)
             );
             INSERT INTO csm_versions (concept_id, version, vector, metadata, modified_at)
             SELECT concept_id, version, vector, metadata, modified_at FROM csm_versions_old;
             DROP TABLE csm_versions_old;
             CREATE INDEX idx_csm_versions_namespace ON csm_versions(namespace);
             CREATE INDEX idx_csm_versions_modified_at ON csm_versions(namespace, modified_at);

             -- 4. HNSW Graph
             ALTER TABLE csm_hnsw_graph RENAME TO csm_hnsw_graph_old;
             CREATE TABLE csm_hnsw_graph (
                 namespace TEXT NOT NULL DEFAULT '_default',
                 id TEXT NOT NULL,
                 data BLOB NOT NULL,
                 modified_at INTEGER NOT NULL,
                 PRIMARY KEY (namespace, id)
             );
             INSERT INTO csm_hnsw_graph (id, data, modified_at)
             SELECT id, data, modified_at FROM csm_hnsw_graph_old;
             DROP TABLE csm_hnsw_graph_old;
             CREATE INDEX idx_csm_hnsw_graph_namespace ON csm_hnsw_graph(namespace);

             -- 5. Canonical Concepts
             ALTER TABLE csm_canonical RENAME TO csm_canonical_old;
             CREATE TABLE csm_canonical (
                 namespace TEXT NOT NULL DEFAULT '_default',
                 id TEXT NOT NULL,
                 version INTEGER NOT NULL,
                 labels_json TEXT NOT NULL,
                 related_json TEXT NOT NULL,
                 PRIMARY KEY (namespace, id)
             );
             INSERT INTO csm_canonical (id, version, labels_json, related_json)
             SELECT id, version, labels_json, related_json FROM csm_canonical_old;
             DROP TABLE csm_canonical_old;
             CREATE INDEX idx_csm_canonical_namespace ON csm_canonical(namespace);",
        )
        .await
        .map_err(|e| MemoryError::database(format!("Failed migration v8 namespace isolation: {e}")))?;

        Ok(())
    }

    /// Internal migration method that reuses an existing connection.
    /// Used by init_schema() to avoid semaphore deadlock from nested permit acquisition.
    pub(crate) async fn apply_migrations_with_conn(
        &self,
        conn: &libsql::Connection,
        target_version: i64,
    ) -> Result<()> {
        let current = self.schema_version_with_conn(conn).await?;
        if target_version <= current {
            return Ok(());
        }

        conn.execute("BEGIN", ()).await.map_err(|e| {
            MemoryError::database(format!("Failed to begin migration transaction: {e}"))
        })?;

        for version in (current + 1)..=target_version {
            info!(version, "applying schema migration");
            if version == 2 {
                conn.execute_batch(
                    "CREATE INDEX IF NOT EXISTS idx_csm_versions_modified_at
                     ON csm_versions(modified_at);",
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v2: {e}")))?;
            }

            if version == 3
                && !self
                    .column_exists(conn, "csm_concepts", "expires_at")
                    .await?
            {
                conn.execute_batch("ALTER TABLE csm_concepts ADD COLUMN expires_at INTEGER;")
                    .await
                    .map_err(|e| MemoryError::database(format!("Failed migration v3: {e}")))?;
            }

            if version == 4 {
                conn.execute_batch(
                    "CREATE TABLE IF NOT EXISTS csm_canonical (
                        id TEXT PRIMARY KEY,
                        version INTEGER NOT NULL,
                        labels_json TEXT NOT NULL,
                        related_json TEXT NOT NULL
                    );",
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v4: {e}")))?;
            }

            if version == 5 {
                self.apply_v5_namespace_migration(conn).await?;
            }

            if version == 6
                && !self
                    .column_exists(conn, "csm_concepts", "canonical_concept_ids_json")
                    .await?
            {
                conn.execute_batch(
                    "ALTER TABLE csm_concepts ADD COLUMN canonical_concept_ids_json TEXT;",
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v6: {e}")))?;
            }

            if version == 7 {
                conn.execute_batch(
                    "CREATE TABLE IF NOT EXISTS csm_hnsw_graph (
                        id TEXT PRIMARY KEY,
                        data BLOB NOT NULL,
                        modified_at INTEGER NOT NULL
                    );",
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v7: {e}")))?;
            }

            if version == 8 {
                self.apply_v8_namespace_migration(conn).await?;
            }

            if version == 9
                && !self
                    .column_exists(conn, "csm_associations", "created_at")
                    .await?
            {
                conn.execute_batch(
                    "ALTER TABLE csm_associations ADD COLUMN created_at INTEGER NOT NULL DEFAULT 0;",
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v9: {e}")))?;
                // Update existing associations with a sensible default if they were 0
                let now = crate::singularity::unix_now_secs();
                conn.execute(
                    "UPDATE csm_associations SET created_at = ?1 WHERE created_at = 0",
                    libsql::params![now],
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v9 update: {e}")))?;
            }

            if version == 10 {
                conn.execute_batch(
                    "CREATE TABLE IF NOT EXISTS csm_absences (
                        id TEXT PRIMARY KEY,
                        query TEXT NOT NULL,
                        normalized_query TEXT NOT NULL,
                        attempt_count INTEGER NOT NULL,
                        last_threshold REAL NOT NULL,
                        best_score_ever REAL,
                        first_seen TEXT NOT NULL,
                        last_seen TEXT NOT NULL
                    );
                    CREATE INDEX IF NOT EXISTS idx_csm_absences_normalized ON csm_absences(normalized_query);
                    CREATE INDEX IF NOT EXISTS idx_csm_absences_attempts ON csm_absences(attempt_count);",
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v10: {e}")))?;
            }

            // ADR-0093: namespace revision for derived ANN snapshot validation
            if version == 11 {
                conn.execute_batch(
                    "CREATE TABLE IF NOT EXISTS csm_namespace_meta (
                        namespace TEXT PRIMARY KEY,
                        revision INTEGER NOT NULL DEFAULT 0
                    );",
                )
                .await
                .map_err(|e| MemoryError::database(format!("Failed migration v11: {e}")))?;
            }

            conn.execute(
                "INSERT INTO csm_schema_version(version) VALUES (?1)",
                libsql::params![version],
            )
            .await
            .map_err(|e| MemoryError::database(format!("Failed to record schema version: {e}")))?;
        }

        conn.execute("COMMIT", ())
            .await
            .map_err(|e| MemoryError::database(format!("Failed to commit migrations: {e}")))?;

        Ok(())
    }

    /// Internal schema version query that reuses an existing connection.
    async fn schema_version_with_conn(&self, conn: &libsql::Connection) -> Result<i64> {
        let mut rows = conn
            .query(
                "SELECT COALESCE(MAX(version), 0) FROM csm_schema_version",
                (),
            )
            .await
            .map_err(|e| MemoryError::database(format!("Failed to get schema version: {e}")))?;

        if let Some(row) = rows.next().await.map_err(|e| {
            MemoryError::database(format!("Failed to fetch schema version row: {e}"))
        })? {
            let version: i64 = row.get(0).map_err(|e| {
                MemoryError::database(format!("Failed to parse schema version: {e}"))
            })?;
            Ok(version)
        } else {
            Ok(0)
        }
    }
}