superlocalmemory 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +106 -71
  3. package/package.json +1 -2
  4. package/pyproject.toml +16 -1
  5. package/src/superlocalmemory/cli/commands.py +309 -0
  6. package/src/superlocalmemory/cli/main.py +44 -0
  7. package/src/superlocalmemory/core/config.py +282 -11
  8. package/src/superlocalmemory/core/consolidation_engine.py +37 -0
  9. package/src/superlocalmemory/core/engine.py +21 -0
  10. package/src/superlocalmemory/core/engine_wiring.py +58 -8
  11. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +374 -0
  12. package/src/superlocalmemory/dynamics/eap_scheduler.py +276 -0
  13. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +171 -0
  14. package/src/superlocalmemory/encoding/cognitive_consolidator.py +804 -0
  15. package/src/superlocalmemory/hooks/auto_invoker.py +46 -8
  16. package/src/superlocalmemory/hooks/auto_parameterize.py +147 -0
  17. package/src/superlocalmemory/infra/heartbeat_monitor.py +140 -0
  18. package/src/superlocalmemory/infra/pid_manager.py +193 -0
  19. package/src/superlocalmemory/infra/process_reaper.py +572 -0
  20. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +115 -0
  21. package/src/superlocalmemory/learning/forgetting_scheduler.py +263 -0
  22. package/src/superlocalmemory/learning/quantization_scheduler.py +320 -0
  23. package/src/superlocalmemory/math/ebbinghaus.py +309 -0
  24. package/src/superlocalmemory/math/fisher_quantized.py +251 -0
  25. package/src/superlocalmemory/math/hopfield.py +279 -0
  26. package/src/superlocalmemory/math/polar_quant.py +379 -0
  27. package/src/superlocalmemory/math/qjl.py +115 -0
  28. package/src/superlocalmemory/mcp/server.py +2 -0
  29. package/src/superlocalmemory/mcp/tools_v3.py +10 -0
  30. package/src/superlocalmemory/mcp/tools_v33.py +351 -0
  31. package/src/superlocalmemory/parameterization/__init__.py +47 -0
  32. package/src/superlocalmemory/parameterization/pattern_extractor.py +534 -0
  33. package/src/superlocalmemory/parameterization/pii_filter.py +106 -0
  34. package/src/superlocalmemory/parameterization/prompt_injector.py +216 -0
  35. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +275 -0
  36. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +425 -0
  37. package/src/superlocalmemory/retrieval/engine.py +21 -3
  38. package/src/superlocalmemory/retrieval/forgetting_filter.py +145 -0
  39. package/src/superlocalmemory/retrieval/hopfield_channel.py +335 -0
  40. package/src/superlocalmemory/retrieval/quantization_aware_search.py +133 -0
  41. package/src/superlocalmemory/retrieval/spreading_activation.py +1 -1
  42. package/src/superlocalmemory/retrieval/strategy.py +16 -6
  43. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  44. package/src/superlocalmemory/server/routes/agents.py +68 -8
  45. package/src/superlocalmemory/server/routes/learning.py +18 -1
  46. package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
  47. package/src/superlocalmemory/server/routes/v3_api.py +503 -1
  48. package/src/superlocalmemory/storage/database.py +206 -0
  49. package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
  50. package/src/superlocalmemory/storage/migration_v33.py +140 -0
  51. package/src/superlocalmemory/storage/quantized_store.py +261 -0
  52. package/src/superlocalmemory/storage/schema_v32.py +137 -0
  53. package/conftest.py +0 -5
@@ -0,0 +1,261 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3
4
+
5
+ """Quantized embedding storage and retrieval.
6
+
7
+ Manages polar_embeddings and embedding_quantization_metadata tables.
8
+ Bridges PolarQuantEncoder/QJLEncoder to SQLite persistence.
9
+
10
+ HR-05: All SQL uses parameterized queries.
11
+ HR-06: BLOB columns use Python bytes, not base64.
12
+ HR-07: QJL is optional -- system works without it.
13
+
14
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
15
+ License: MIT
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from typing import TYPE_CHECKING
22
+
23
+ import numpy as np
24
+ from numpy.typing import NDArray
25
+
26
+ from superlocalmemory.core.config import QuantizationConfig
27
+ from superlocalmemory.math.polar_quant import PolarQuantEncoder, QuantizedEmbedding
28
+ from superlocalmemory.math.qjl import QJLEncoder
29
+
30
+ if TYPE_CHECKING:
31
+ from superlocalmemory.storage.database import DatabaseManager
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Quantization level names
38
+ # ---------------------------------------------------------------------------
39
+
40
+ _BW_TO_LEVEL: dict[int, str] = {
41
+ 32: "float32",
42
+ 8: "int8",
43
+ 4: "polar4",
44
+ 2: "polar2",
45
+ 0: "deleted",
46
+ }
47
+
48
+
49
+ class QuantizedEmbeddingStore:
50
+ """CRUD for quantized polar embeddings with optional QJL correction.
51
+
52
+ Uses two tables:
53
+ - polar_embeddings: stores angle indices, radius, QJL bits
54
+ - embedding_quantization_metadata: tracks quantization state per fact
55
+ """
56
+
57
+ __slots__ = ("_db", "_polar", "_qjl", "_config")
58
+
59
+ def __init__(
60
+ self,
61
+ db: DatabaseManager,
62
+ polar: PolarQuantEncoder,
63
+ qjl: QJLEncoder | None,
64
+ config: QuantizationConfig,
65
+ ) -> None:
66
+ self._db = db
67
+ self._polar = polar
68
+ self._qjl = qjl
69
+ self._config = config
70
+
71
+ # -- CRUD: polar_embeddings --------------------------------------------
72
+
73
+ def store(
74
+ self, fact_id: str, profile_id: str, qe: QuantizedEmbedding,
75
+ ) -> bool:
76
+ """UPSERT a quantized embedding into polar_embeddings.
77
+
78
+ Also updates embedding_quantization_metadata.
79
+ Returns True on success, False on error.
80
+ """
81
+ try:
82
+ # UPSERT polar embedding (HR-05: parameterized)
83
+ self._db.execute(
84
+ "INSERT INTO polar_embeddings "
85
+ "(fact_id, profile_id, radius, angle_indices, qjl_bits, bit_width, created_at) "
86
+ "VALUES (?, ?, ?, ?, ?, ?, datetime('now')) "
87
+ "ON CONFLICT(fact_id) DO UPDATE SET "
88
+ " radius = excluded.radius, "
89
+ " angle_indices = excluded.angle_indices, "
90
+ " qjl_bits = excluded.qjl_bits, "
91
+ " bit_width = excluded.bit_width",
92
+ (fact_id, profile_id, qe.radius, qe.angle_indices,
93
+ qe.qjl_bits, qe.bit_width),
94
+ )
95
+
96
+ # UPSERT quantization metadata
97
+ level = _BW_TO_LEVEL.get(qe.bit_width, "float32")
98
+ compressed_size = len(qe.angle_indices)
99
+ if qe.qjl_bits:
100
+ compressed_size += len(qe.qjl_bits)
101
+
102
+ self._db.execute(
103
+ "INSERT INTO embedding_quantization_metadata "
104
+ "(fact_id, profile_id, quantization_level, bit_width, "
105
+ " compressed_size_bytes, created_at) "
106
+ "VALUES (?, ?, ?, ?, ?, datetime('now')) "
107
+ "ON CONFLICT(fact_id) DO UPDATE SET "
108
+ " quantization_level = excluded.quantization_level, "
109
+ " bit_width = excluded.bit_width, "
110
+ " compressed_size_bytes = excluded.compressed_size_bytes",
111
+ (fact_id, profile_id, level, qe.bit_width, compressed_size),
112
+ )
113
+
114
+ return True
115
+ except Exception as exc:
116
+ logger.error("store failed for fact_id=%s: %s", fact_id, exc)
117
+ return False
118
+
119
+ def load(
120
+ self, fact_id: str, profile_id: str,
121
+ ) -> QuantizedEmbedding | None:
122
+ """Load a quantized embedding by fact_id and profile_id.
123
+
124
+ Returns None if not found.
125
+ """
126
+ try:
127
+ rows = self._db.execute(
128
+ "SELECT radius, angle_indices, qjl_bits, bit_width "
129
+ "FROM polar_embeddings "
130
+ "WHERE fact_id = ? AND profile_id = ?",
131
+ (fact_id, profile_id),
132
+ )
133
+ if not rows:
134
+ return None
135
+
136
+ row = dict(rows[0])
137
+ return QuantizedEmbedding(
138
+ fact_id=fact_id,
139
+ radius=float(row["radius"]),
140
+ angle_indices=bytes(row["angle_indices"]),
141
+ bit_width=int(row["bit_width"]),
142
+ qjl_bits=bytes(row["qjl_bits"]) if row["qjl_bits"] else None,
143
+ )
144
+ except Exception as exc:
145
+ logger.error("load failed for fact_id=%s: %s", fact_id, exc)
146
+ return None
147
+
148
+ def search(
149
+ self,
150
+ query_embedding: NDArray,
151
+ profile_id: str,
152
+ top_k: int = 50,
153
+ ) -> list[tuple[str, float]]:
154
+ """Search polar embeddings for a profile.
155
+
156
+ Pre-filters by lifecycle_zone (excludes 'forgotten').
157
+ Returns [(fact_id, similarity)] sorted descending.
158
+ """
159
+ try:
160
+ rows = self._db.execute(
161
+ "SELECT pe.fact_id, pe.radius, pe.angle_indices, "
162
+ " pe.qjl_bits, pe.bit_width "
163
+ "FROM polar_embeddings pe "
164
+ "JOIN fact_retention fr "
165
+ " ON pe.fact_id = fr.fact_id AND fr.profile_id = pe.profile_id "
166
+ "WHERE pe.profile_id = ? "
167
+ " AND fr.lifecycle_zone NOT IN ('forgotten')",
168
+ (profile_id,),
169
+ )
170
+ except Exception as exc:
171
+ logger.error("search query failed: %s", exc)
172
+ return []
173
+
174
+ if not rows:
175
+ return []
176
+
177
+ # Build QuantizedEmbedding objects and compute similarities
178
+ results: list[tuple[str, float]] = []
179
+ for row in rows:
180
+ d = dict(row)
181
+ qe = QuantizedEmbedding(
182
+ fact_id=d["fact_id"],
183
+ radius=float(d["radius"]),
184
+ angle_indices=bytes(d["angle_indices"]),
185
+ bit_width=int(d["bit_width"]),
186
+ qjl_bits=bytes(d["qjl_bits"]) if d["qjl_bits"] else None,
187
+ )
188
+
189
+ sim = self._polar.approximate_similarity(query_embedding, qe)
190
+
191
+ # QJL correction (HR-07: optional)
192
+ if qe.qjl_bits and self._qjl:
193
+ correction = self._qjl.estimate_correction(
194
+ query_embedding, qe.qjl_bits,
195
+ )
196
+ sim += correction
197
+
198
+ results.append((qe.fact_id, sim))
199
+
200
+ # Sort descending by similarity
201
+ results.sort(key=lambda x: x[1], reverse=True)
202
+ return results[:top_k]
203
+
204
+ # -- Compression helpers -----------------------------------------------
205
+
206
+ def compress_fact(
207
+ self,
208
+ fact_id: str,
209
+ profile_id: str,
210
+ original_embedding: NDArray,
211
+ target_bit_width: int,
212
+ ) -> bool:
213
+ """Quantize an embedding and store it.
214
+
215
+ For bit_width <= 4, also computes QJL residual correction
216
+ (if QJL encoder is available).
217
+
218
+ Returns True on success.
219
+ """
220
+ try:
221
+ qe = self._polar.encode(original_embedding, target_bit_width)
222
+
223
+ # QJL residual for low bit-widths (HR-07: optional)
224
+ qjl_bits: bytes | None = None
225
+ if target_bit_width <= 4 and self._qjl:
226
+ decoded = self._polar.decode(qe)
227
+ residual = original_embedding - decoded
228
+ qjl_bits = self._qjl.encode_residual(residual)
229
+
230
+ # Build final QuantizedEmbedding with fact_id and QJL bits
231
+ qe_final = QuantizedEmbedding(
232
+ fact_id=fact_id,
233
+ radius=qe.radius,
234
+ angle_indices=qe.angle_indices,
235
+ bit_width=qe.bit_width,
236
+ qjl_bits=qjl_bits,
237
+ )
238
+
239
+ return self.store(fact_id, profile_id, qe_final)
240
+ except Exception as exc:
241
+ logger.error(
242
+ "compress_fact failed for fact_id=%s: %s", fact_id, exc,
243
+ )
244
+ return False
245
+
246
+ def batch_compress(
247
+ self,
248
+ fact_ids: list[str],
249
+ profile_id: str,
250
+ embeddings: dict[str, NDArray],
251
+ target_bit_width: int,
252
+ ) -> int:
253
+ """Compress a batch of facts. Returns count of successful compressions."""
254
+ count = 0
255
+ for fact_id in fact_ids:
256
+ if fact_id in embeddings:
257
+ if self.compress_fact(
258
+ fact_id, profile_id, embeddings[fact_id], target_bit_width,
259
+ ):
260
+ count += 1
261
+ return count
@@ -31,6 +31,9 @@ from typing import Final
31
31
  # ---------------------------------------------------------------------------
32
32
 
33
33
  V32_TABLES: Final[tuple[str, ...]] = (
34
+ "fact_retention",
35
+ "polar_embeddings",
36
+ "embedding_quantization_metadata",
34
37
  "fact_access_log",
35
38
  "fact_embeddings",
36
39
  "embedding_metadata",
@@ -40,6 +43,9 @@ V32_TABLES: Final[tuple[str, ...]] = (
40
43
  "fact_importance",
41
44
  "fact_temporal_validity",
42
45
  "core_memory_blocks",
46
+ "ccq_consolidated_blocks",
47
+ "ccq_audit_log",
48
+ "soft_prompt_templates",
43
49
  )
44
50
 
45
51
  # ---------------------------------------------------------------------------
@@ -48,6 +54,60 @@ V32_TABLES: Final[tuple[str, ...]] = (
48
54
  # ---------------------------------------------------------------------------
49
55
 
50
56
  V32_DDL: list[str] = [
57
+ # --- Phase A: Forgetting Brain (fact_retention) ---
58
+ """
59
+ CREATE TABLE IF NOT EXISTS fact_retention (
60
+ fact_id TEXT PRIMARY KEY,
61
+ profile_id TEXT NOT NULL,
62
+ retention_score REAL NOT NULL DEFAULT 1.0,
63
+ memory_strength REAL NOT NULL DEFAULT 1.0,
64
+ access_count INTEGER NOT NULL DEFAULT 0,
65
+ last_accessed_at TEXT,
66
+ last_computed_at TEXT NOT NULL DEFAULT (datetime('now')),
67
+ lifecycle_zone TEXT NOT NULL DEFAULT 'active'
68
+ CHECK (lifecycle_zone IN ('active', 'warm', 'cold', 'archive', 'forgotten')),
69
+
70
+ FOREIGN KEY (fact_id) REFERENCES atomic_facts (fact_id) ON DELETE CASCADE,
71
+ FOREIGN KEY (profile_id) REFERENCES profiles (profile_id) ON DELETE CASCADE
72
+ );
73
+ CREATE INDEX IF NOT EXISTS idx_retention_profile
74
+ ON fact_retention (profile_id, lifecycle_zone);
75
+ CREATE INDEX IF NOT EXISTS idx_retention_score
76
+ ON fact_retention (profile_id, retention_score DESC);
77
+ """,
78
+ # --- Phase B: PolarQuant Embedding Quantization ---
79
+ """
80
+ CREATE TABLE IF NOT EXISTS polar_embeddings (
81
+ fact_id TEXT PRIMARY KEY,
82
+ profile_id TEXT NOT NULL,
83
+ radius REAL NOT NULL,
84
+ angle_indices BLOB NOT NULL,
85
+ qjl_bits BLOB,
86
+ bit_width INTEGER NOT NULL DEFAULT 4,
87
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
88
+
89
+ FOREIGN KEY (fact_id) REFERENCES atomic_facts (fact_id) ON DELETE CASCADE,
90
+ FOREIGN KEY (profile_id) REFERENCES profiles (profile_id) ON DELETE CASCADE
91
+ );
92
+ CREATE INDEX IF NOT EXISTS idx_polar_profile
93
+ ON polar_embeddings (profile_id);
94
+ """,
95
+ """
96
+ CREATE TABLE IF NOT EXISTS embedding_quantization_metadata (
97
+ fact_id TEXT PRIMARY KEY,
98
+ profile_id TEXT NOT NULL,
99
+ quantization_level TEXT NOT NULL DEFAULT 'float32'
100
+ CHECK (quantization_level IN ('float32', 'int8', 'polar4', 'polar2', 'deleted')),
101
+ bit_width INTEGER NOT NULL DEFAULT 32,
102
+ compressed_size_bytes INTEGER,
103
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
104
+
105
+ FOREIGN KEY (fact_id) REFERENCES atomic_facts (fact_id) ON DELETE CASCADE,
106
+ FOREIGN KEY (profile_id) REFERENCES profiles (profile_id) ON DELETE CASCADE
107
+ );
108
+ CREATE INDEX IF NOT EXISTS idx_eqm_profile_level
109
+ ON embedding_quantization_metadata (profile_id, quantization_level);
110
+ """,
51
111
  # --- Phase 1: Vector Foundation ---
52
112
  """
53
113
  CREATE TABLE IF NOT EXISTS fact_access_log (
@@ -198,6 +258,52 @@ V32_DDL: list[str] = [
198
258
  CREATE UNIQUE INDEX IF NOT EXISTS idx_core_blocks_unique
199
259
  ON core_memory_blocks(profile_id, block_type);
200
260
  """,
261
+ # --- Phase E: CCQ Consolidated Blocks (dedicated table, many-per-profile) ---
262
+ """
263
+ CREATE TABLE IF NOT EXISTS ccq_consolidated_blocks (
264
+ block_id TEXT PRIMARY KEY,
265
+ profile_id TEXT NOT NULL,
266
+ content TEXT NOT NULL,
267
+ source_fact_ids TEXT NOT NULL DEFAULT '[]',
268
+ gist_embedding_rowid INTEGER,
269
+ char_count INTEGER NOT NULL DEFAULT 0,
270
+ compiled_by TEXT NOT NULL DEFAULT 'ccq',
271
+ cluster_id TEXT NOT NULL,
272
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
273
+
274
+ FOREIGN KEY (profile_id) REFERENCES profiles (profile_id) ON DELETE CASCADE
275
+ );
276
+ CREATE INDEX IF NOT EXISTS idx_ccq_blocks_profile
277
+ ON ccq_consolidated_blocks (profile_id);
278
+ CREATE INDEX IF NOT EXISTS idx_ccq_blocks_cluster
279
+ ON ccq_consolidated_blocks (cluster_id);
280
+ """,
281
+ # --- Phase E: CCQ Audit Log ---
282
+ """
283
+ CREATE TABLE IF NOT EXISTS ccq_audit_log (
284
+ audit_id TEXT PRIMARY KEY,
285
+ profile_id TEXT NOT NULL,
286
+ cluster_id TEXT NOT NULL,
287
+ block_id TEXT NOT NULL,
288
+ fact_ids TEXT NOT NULL DEFAULT '[]',
289
+ fact_count INTEGER NOT NULL DEFAULT 0,
290
+ gist_text TEXT NOT NULL,
291
+ extraction_mode TEXT NOT NULL DEFAULT 'rules'
292
+ CHECK (extraction_mode IN ('rules', 'llm')),
293
+ bytes_before INTEGER NOT NULL DEFAULT 0,
294
+ bytes_after INTEGER NOT NULL DEFAULT 0,
295
+ compression_ratio REAL NOT NULL DEFAULT 0.0,
296
+ shared_entities TEXT NOT NULL DEFAULT '[]',
297
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
298
+
299
+ FOREIGN KEY (profile_id) REFERENCES profiles (profile_id) ON DELETE CASCADE,
300
+ FOREIGN KEY (block_id) REFERENCES ccq_consolidated_blocks (block_id) ON DELETE CASCADE
301
+ );
302
+ CREATE INDEX IF NOT EXISTS idx_ccq_audit_profile
303
+ ON ccq_audit_log (profile_id, created_at DESC);
304
+ CREATE INDEX IF NOT EXISTS idx_ccq_audit_block
305
+ ON ccq_audit_log (block_id);
306
+ """,
201
307
  # --- Phase 4: Temporal Intelligence ---
202
308
  """
203
309
  CREATE TABLE IF NOT EXISTS fact_temporal_validity (
@@ -226,6 +332,37 @@ V32_DDL: list[str] = [
226
332
  CREATE INDEX IF NOT EXISTS idx_temporal_invalidated_by
227
333
  ON fact_temporal_validity(invalidated_by);
228
334
  """,
335
+ # --- Phase F: The Learning Brain (Memory Parameterization) ---
336
+ """
337
+ CREATE TABLE IF NOT EXISTS soft_prompt_templates (
338
+ prompt_id TEXT PRIMARY KEY,
339
+ profile_id TEXT NOT NULL,
340
+ category TEXT NOT NULL CHECK (category IN (
341
+ 'identity', 'tech_preference', 'communication_style',
342
+ 'workflow_pattern', 'project_context', 'decision_history',
343
+ 'avoidance', 'custom'
344
+ )),
345
+ content TEXT NOT NULL,
346
+ source_pattern_ids TEXT NOT NULL DEFAULT '[]',
347
+ confidence REAL NOT NULL DEFAULT 0.0,
348
+ effectiveness REAL NOT NULL DEFAULT 0.5,
349
+ token_count INTEGER NOT NULL DEFAULT 0,
350
+ retention_score REAL NOT NULL DEFAULT 1.0,
351
+ active INTEGER NOT NULL DEFAULT 1,
352
+ version INTEGER NOT NULL DEFAULT 1,
353
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
354
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
355
+
356
+ FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
357
+ );
358
+ CREATE INDEX IF NOT EXISTS idx_soft_prompt_profile
359
+ ON soft_prompt_templates(profile_id, active);
360
+ CREATE INDEX IF NOT EXISTS idx_soft_prompt_category
361
+ ON soft_prompt_templates(profile_id, category);
362
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_soft_prompt_unique_cat
363
+ ON soft_prompt_templates(profile_id, category)
364
+ WHERE active = 1;
365
+ """,
229
366
  ]
230
367
 
231
368
  # vec0 virtual table DDL — executed by VectorStore ONLY (requires extension loaded first).
package/conftest.py DELETED
@@ -1,5 +0,0 @@
1
- """Root conftest — ensures src/ is on PYTHONPATH for test discovery."""
2
- import sys
3
- from pathlib import Path
4
-
5
- sys.path.insert(0, str(Path(__file__).parent / "src"))