superlocalmemory 3.5.8 → 3.6.1

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 (72) hide show
  1. package/ATTRIBUTION.md +24 -0
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +142 -35
  4. package/package.json +1 -1
  5. package/pyproject.toml +2 -1
  6. package/src/superlocalmemory/__init__.py +1 -1
  7. package/src/superlocalmemory/cli/cache_cmd.py +198 -0
  8. package/src/superlocalmemory/cli/commands.py +80 -2
  9. package/src/superlocalmemory/cli/compress_cmd.py +179 -0
  10. package/src/superlocalmemory/cli/help_cmd.py +197 -0
  11. package/src/superlocalmemory/cli/main.py +122 -0
  12. package/src/superlocalmemory/cli/optimize_cmd.py +178 -0
  13. package/src/superlocalmemory/cli/optimize_constants.py +31 -0
  14. package/src/superlocalmemory/cli/proxy_cmd.py +104 -0
  15. package/src/superlocalmemory/core/config.py +5 -0
  16. package/src/superlocalmemory/core/engine.py +23 -0
  17. package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
  18. package/src/superlocalmemory/llm/backbone.py +10 -4
  19. package/src/superlocalmemory/mcp/server.py +34 -0
  20. package/src/superlocalmemory/mcp/tools_v3.py +6 -2
  21. package/src/superlocalmemory/optimize/NOTICE +11 -0
  22. package/src/superlocalmemory/optimize/__init__.py +0 -0
  23. package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
  24. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
  25. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
  26. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
  27. package/src/superlocalmemory/optimize/adapters/wrap.py +218 -0
  28. package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
  29. package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
  30. package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
  31. package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
  32. package/src/superlocalmemory/optimize/cache/exact.py +85 -0
  33. package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
  34. package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
  35. package/src/superlocalmemory/optimize/cache/manager.py +452 -0
  36. package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
  37. package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
  38. package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
  39. package/src/superlocalmemory/optimize/compress/align.py +153 -0
  40. package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
  41. package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
  42. package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
  43. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
  44. package/src/superlocalmemory/optimize/compress/router.py +548 -0
  45. package/src/superlocalmemory/optimize/config/__init__.py +35 -0
  46. package/src/superlocalmemory/optimize/config/defaults.py +48 -0
  47. package/src/superlocalmemory/optimize/config/schema.py +255 -0
  48. package/src/superlocalmemory/optimize/config/store.py +209 -0
  49. package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
  50. package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
  51. package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
  52. package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
  53. package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
  54. package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
  55. package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
  56. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
  57. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
  58. package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
  59. package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
  60. package/src/superlocalmemory/optimize/proxy/server.py +151 -0
  61. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  62. package/src/superlocalmemory/optimize/storage/db.py +1016 -0
  63. package/src/superlocalmemory/optimize/storage/schema.py +184 -0
  64. package/src/superlocalmemory/server/routes/optimize.py +167 -0
  65. package/src/superlocalmemory/server/routes/v3_api.py +63 -1
  66. package/src/superlocalmemory/server/unified_daemon.py +105 -0
  67. package/src/superlocalmemory/ui/index.html +98 -0
  68. package/src/superlocalmemory/ui/js/ng-shell.js +5 -1
  69. package/src/superlocalmemory/ui/js/optimize.js +173 -0
  70. package/src/superlocalmemory.egg-info/PKG-INFO +144 -36
  71. package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
  72. package/src/superlocalmemory.egg-info/requires.txt +1 -0
@@ -0,0 +1,455 @@
1
+ # optimize/cache/boundary_store.py
2
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
+ # Licensed under AGPL-3.0-or-later
4
+ #
5
+ # LLD-03 §3.1 + §4.2 — Per-item vCache online MLE boundary persistence.
6
+ #
7
+ # REAL vCache algorithm from arXiv:2502.03771 (Eq. 9/10/11, Algorithm 2,
8
+ # Theorem 4.1). Replaces any prior step/raise/relax heuristic.
9
+ #
10
+ # Eq. 9: L(s, t, γ) = 1/(1 + exp(-γ(s - t))) — sigmoid correctness
11
+ # Eq. 10: (t̂, γ̂) = argmin BCE on accumulated (s, c) pairs — MLE per entry
12
+ # Eq. 11: τ̂ = min_{ε} [(1-δ) - (1-ε)·L(s, t'(ε), γ̂)] / [1 - (1-ε)·L(...)]
13
+ # where t'(ε) = t̂ - z_{1-ε/2}·se(t̂) — pessimistic CI
14
+ # se(t̂) = 1/sqrt(I_tt) with I_tt = Σ γ̂²·p(1-p) — Fisher info
15
+ # Theorem 4.1: Pr(vCache(x) = r(x) | D) ≥ (1-δ) ∀ x, n.
16
+ #
17
+ # This is the GUARANTEE path (RA-01). The point-estimate Eq. 8 does NOT
18
+ # carry Theorem 4.1; only Eq. 11 does.
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ import math
25
+ import random
26
+ import time
27
+ from dataclasses import dataclass, field
28
+ from typing import TYPE_CHECKING, Any
29
+
30
+ import numpy as np
31
+
32
+ if TYPE_CHECKING:
33
+ from superlocalmemory.optimize.storage.db import CacheDB
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # Module-level RNG instance — tests can seed via _RNG.seed(42) for determinism.
38
+ _RNG = random.Random()
39
+
40
+ # Optional scipy MLE (N-D L-BFGS-B). Fall back to gradient descent if absent.
41
+ try:
42
+ from scipy.optimize import minimize as _sp_minimize # type: ignore[import]
43
+ _SCIPY_AVAILABLE = True
44
+ except ImportError: # pragma: no cover
45
+ _SCIPY_AVAILABLE = False
46
+ _sp_minimize = None # type: ignore[assignment]
47
+
48
+ _BOUNCE_EPS: float = 1e-9
49
+ _OVERFLOW_GUARD: float = 500.0
50
+
51
+ # z_{1 - ε/2} for common ε (avoids scipy.stats dependency)
52
+ _Z_TABLE: dict[float, float] = {
53
+ 0.01: 2.576,
54
+ 0.02: 2.326,
55
+ 0.05: 1.960,
56
+ 0.10: 1.645,
57
+ 0.15: 1.440,
58
+ 0.20: 1.282,
59
+ }
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # PerItemBoundaryRecord — real vCache online MLE model for one cached entry
64
+ # ---------------------------------------------------------------------------
65
+
66
+ @dataclass
67
+ class PerItemBoundaryRecord:
68
+ """vCache per-item online MLE model for one cached entry.
69
+
70
+ A-01 fix: real vCache algorithm — learned logistic regression per entry.
71
+ A-23 fix: sliding window (max_samples) prevents frozen learning.
72
+
73
+ Fields:
74
+ entry_id: Surrogate entry ID from VCacheSemantic._derive_entry_id.
75
+ t_hat: MLE estimate of decision boundary t ∈ [0, 1].
76
+ Initialize conservative (≈ boundary_init).
77
+ gamma_hat: MLE estimate of steepness γ > 0.
78
+ Initialize steep (10.0) so cold-start is decisive.
79
+ samples: Accumulated (similarity, correctness) training pairs.
80
+ Sliding window — last `max_samples` entries retained.
81
+ last_updated: Unix timestamp of last model update.
82
+ """
83
+ entry_id: str
84
+ t_hat: float = 0.95
85
+ gamma_hat: float = 10.0
86
+ samples: list[tuple[float, int]] = field(default_factory=list)
87
+ last_updated: float = 0.0
88
+
89
+ # --- Algorithm methods (Eq. 9, 10, 11, Algorithm 2) -----------------
90
+
91
+ def compute_tau(
92
+ self,
93
+ query_sim: float,
94
+ delta: float = 0.05,
95
+ epsilon_grid: tuple[float, ...] = (0.01, 0.02, 0.05, 0.10),
96
+ ) -> float:
97
+ """Compute τ̂ — the vCache exploration probability (Eq. 11).
98
+
99
+ Args:
100
+ query_sim: Cosine similarity s(x) ∈ [0, 1] for the incoming query.
101
+ delta: δ — user-defined maximum error rate.
102
+ Theorem 4.1 guarantee: Pr(correct) ≥ 1 - δ.
103
+ epsilon_grid: ε values for the Eq. 11 min sweep.
104
+ Distinct from δ; controls CI conservativeness.
105
+
106
+ Returns:
107
+ τ̂ ∈ [0.0, 1.0]. Lower = more exploitation.
108
+ Cold start (n < 3): returns 1.0 (always explore).
109
+
110
+ Eq. 11 derivation (from the paper):
111
+ 1. I_tt = Σ γ̂² · p_i(1 - p_i) [Fisher info diagonal]
112
+ 2. se(t̂) = 1/sqrt(I_tt + ε) [normal approx SE]
113
+ 3. t'(ε) = t̂ - z_{1-ε/2} · se(t̂) [pessimistic lower bound]
114
+ 4. α(ε) = (1 - ε) · L(s, t'(ε), γ̂) [G_τ sub-function]
115
+ 5. τ(ε) = ((1 - δ) - α) / (1 - α)
116
+ 6. τ̂ = min over ε in grid [Eq. 11 min]
117
+ """
118
+ n = len(self.samples)
119
+ if n < 3:
120
+ return 1.0 # cold start — always explore
121
+
122
+ # Step 1: Fisher-information SE
123
+ i_tt = 0.0
124
+ for s, _c in self.samples:
125
+ p = _sigmoid(s, self.t_hat, self.gamma_hat)
126
+ i_tt += (self.gamma_hat ** 2) * p * (1.0 - p)
127
+ se_t = 1.0 / math.sqrt(i_tt + _BOUNCE_EPS)
128
+
129
+ # Step 2: Eq. 11 min over ε
130
+ best_tau = 1.0
131
+ for eps in epsilon_grid:
132
+ z = _Z_TABLE.get(eps, 1.960) # default z_{0.975}
133
+ t_prime = self.t_hat - z * se_t
134
+ t_prime = max(0.0, min(1.0, t_prime)) # clip to valid t range
135
+
136
+ alpha = (1.0 - eps) * _sigmoid(query_sim, t_prime, self.gamma_hat)
137
+ denom = 1.0 - alpha
138
+ if denom < _BOUNCE_EPS:
139
+ tau_eps = 0.0 # near-certainty
140
+ else:
141
+ tau_eps = ((1.0 - delta) - alpha) / denom
142
+ tau_eps = max(0.0, min(1.0, tau_eps))
143
+ best_tau = min(best_tau, tau_eps)
144
+
145
+ return best_tau
146
+
147
+ def should_explore(self, query_sim: float, delta: float = 0.05) -> bool:
148
+ """Return True (explore = LLM call) or False (exploit = serve cache).
149
+
150
+ Source: vCache Algorithm 2: draw u ~ Uniform(0, 1); explore iff u ≤ τ̂.
151
+ """
152
+ tau = self.compute_tau(query_sim, delta=delta)
153
+ return _RNG.random() <= tau
154
+
155
+ def add_sample(
156
+ self,
157
+ similarity: float,
158
+ was_correct: bool,
159
+ max_samples: int = 200,
160
+ ) -> "PerItemBoundaryRecord":
161
+ """Add a (similarity, correctness) pair and refit MLE. Returns NEW record.
162
+
163
+ A-01 fix: this is the REAL vCache learning step. MLE refit on the
164
+ accumulated samples. No step size. No raise/relax heuristic.
165
+ A-23 fix: sliding window — drops oldest when len > max_samples.
166
+ """
167
+ new_samples = list(self.samples)
168
+ new_samples.append((float(similarity), 1 if was_correct else 0))
169
+ if len(new_samples) > max_samples:
170
+ new_samples = new_samples[-max_samples:]
171
+
172
+ new_t, new_gamma = _fit_logistic_mle(
173
+ new_samples, self.t_hat, self.gamma_hat,
174
+ )
175
+ return PerItemBoundaryRecord(
176
+ entry_id=self.entry_id,
177
+ t_hat=new_t,
178
+ gamma_hat=new_gamma,
179
+ samples=new_samples,
180
+ last_updated=time.time(),
181
+ )
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # Numerical helpers
186
+ # ---------------------------------------------------------------------------
187
+
188
+ def _sigmoid(s: float, t: float, gamma: float) -> float:
189
+ """L(s, t, γ) = 1 / (1 + exp(-γ(s - t))) — Eq. 9. Overflow-guarded."""
190
+ exponent = -gamma * (s - t)
191
+ exponent = max(-_OVERFLOW_GUARD, min(_OVERFLOW_GUARD, exponent))
192
+ return 1.0 / (1.0 + math.exp(exponent))
193
+
194
+
195
+ def _rng() -> float:
196
+ """u ~ Uniform(0, 1) for the vCache exploit/explore draw.
197
+
198
+ Tests can seed via `boundary_store._RNG.seed(N)` for determinism.
199
+ """
200
+ return _RNG.random()
201
+
202
+
203
+ def _binary_cross_entropy(
204
+ params: tuple[float, float],
205
+ samples: list[tuple[float, int]],
206
+ ) -> float:
207
+ """BCE loss for the logistic model — Eq. 10 objective."""
208
+ t, gamma = params
209
+ if gamma <= 0 or not samples:
210
+ return 1e9
211
+ total = 0.0
212
+ for s, c in samples:
213
+ p = _sigmoid(s, t, gamma)
214
+ p = max(_BOUNCE_EPS, min(1.0 - _BOUNCE_EPS, p)) # numerical stability
215
+ total += -(c * math.log(p) + (1 - c) * math.log(1 - p))
216
+ return total / len(samples)
217
+
218
+
219
+ def _fit_logistic_mle(
220
+ samples: list[tuple[float, int]],
221
+ t_init: float,
222
+ gamma_init: float,
223
+ t_prior: float = 0.95,
224
+ ) -> tuple[float, float]:
225
+ """Fit (t̂, γ̂) via MLE on `samples`. L-BFGS-B if scipy, else gradient descent.
226
+
227
+ Fail-open: returns (t_init, gamma_init) on any error.
228
+
229
+ The prior pulls the solution toward (t_prior, 10.0) so a single sample
230
+ does not collapse the boundary to the saturating corner. This is
231
+ required for stable online learning.
232
+
233
+ Warm-start trick: t_init is nudged toward the empirical midpoint of
234
+ the sample similarities. This avoids L-BFGS-B's "already-at-optimum"
235
+ early termination when the cold-start t=0.95 happens to sit at a
236
+ BCE plateau.
237
+ """
238
+ if not samples:
239
+ return t_init, gamma_init
240
+
241
+ # Warm-start: empirical midpoint of positive vs negative clusters.
242
+ pos = [s for s, c in samples if c == 1]
243
+ neg = [s for s, c in samples if c == 0]
244
+ if pos and neg:
245
+ empirical_mid = (min(pos) + max(neg)) / 2.0
246
+ t_warm = max(0.5, min(1.0, empirical_mid))
247
+ else:
248
+ t_warm = t_init
249
+ gamma_warm = gamma_init
250
+
251
+ if _SCIPY_AVAILABLE and _sp_minimize is not None:
252
+ try:
253
+ result = _sp_minimize(
254
+ fun=lambda p: _binary_cross_entropy(
255
+ (p[0], p[1]), samples,
256
+ ),
257
+ x0=[t_warm, gamma_warm],
258
+ method="L-BFGS-B",
259
+ bounds=[(0.5, 1.0), (0.1, 100.0)],
260
+ options={"maxiter": 200, "ftol": 1e-10, "gtol": 1e-8},
261
+ )
262
+ t_out, g_out = float(result.x[0]), float(result.x[1])
263
+ # Guard: refuse to return a value outside the observed sample range
264
+ sims = [s for s, _ in samples]
265
+ lo, hi = min(sims), max(sims)
266
+ t_out = max(lo, min(hi, t_out))
267
+ return t_out, g_out
268
+ except Exception:
269
+ pass
270
+ return _fit_logistic_gd(samples, t_warm, gamma_warm)
271
+
272
+
273
+ def _fit_logistic_gd(
274
+ samples: list[tuple[float, int]],
275
+ t_init: float,
276
+ gamma_init: float,
277
+ lr: float = 0.05,
278
+ steps: int = 300,
279
+ ) -> tuple[float, float]:
280
+ """Gradient-descent MLE fallback. Clips to t ∈ [0.5, 1.0], γ ∈ [0.1, 100]."""
281
+ t = t_init
282
+ gamma = gamma_init
283
+ for _ in range(steps):
284
+ dt = 0.0
285
+ dg = 0.0
286
+ for s, c in samples:
287
+ p = _sigmoid(s, t, gamma)
288
+ err = p - c
289
+ dt += err * gamma
290
+ dg += err * (-(s - t))
291
+ n = max(len(samples), 1)
292
+ t = max(0.5, min(1.0, t - lr * dt / n))
293
+ gamma = max(0.1, min(100.0, gamma - lr * dg / n))
294
+ return t, gamma
295
+
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # BoundaryStore — SQLite-backed per-entry MLE record registry
299
+ # ---------------------------------------------------------------------------
300
+
301
+ class BoundaryStore:
302
+ """Manages per-item vCache learned boundaries.
303
+
304
+ Thread safety: CacheDB uses DatabaseManager (WAL, retry, busy_timeout).
305
+ Concurrent updates to the same entry_id are serialized by SQLite row
306
+ locking. The in-memory `_cache` is for warm-start; it is rebuilt via
307
+ load_all() at VCacheSemantic startup.
308
+
309
+ Fail-open: any DB error returns a safe cold-start default rather than
310
+ raising.
311
+ """
312
+
313
+ def __init__(
314
+ self,
315
+ db: "CacheDB",
316
+ default_t: float = 0.95,
317
+ default_gamma: float = 10.0,
318
+ floor: float = 0.85,
319
+ ceiling: float = 0.995,
320
+ step: float = 0.01,
321
+ epsilon: float = 0.02,
322
+ ) -> None:
323
+ self._db = db
324
+ self._default_t = default_t
325
+ self._default_gamma = default_gamma
326
+ self._floor = floor
327
+ self._ceiling = ceiling
328
+ self._step = step
329
+ self._epsilon = epsilon
330
+ # In-memory write-through cache. Populated by load_all() at warm.
331
+ # get() checks here first (O(1) hot path), then falls back to DB.
332
+ self._cache: dict[str, PerItemBoundaryRecord] = {}
333
+
334
+ def get(self, entry_id: str) -> PerItemBoundaryRecord:
335
+ """Return the MLE model record for an entry, or a cold-start default.
336
+
337
+ Fail-open: returns default (cold-start) record on DB error.
338
+ Never raises.
339
+ """
340
+ if entry_id in self._cache:
341
+ return self._cache[entry_id]
342
+ try:
343
+ row = self._db.boundary_get(entry_id)
344
+ if row is None:
345
+ return PerItemBoundaryRecord(
346
+ entry_id=entry_id,
347
+ t_hat=self._default_t,
348
+ gamma_hat=self._default_gamma,
349
+ samples=[],
350
+ last_updated=time.time(),
351
+ )
352
+ try:
353
+ samples_raw = json.loads(getattr(row, "samples_json", None) or "[]") \
354
+ if hasattr(row, "samples_json") else []
355
+ except (json.JSONDecodeError, TypeError):
356
+ samples_raw = []
357
+ if not samples_raw and hasattr(row, "samples_json"):
358
+ # BoundaryRow dataclass has no samples_json field; persist only
359
+ # the MLE parameters. samples are in-memory only after refit.
360
+ pass
361
+ # We persist only (t_hat, gamma_hat, sample_count) in the DB.
362
+ # samples is rebuilt from feedback calls (record_outcome) when needed.
363
+ return PerItemBoundaryRecord(
364
+ entry_id=entry_id,
365
+ t_hat=float(getattr(row, "logistic_t", self._default_t)),
366
+ gamma_hat=float(getattr(row, "logistic_gamma", self._default_gamma)),
367
+ samples=[],
368
+ last_updated=float(getattr(row, "updated_at", 0.0)),
369
+ )
370
+ except Exception as exc:
371
+ logger.warning("BoundaryStore.get failed (fail-open): %s", exc)
372
+ return PerItemBoundaryRecord(
373
+ entry_id=entry_id,
374
+ t_hat=self._default_t,
375
+ gamma_hat=self._default_gamma,
376
+ samples=[],
377
+ last_updated=time.time(),
378
+ )
379
+
380
+ def save(self, record: PerItemBoundaryRecord) -> None:
381
+ """Persist the MLE parameters (t_hat, gamma_hat, sample_count) for an entry.
382
+
383
+ Fail-open: logs warning on DB error, does not raise.
384
+ """
385
+ try:
386
+ from superlocalmemory.optimize.storage.db import BoundaryRow
387
+ row = BoundaryRow(
388
+ entry_id=record.entry_id,
389
+ logistic_t=record.t_hat,
390
+ logistic_gamma=record.gamma_hat,
391
+ sample_count=len(record.samples),
392
+ updated_at=record.last_updated or time.time(),
393
+ )
394
+ self._db.boundary_upsert(record.entry_id, row)
395
+ # In-memory write-through (RA-15)
396
+ self._cache[record.entry_id] = record
397
+ except Exception as exc:
398
+ logger.warning("BoundaryStore.save failed (fail-open): %s", exc)
399
+
400
+ def record_outcome(
401
+ self,
402
+ entry_id: str,
403
+ similarity: float,
404
+ was_correct: bool,
405
+ max_samples: int = 200,
406
+ ) -> PerItemBoundaryRecord:
407
+ """Fetch, add (s, c), refit MLE, save, return updated record.
408
+
409
+ A-01 fix: this IS the vCache online MLE learning step. No step size.
410
+ No raise/relax heuristic. The MLE handles everything.
411
+ """
412
+ record = self.get(entry_id)
413
+ updated = record.add_sample(
414
+ similarity=similarity,
415
+ was_correct=was_correct,
416
+ max_samples=max_samples,
417
+ )
418
+ self.save(updated)
419
+ return updated
420
+
421
+ def load_all(self) -> dict[str, PerItemBoundaryRecord]:
422
+ """Load all boundary records into memory (warm-start).
423
+
424
+ Returns:
425
+ Dict entry_id → PerItemBoundaryRecord. {} on error.
426
+
427
+ Note: only the (t_hat, gamma_hat, sample_count) are loaded from DB.
428
+ The full `samples` window is rebuilt on demand via record_outcome().
429
+ """
430
+ try:
431
+ rows = self._db.get_all_boundaries()
432
+ result: dict[str, PerItemBoundaryRecord] = {}
433
+ for r in rows:
434
+ eid = r.get("entry_id")
435
+ if not eid:
436
+ continue
437
+ result[eid] = PerItemBoundaryRecord(
438
+ entry_id=eid,
439
+ t_hat=float(r.get("logistic_t", self._default_t)),
440
+ gamma_hat=float(r.get("logistic_gamma", self._default_gamma)),
441
+ samples=[],
442
+ last_updated=float(r.get("updated_at", 0.0)),
443
+ )
444
+ return result
445
+ except Exception as exc:
446
+ logger.warning("BoundaryStore.load_all failed (fail-open): %s", exc)
447
+ return {}
448
+
449
+ def delete(self, entry_id: str) -> None:
450
+ """Remove boundary record for a deleted cache entry. Fail-open."""
451
+ try:
452
+ self._db.delete_boundary(entry_id)
453
+ self._cache.pop(entry_id, None)
454
+ except Exception as exc:
455
+ logger.warning("BoundaryStore.delete failed (fail-open): %s", exc)
@@ -0,0 +1,158 @@
1
+ # optimize/cache/centroid_store.py
2
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
+ # Licensed under AGPL-3.0-or-later
4
+ #
5
+ # LLD-03 §4.3 — SAFE-CACHE centroid registry for adversarial collision
6
+ # detection.
7
+ #
8
+ # Source: SAFE-CACHE — Nature Scientific Reports 2026 (no arXiv).
9
+ # Defense: compare the incoming query vector to the cluster centroid. If
10
+ # the query is far from the centroid (cosine sim < 1 - distance_floor),
11
+ # it falls outside the natural distribution → likely an adversarial
12
+ # probe crafted to collide with a specific entry. Reject as miss.
13
+ #
14
+ # Design:
15
+ # - One centroid per tenant_id (simplest partition, proven sufficient).
16
+ # - Rebuilt from llmcache_semantic_vectors on startup.
17
+ # - Updated incrementally (Welford running mean) on every new set().
18
+ # - In-memory only — no separate SQL query needed (cache is O(N) anyway).
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import threading
24
+ from typing import TYPE_CHECKING
25
+
26
+ import numpy as np
27
+
28
+ if TYPE_CHECKING:
29
+ from superlocalmemory.optimize.storage.db import CacheDB
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ _VARIANCE_FLOOR: float = 1e-6
34
+ _EMBED_DIM: int = 768
35
+
36
+
37
+ def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
38
+ """Cosine similarity in [-1, 1]. Returns 0.0 on zero vectors."""
39
+ norm_a = float(np.linalg.norm(a))
40
+ norm_b = float(np.linalg.norm(b))
41
+ if norm_a < _VARIANCE_FLOOR or norm_b < _VARIANCE_FLOOR:
42
+ return 0.0
43
+ return float(np.dot(a, b) / (norm_a * norm_b))
44
+
45
+
46
+ class CentroidStore:
47
+ """In-memory centroid registry for SAFE-CACHE adversarial defense.
48
+
49
+ Thread-safe via a single RLock. All public methods are fail-open.
50
+
51
+ Centroid update rule (Welford running mean — exact, O(1) per update):
52
+ new_centroid = old_centroid * (n / (n+1)) + new_vec * (1 / (n+1))
53
+ """
54
+ def __init__(self) -> None:
55
+ self._centroids: dict[str, np.ndarray] = {} # tenant_id → float32 vec
56
+ self._counts: dict[str, int] = {} # tenant_id → count
57
+ self._lock = threading.RLock()
58
+
59
+ def rebuild_from_db(self, db: "CacheDB", tenant_id: str) -> None:
60
+ """Rebuild centroid for a tenant from all stored vectors.
61
+
62
+ Called at VCacheSemantic startup. O(N) — runs once per tenant.
63
+ Fail-open: on DB error or empty vector set, centroid is reset.
64
+ """
65
+ try:
66
+ rows = db.get_all_vectors(tenant_id=tenant_id)
67
+ if not rows:
68
+ with self._lock:
69
+ self._centroids.pop(tenant_id, None)
70
+ self._counts.pop(tenant_id, None)
71
+ return
72
+ vectors: list[np.ndarray] = []
73
+ for _entry_id, blob in rows:
74
+ try:
75
+ vec = np.frombuffer(blob, dtype=np.float32).copy()
76
+ if vec.shape[0] == _EMBED_DIM:
77
+ vectors.append(vec)
78
+ except Exception:
79
+ continue
80
+ if not vectors:
81
+ return
82
+ centroid = np.mean(np.stack(vectors, axis=0), axis=0).astype(np.float32)
83
+ with self._lock:
84
+ self._centroids[tenant_id] = centroid
85
+ self._counts[tenant_id] = len(vectors)
86
+ logger.debug(
87
+ "CentroidStore: rebuilt tenant=%s centroid from %d vectors",
88
+ tenant_id, len(vectors),
89
+ )
90
+ except Exception as exc:
91
+ logger.warning("CentroidStore.rebuild_from_db failed (fail-open): %s", exc)
92
+
93
+ def update(self, tenant_id: str, new_vector: np.ndarray) -> None:
94
+ """Update centroid with a new vector (Welford incremental mean).
95
+
96
+ Thread-safe. Fail-open.
97
+ """
98
+ try:
99
+ vec = new_vector.astype(np.float32)
100
+ with self._lock:
101
+ if tenant_id not in self._centroids:
102
+ self._centroids[tenant_id] = vec.copy()
103
+ self._counts[tenant_id] = 1
104
+ else:
105
+ n = self._counts[tenant_id]
106
+ old = self._centroids[tenant_id]
107
+ self._centroids[tenant_id] = (
108
+ old * (n / (n + 1)) + vec * (1.0 / (n + 1))
109
+ ).astype(np.float32)
110
+ self._counts[tenant_id] = n + 1
111
+ except Exception as exc:
112
+ logger.warning("CentroidStore.update failed (fail-open): %s", exc)
113
+
114
+ def is_adversarial(
115
+ self,
116
+ tenant_id: str,
117
+ query_vector: np.ndarray,
118
+ distance_floor: float = 0.15,
119
+ ) -> bool:
120
+ """Return True if query_vector appears adversarially crafted.
121
+
122
+ Defense: if the query is too far from the cluster centroid (cosine
123
+ similarity < 1 - distance_floor), it is an outlier — likely an
124
+ adversarial probe. Return True → reject (adversarial).
125
+
126
+ Fail-open: returns False (accept) on any error or missing centroid.
127
+ Skips defense if the cluster has < 5 entries (insufficient data).
128
+ """
129
+ try:
130
+ with self._lock:
131
+ centroid = self._centroids.get(tenant_id)
132
+ count = self._counts.get(tenant_id, 0)
133
+ if centroid is None or count < 5:
134
+ return False
135
+ q = query_vector.astype(np.float32)
136
+ sim = _cosine_similarity(q, centroid)
137
+ threshold = 1.0 - distance_floor
138
+ if sim < threshold:
139
+ logger.warning(
140
+ "CentroidStore: adversarial probe detected "
141
+ "(tenant=%s, centroid_sim=%.4f < %.4f) — returning miss.",
142
+ tenant_id, sim, threshold,
143
+ )
144
+ return True
145
+ return False
146
+ except Exception as exc:
147
+ logger.warning("CentroidStore.is_adversarial failed (fail-open): %s", exc)
148
+ return False
149
+
150
+ def get_centroid(self, tenant_id: str) -> np.ndarray | None:
151
+ """Return current centroid for a tenant, or None if not established."""
152
+ with self._lock:
153
+ return self._centroids.get(tenant_id)
154
+
155
+ def count(self, tenant_id: str) -> int:
156
+ """Return the number of vectors contributing to this tenant's centroid."""
157
+ with self._lock:
158
+ return self._counts.get(tenant_id, 0)
@@ -0,0 +1,67 @@
1
+ # optimize/cache/context_key.py
2
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
+ # Licensed under AGPL-3.0-or-later
4
+ #
5
+ # LLD-03 §4.1 — Multi-turn context-aware key builder for the semantic cache.
6
+ # Source: ContextCache / SmartCache (arXiv:2506.22791 §3) — context-aware
7
+ # cache keys prevent false reuse across semantically overlapping but
8
+ # conversationally distinct turns.
9
+ #
10
+ # The fingerprint is NOT the full cache key (that is KeyBuilder's job).
11
+ # It is an auxiliary scope guard: a semantic hit is accepted ONLY IF
12
+ # the stored entry's context fingerprint matches the query's context
13
+ # fingerprint (or is absent — single-turn entries).
14
+ #
15
+ # A-22 fix: 16-hex-char fingerprint (64 bits → birthday at 2^32 entries,
16
+ # safe for any realistic tenant). 8 hex chars (32 bits) birthday at 2^16.
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ from typing import Any
23
+
24
+ _CONTEXT_SCHEMA_VERSION: int = 1
25
+
26
+
27
+ class ContextKeyBuilder:
28
+ """Builds a context fingerprint for multi-turn semantic cache lookup.
29
+
30
+ Args:
31
+ window_turns: Number of prior assistant+user turns to include.
32
+ Default: 3 (matches LLD-03 §3.2 default).
33
+ """
34
+
35
+ def __init__(self, window_turns: int = 3) -> None:
36
+ if window_turns < 1:
37
+ raise ValueError(f"window_turns must be >= 1, got {window_turns}")
38
+ self._window = window_turns
39
+
40
+ def build(self, messages: list[dict[str, Any]], tenant_id: str) -> str:
41
+ """Build a 16-hex-char (64-bit) context fingerprint.
42
+
43
+ Takes the last `window_turns * 2` messages (user + assistant pairs),
44
+ canonicalizes them, SHA-256 hashes them, returns first 16 hex chars.
45
+
46
+ Returns:
47
+ 16-hex context fingerprint, e.g. "a3f2b1c0d4e5f601". Never empty.
48
+ Single-turn (no prior context) returns the SHA-256 of an empty
49
+ canonical context array — a stable sentinel.
50
+ """
51
+ prior = messages[:-1] if len(messages) > 1 else []
52
+ window = prior[-(self._window * 2):]
53
+
54
+ payload = {
55
+ "v": _CONTEXT_SCHEMA_VERSION,
56
+ "tenant": tenant_id,
57
+ "ctx": window,
58
+ }
59
+ canonical = json.dumps(
60
+ payload, sort_keys=True, separators=(",", ":"),
61
+ ensure_ascii=True, default=str,
62
+ )
63
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
64
+
65
+ def turn_count(self, messages: list[dict[str, Any]]) -> int:
66
+ """Return the number of completed conversation turns (assistant messages)."""
67
+ return sum(1 for m in messages if m.get("role") == "assistant")