superlocalmemory 3.3.25 → 3.3.26
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.
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/core/config.py +2 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +77 -25
- package/src/superlocalmemory/math/ebbinghaus.py +44 -1
- package/src/superlocalmemory/math/fisher_quantized.py +8 -8
- package/src/superlocalmemory/retrieval/semantic_channel.py +54 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.26",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -259,6 +259,8 @@ class ForgettingConfig:
|
|
|
259
259
|
learning_rate: float = 1.0 # eta in spaced repetition update
|
|
260
260
|
# Coupling
|
|
261
261
|
forgetting_drift_scale: float = 0.5 # How strongly forgetting affects Langevin drift
|
|
262
|
+
# Trust-weighted forgetting (Paper 3, Section 5.5)
|
|
263
|
+
trust_kappa: float = 2.0 # Sensitivity: lambda_eff = lambda * (1 + trust_kappa * (1 - tau))
|
|
262
264
|
# Scheduler
|
|
263
265
|
scheduler_interval_minutes: int = 30 # How often to recompute retentions
|
|
264
266
|
# Immunity
|
|
@@ -202,31 +202,69 @@ class ForgettingScheduler:
|
|
|
202
202
|
- confirmation_count mapped from atomic_facts.evidence_count
|
|
203
203
|
- emotional_salience from atomic_facts.emotional_valence
|
|
204
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
|
-
|
|
205
|
+
# V3.3.26: Trust-weighted forgetting — look up trust score for
|
|
206
|
+
# the agent that created each fact. Falls back to 1.0 if trust_scores
|
|
207
|
+
# table or created_by column is unavailable.
|
|
208
|
+
trust_available = self._has_trust_tables()
|
|
209
|
+
if trust_available:
|
|
210
|
+
sql = (
|
|
211
|
+
"SELECT f.fact_id, "
|
|
212
|
+
" COALESCE(al.access_count, 0) as access_count, "
|
|
213
|
+
" COALESCE(fi.pagerank_score, 0.0) as importance, "
|
|
214
|
+
" COALESCE(f.evidence_count, 0) as confirmation_count, "
|
|
215
|
+
" f.created_at, "
|
|
216
|
+
" COALESCE(r.last_accessed_at, f.created_at) as last_accessed_at, "
|
|
217
|
+
" COALESCE(f.emotional_valence, 0.0) as emotional_salience, "
|
|
218
|
+
" COALESCE(ts.trust_score, 1.0) as trust_score "
|
|
219
|
+
"FROM atomic_facts f "
|
|
220
|
+
"LEFT JOIN ("
|
|
221
|
+
" SELECT fact_id, COUNT(*) as access_count "
|
|
222
|
+
" FROM fact_access_log WHERE profile_id = ? GROUP BY fact_id"
|
|
223
|
+
") al ON f.fact_id = al.fact_id "
|
|
224
|
+
"LEFT JOIN fact_importance fi "
|
|
225
|
+
" ON f.fact_id = fi.fact_id AND fi.profile_id = ? "
|
|
226
|
+
"LEFT JOIN fact_retention r "
|
|
227
|
+
" ON f.fact_id = r.fact_id AND r.profile_id = ? "
|
|
228
|
+
"LEFT JOIN trust_scores ts "
|
|
229
|
+
" ON ts.target_id = f.created_by "
|
|
230
|
+
" AND ts.target_type = 'agent' "
|
|
231
|
+
" AND ts.profile_id = ? "
|
|
232
|
+
"WHERE f.profile_id = ? "
|
|
233
|
+
"AND f.fact_id NOT IN ("
|
|
234
|
+
" SELECT json_each.value "
|
|
235
|
+
" FROM core_memory_blocks, json_each(core_memory_blocks.source_fact_ids) "
|
|
236
|
+
" WHERE core_memory_blocks.profile_id = ?"
|
|
237
|
+
")"
|
|
238
|
+
)
|
|
239
|
+
params = (profile_id,) * 6
|
|
240
|
+
else:
|
|
241
|
+
sql = (
|
|
242
|
+
"SELECT f.fact_id, "
|
|
243
|
+
" COALESCE(al.access_count, 0) as access_count, "
|
|
244
|
+
" COALESCE(fi.pagerank_score, 0.0) as importance, "
|
|
245
|
+
" COALESCE(f.evidence_count, 0) as confirmation_count, "
|
|
246
|
+
" f.created_at, "
|
|
247
|
+
" COALESCE(r.last_accessed_at, f.created_at) as last_accessed_at, "
|
|
248
|
+
" COALESCE(f.emotional_valence, 0.0) as emotional_salience "
|
|
249
|
+
"FROM atomic_facts f "
|
|
250
|
+
"LEFT JOIN ("
|
|
251
|
+
" SELECT fact_id, COUNT(*) as access_count "
|
|
252
|
+
" FROM fact_access_log WHERE profile_id = ? GROUP BY fact_id"
|
|
253
|
+
") al ON f.fact_id = al.fact_id "
|
|
254
|
+
"LEFT JOIN fact_importance fi "
|
|
255
|
+
" ON f.fact_id = fi.fact_id AND fi.profile_id = ? "
|
|
256
|
+
"LEFT JOIN fact_retention r "
|
|
257
|
+
" ON f.fact_id = r.fact_id AND r.profile_id = ? "
|
|
258
|
+
"WHERE f.profile_id = ? "
|
|
259
|
+
"AND f.fact_id NOT IN ("
|
|
260
|
+
" SELECT json_each.value "
|
|
261
|
+
" FROM core_memory_blocks, json_each(core_memory_blocks.source_fact_ids) "
|
|
262
|
+
" WHERE core_memory_blocks.profile_id = ?"
|
|
263
|
+
")"
|
|
264
|
+
)
|
|
265
|
+
params = (profile_id,) * 5
|
|
266
|
+
|
|
267
|
+
rows = self._db.execute(sql, params)
|
|
230
268
|
|
|
231
269
|
facts: list[dict] = []
|
|
232
270
|
for row in rows:
|
|
@@ -238,6 +276,7 @@ class ForgettingScheduler:
|
|
|
238
276
|
"confirmation_count": int(d["confirmation_count"]),
|
|
239
277
|
"emotional_salience": float(d["emotional_salience"]),
|
|
240
278
|
"last_accessed_at": str(d["last_accessed_at"]),
|
|
279
|
+
"trust_score": float(d.get("trust_score", 1.0)),
|
|
241
280
|
})
|
|
242
281
|
return facts
|
|
243
282
|
|
|
@@ -251,6 +290,19 @@ class ForgettingScheduler:
|
|
|
251
290
|
retention_rows = self._db.batch_get_retention(fact_ids, profile_id)
|
|
252
291
|
return {r["fact_id"]: r["lifecycle_zone"] for r in retention_rows}
|
|
253
292
|
|
|
293
|
+
def _has_trust_tables(self) -> bool:
|
|
294
|
+
"""Check if trust_scores table and created_by column exist."""
|
|
295
|
+
try:
|
|
296
|
+
self._db.execute(
|
|
297
|
+
"SELECT 1 FROM trust_scores LIMIT 0", (),
|
|
298
|
+
)
|
|
299
|
+
self._db.execute(
|
|
300
|
+
"SELECT created_by FROM atomic_facts LIMIT 0", (),
|
|
301
|
+
)
|
|
302
|
+
return True
|
|
303
|
+
except Exception:
|
|
304
|
+
return False
|
|
305
|
+
|
|
254
306
|
def _soft_delete_with_audit(self, fact_id: str, profile_id: str) -> None:
|
|
255
307
|
"""Soft-delete a forgotten fact with compliance audit trail.
|
|
256
308
|
|
|
@@ -78,6 +78,7 @@ class FactRetentionInput(TypedDict):
|
|
|
78
78
|
confirmation_count: int # Mapped from atomic_facts.evidence_count
|
|
79
79
|
emotional_salience: float # Mapped from atomic_facts.emotional_valence
|
|
80
80
|
last_accessed_at: str # ISO 8601 datetime string
|
|
81
|
+
trust_score: float # Source trust in [0, 1]. Default 1.0.
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
# ---------------------------------------------------------------------------
|
|
@@ -142,6 +143,47 @@ class EbbinghausCurve:
|
|
|
142
143
|
# HR-02: Clamp to [0.0, 1.0]
|
|
143
144
|
return max(0.0, min(1.0, r))
|
|
144
145
|
|
|
146
|
+
def trust_modulated_retention(
|
|
147
|
+
self,
|
|
148
|
+
hours_since_access: float,
|
|
149
|
+
strength: float,
|
|
150
|
+
trust_score: float = 1.0,
|
|
151
|
+
) -> float:
|
|
152
|
+
"""Compute trust-weighted Ebbinghaus retention.
|
|
153
|
+
|
|
154
|
+
lambda_eff = lambda * (1 + kappa * (1 - trust))
|
|
155
|
+
|
|
156
|
+
Low-trust memories decay faster. When trust=1.0, identical to
|
|
157
|
+
standard retention. When trust=0.0, decay rate is (1+kappa)x faster.
|
|
158
|
+
|
|
159
|
+
Paper 3, Section 5.5: Trust-Weighted Forgetting.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
hours_since_access: Hours since last access.
|
|
163
|
+
strength: Memory strength S.
|
|
164
|
+
trust_score: Source trust in [0, 1]. Default 1.0 (fully trusted).
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Retention score in [0.0, 1.0].
|
|
168
|
+
"""
|
|
169
|
+
if hours_since_access < 0:
|
|
170
|
+
return 1.0
|
|
171
|
+
|
|
172
|
+
s = max(self._config.min_strength, strength)
|
|
173
|
+
tau = max(0.0, min(1.0, trust_score))
|
|
174
|
+
kappa = self._config.trust_kappa
|
|
175
|
+
|
|
176
|
+
# Trust-modulated decay rate
|
|
177
|
+
lambda_base = 1.0 / s
|
|
178
|
+
lambda_eff = lambda_base * (1.0 + kappa * (1.0 - tau))
|
|
179
|
+
|
|
180
|
+
r = math.exp(-lambda_eff * hours_since_access)
|
|
181
|
+
|
|
182
|
+
if math.isnan(r) or math.isinf(r):
|
|
183
|
+
return 0.0
|
|
184
|
+
|
|
185
|
+
return max(0.0, min(1.0, r))
|
|
186
|
+
|
|
145
187
|
def memory_strength(
|
|
146
188
|
self,
|
|
147
189
|
access_count: int,
|
|
@@ -294,7 +336,8 @@ class EbbinghausCurve:
|
|
|
294
336
|
strength = self.memory_strength(
|
|
295
337
|
access_count, importance, confirmation_count, emotional_salience,
|
|
296
338
|
)
|
|
297
|
-
|
|
339
|
+
trust = fact.get("trust_score", 1.0)
|
|
340
|
+
ret = self.trust_modulated_retention(hours_since, strength, trust)
|
|
298
341
|
zone = self.lifecycle_zone(ret)
|
|
299
342
|
|
|
300
343
|
results.append({
|
|
@@ -145,14 +145,14 @@ class FRQADMetric:
|
|
|
145
145
|
if bit_width >= 32:
|
|
146
146
|
return np.array(base_variance, dtype=np.float64)
|
|
147
147
|
|
|
148
|
-
# V3.3.
|
|
149
|
-
# sigma²
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return np.clip(
|
|
148
|
+
# V3.3.26: MULTIPLICATIVE variance inflation (Paper 3, Equation 2).
|
|
149
|
+
# sigma²_eff = sigma²_obs * (32 / bit_width) ^ kappa
|
|
150
|
+
# When bw=32: scale=1.0 (no change). When bw=4: scale=2.83x (kappa=0.5).
|
|
151
|
+
# This is MORE novel and MORE aggressive than additive Delta²/12.
|
|
152
|
+
scale = (32.0 / bit_width) ** self._config.kappa
|
|
153
|
+
sigma_inflated = np.asarray(base_variance, dtype=np.float64) * scale
|
|
154
|
+
|
|
155
|
+
return np.clip(sigma_inflated, self._config.variance_floor, self._config.variance_ceiling)
|
|
156
156
|
|
|
157
157
|
# ------------------------------------------------------------------
|
|
158
158
|
# Core distance (THE novel contribution)
|
|
@@ -92,6 +92,8 @@ class SemanticChannel:
|
|
|
92
92
|
self._fisher_mode = fisher_mode if fisher_mode in ("simplified", "full") else "simplified"
|
|
93
93
|
# Lazily instantiated full metric (avoids import cost when not needed)
|
|
94
94
|
self._full_metric: object | None = None
|
|
95
|
+
# V3.3.26: Lazily instantiated FRQAD metric for mixed-precision scoring
|
|
96
|
+
self._frqad_metric: object | None = None
|
|
95
97
|
self._vector_store = vector_store
|
|
96
98
|
# V3.3.19: TurboQuant 3-tier search (stateless, optional)
|
|
97
99
|
self._qas = quantization_aware_search
|
|
@@ -276,21 +278,68 @@ class SemanticChannel:
|
|
|
276
278
|
q_mean: np.ndarray | None,
|
|
277
279
|
q_var: np.ndarray | None,
|
|
278
280
|
) -> float:
|
|
279
|
-
"""Compute Fisher-Rao similarity using simplified or
|
|
281
|
+
"""Compute Fisher-Rao similarity using simplified, full, or FRQAD metric.
|
|
280
282
|
|
|
281
283
|
Simplified (default): Mahalanobis-like distance using only fact variance.
|
|
282
|
-
Full: Atkinson-Mitchell geodesic via FisherRaoMetric.similarity()
|
|
283
|
-
|
|
284
|
+
Full: Atkinson-Mitchell geodesic via FisherRaoMetric.similarity().
|
|
285
|
+
FRQAD: V3.3.26 — quantization-aware distance via FRQADMetric when
|
|
286
|
+
the fact has a non-32-bit embedding (mixed precision).
|
|
284
287
|
|
|
285
|
-
Falls back to simplified if full
|
|
286
|
-
missing fisher_mean on the fact, or missing query variance).
|
|
288
|
+
Falls back to simplified if full/FRQAD cannot be applied.
|
|
287
289
|
"""
|
|
290
|
+
# V3.3.26: FRQAD for mixed-precision facts
|
|
291
|
+
fact_bw = getattr(fact, "bit_width", 32) or 32
|
|
292
|
+
if fact_bw < 32 and q_mean is not None and q_var is not None:
|
|
293
|
+
return self._compute_frqad_sim(
|
|
294
|
+
q_mean, q_var, 32, f_vec, var_vec, fact_bw, fact,
|
|
295
|
+
)
|
|
296
|
+
|
|
288
297
|
if self._fisher_mode == "full":
|
|
289
298
|
return self._compute_full_fisher_sim(
|
|
290
299
|
q_vec, f_vec, var_vec, fact, q_mean, q_var,
|
|
291
300
|
)
|
|
292
301
|
return _fisher_rao_similarity(q_vec, f_vec, var_vec, self._temperature)
|
|
293
302
|
|
|
303
|
+
def _compute_frqad_sim(
|
|
304
|
+
self,
|
|
305
|
+
q_mean: np.ndarray,
|
|
306
|
+
q_var: np.ndarray,
|
|
307
|
+
q_bw: int,
|
|
308
|
+
f_mean: np.ndarray,
|
|
309
|
+
f_var: np.ndarray,
|
|
310
|
+
f_bw: int,
|
|
311
|
+
fact: AtomicFact,
|
|
312
|
+
) -> float:
|
|
313
|
+
"""FRQAD: quantization-aware Fisher-Rao similarity (Paper 3, C1).
|
|
314
|
+
|
|
315
|
+
Uses variance inflation: sigma_eff = sigma * (32/bw)^kappa
|
|
316
|
+
to penalize lower-precision embeddings on the statistical manifold.
|
|
317
|
+
"""
|
|
318
|
+
frqad = self._get_frqad_metric()
|
|
319
|
+
if frqad is None:
|
|
320
|
+
return _fisher_rao_similarity(q_mean, f_mean, f_var, self._temperature)
|
|
321
|
+
try:
|
|
322
|
+
return frqad.similarity(
|
|
323
|
+
q_mean, q_var, q_bw,
|
|
324
|
+
f_mean, f_var, f_bw,
|
|
325
|
+
)
|
|
326
|
+
except (ValueError, FloatingPointError):
|
|
327
|
+
logger.debug("FRQAD raised; falling back to simplified Fisher-Rao")
|
|
328
|
+
return _fisher_rao_similarity(q_mean, f_mean, f_var, self._temperature)
|
|
329
|
+
|
|
330
|
+
def _get_frqad_metric(self) -> object | None:
|
|
331
|
+
"""Lazy-load FRQADMetric to avoid import-time cost."""
|
|
332
|
+
if self._frqad_metric is None:
|
|
333
|
+
try:
|
|
334
|
+
from superlocalmemory.math.fisher import FisherRaoMetric
|
|
335
|
+
from superlocalmemory.math.fisher_quantized import FRQADConfig, FRQADMetric
|
|
336
|
+
base = FisherRaoMetric(temperature=self._temperature)
|
|
337
|
+
self._frqad_metric = FRQADMetric(base, FRQADConfig())
|
|
338
|
+
except Exception:
|
|
339
|
+
logger.debug("FRQAD metric unavailable; mixed-precision scoring disabled")
|
|
340
|
+
return None
|
|
341
|
+
return self._frqad_metric
|
|
342
|
+
|
|
294
343
|
def _compute_full_fisher_sim(
|
|
295
344
|
self,
|
|
296
345
|
q_vec: np.ndarray,
|