superlocalmemory 3.3.2 → 3.3.3

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/CHANGELOG.md CHANGED
@@ -16,6 +16,18 @@ SuperLocalMemory V3 - Intelligent local memory system for AI coding assistants.
16
16
 
17
17
  ---
18
18
 
19
+ ## [3.3.3] - 2026-04-01 — Langevin Awakening
20
+
21
+ ### Fixed
22
+ - **Langevin dynamics now active** — positions were never initialized at store time, causing the entire Langevin lifecycle system to be inert (0 positioned facts). New facts now receive near-origin positions (Strategy A).
23
+ - **Backfill for existing facts** — maintenance now initializes unpositioned facts using metadata-aware equilibrium seeding (Strategy B) followed by 50-step burn-in (Strategy C). Old, rarely-accessed facts land in their correct lifecycle zones immediately.
24
+
25
+ ### Improved
26
+ - Maintenance returns `langevin_backfilled` count for observability
27
+ - Health check now reports positioned facts accurately after backfill
28
+
29
+ ---
30
+
19
31
  ## [3.3.0] - 2026-03-31 — The Living Brain
20
32
 
21
33
  ### New Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.3.2"
3
+ version = "3.3.3"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -6,6 +6,7 @@
6
6
 
7
7
  Periodic batch processing for mathematical layers:
8
8
  1. Langevin batch_step on all active facts (self-organization)
9
+ 1a. Backfill: seed uninitialized facts with metadata-aware positions (B+C)
9
10
  2. Sheaf batch consistency check on recent facts
10
11
  3. Fisher adaptive temperature recalculation
11
12
 
@@ -18,15 +19,72 @@ License: MIT
18
19
  from __future__ import annotations
19
20
 
20
21
  import logging
22
+ import math as _math
21
23
  from datetime import UTC, datetime, timedelta
22
24
  from typing import TYPE_CHECKING
23
25
 
26
+ import numpy as np
27
+
24
28
  if TYPE_CHECKING:
25
29
  from superlocalmemory.core.config import SLMConfig
26
30
  from superlocalmemory.storage.database import DatabaseManager
27
31
 
28
32
  logger = logging.getLogger(__name__)
29
33
 
34
+ # Backfill constants
35
+ _BACKFILL_BURN_IN_STEPS = 50
36
+ _LANGEVIN_DIM = 8
37
+ _MAX_NORM = 0.99
38
+
39
+
40
+ def _compute_equilibrium_radius(
41
+ access_count: int,
42
+ age_days: float,
43
+ importance: float,
44
+ temperature: float = 0.3,
45
+ dim: int = 8,
46
+ ) -> float:
47
+ """Compute metadata-aware equilibrium radius (Strategy B).
48
+
49
+ Uses the Langevin potential coefficients to estimate where a fact
50
+ would settle if it had been in the dynamics from the start.
51
+
52
+ r_eq ≈ sqrt(T * dim / (2 * effective_alpha))
53
+ """
54
+ alpha, beta, gamma, delta = 3.0, 0.8, 0.005, 0.5
55
+ effective_alpha = (
56
+ alpha
57
+ + beta * _math.log(access_count + 1) / 10.0
58
+ - gamma * min(age_days, 365.0) / 365.0
59
+ + delta * importance
60
+ )
61
+ effective_alpha = max(0.1, effective_alpha)
62
+ r_eq = _math.sqrt(temperature * dim / (2.0 * effective_alpha))
63
+ return min(r_eq, _MAX_NORM * 0.95)
64
+
65
+
66
+ def _seed_langevin_position(
67
+ access_count: int,
68
+ age_days: float,
69
+ importance: float,
70
+ temperature: float = 0.3,
71
+ dim: int = 8,
72
+ ) -> list[float]:
73
+ """Create a metadata-aware initial position (Strategy B).
74
+
75
+ Places the fact at the equilibrium radius with a random direction.
76
+ """
77
+ r_eq = _compute_equilibrium_radius(
78
+ access_count, age_days, importance, temperature, dim,
79
+ )
80
+ rng = np.random.default_rng()
81
+ direction = rng.standard_normal(dim)
82
+ norm = float(np.linalg.norm(direction))
83
+ if norm < 1e-8:
84
+ direction = np.ones(dim)
85
+ norm = float(np.linalg.norm(direction))
86
+ return (direction / norm * r_eq).tolist()
87
+
30
88
 
31
89
  def run_maintenance(
32
90
  db: DatabaseManager,
@@ -44,6 +102,7 @@ def run_maintenance(
44
102
  Dict of counts: langevin_updated, sheaf_checked, etc.
45
103
  """
46
104
  counts: dict[str, int] = {
105
+ "langevin_backfilled": 0,
47
106
  "langevin_updated": 0,
48
107
  "fisher_coupled": 0,
49
108
  "sheaf_checked": 0,
@@ -53,13 +112,60 @@ def run_maintenance(
53
112
  if not facts:
54
113
  return counts
55
114
 
56
- # 1. Langevin batch step
115
+ # 1a. Backfill: seed uninitialized facts with metadata-aware positions (B+C)
116
+ if config.math.langevin_persist_positions:
117
+ try:
118
+ from superlocalmemory.math.langevin import LangevinDynamics
119
+
120
+ ld = LangevinDynamics(
121
+ dim=_LANGEVIN_DIM,
122
+ dt=config.math.langevin_dt,
123
+ temperature=config.math.langevin_temperature,
124
+ )
125
+
126
+ backfilled = 0
127
+ for f in facts:
128
+ if f.langevin_position is not None:
129
+ continue
130
+ created = datetime.fromisoformat(
131
+ f.created_at.replace("Z", "+00:00")
132
+ ) if f.created_at else datetime.now(UTC)
133
+ age_days = max(
134
+ 0.0,
135
+ (datetime.now(UTC) - created).total_seconds() / 86400.0,
136
+ )
137
+ # Strategy B: metadata-aware seed position
138
+ position = _seed_langevin_position(
139
+ f.access_count, age_days, f.importance,
140
+ config.math.langevin_temperature, _LANGEVIN_DIM,
141
+ )
142
+ # Strategy C: burn-in from the seeded position
143
+ for step_i in range(_BACKFILL_BURN_IN_STEPS):
144
+ position, _ = ld.step(
145
+ position, f.access_count, age_days, f.importance,
146
+ )
147
+ weight = ld.compute_lifecycle_weight(position)
148
+ lifecycle = ld.get_lifecycle_state(weight).value
149
+ db.update_fact(f.fact_id, {
150
+ "langevin_position": position,
151
+ "lifecycle": lifecycle,
152
+ })
153
+ f.langevin_position = position # update in-memory for step 1b
154
+ backfilled += 1
155
+
156
+ counts["langevin_backfilled"] = backfilled
157
+ if backfilled:
158
+ logger.info("Langevin backfill: %d facts initialized", backfilled)
159
+ except Exception as exc:
160
+ logger.warning("Langevin backfill failed: %s", exc)
161
+
162
+ # 1b. Langevin batch step on all positioned facts
57
163
  if config.math.langevin_persist_positions:
58
164
  try:
59
165
  from superlocalmemory.math.langevin import LangevinDynamics
60
166
 
61
167
  ld = LangevinDynamics(
62
- dim=8,
168
+ dim=_LANGEVIN_DIM,
63
169
  dt=config.math.langevin_dt,
64
170
  temperature=config.math.langevin_temperature,
65
171
  )
@@ -165,8 +271,8 @@ def run_maintenance(
165
271
  logger.warning("Sheaf maintenance failed: %s", exc)
166
272
 
167
273
  logger.info(
168
- "Maintenance complete: %d Langevin, %d Fisher-coupled, %d Sheaf",
169
- counts["langevin_updated"], counts["fisher_coupled"],
170
- counts["sheaf_checked"],
274
+ "Maintenance complete: %d backfilled, %d Langevin, %d Fisher-coupled, %d Sheaf",
275
+ counts["langevin_backfilled"], counts["langevin_updated"],
276
+ counts["fisher_coupled"], counts["sheaf_checked"],
171
277
  )
172
278
  return counts
@@ -25,6 +25,25 @@ from superlocalmemory.storage.models import (
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
27
 
28
+ # Langevin initialization radius for new facts (ACTIVE zone < 0.3)
29
+ _INIT_LANGEVIN_RADIUS = 0.05
30
+
31
+
32
+ def _init_langevin_position(dim: int = 8) -> list[float]:
33
+ """Initialize Langevin position near origin for a new fact.
34
+
35
+ Small random perturbation ensures each fact gets a unique position
36
+ while staying deep in the ACTIVE zone (radius < 0.3).
37
+ """
38
+ import numpy as np
39
+ rng = np.random.default_rng()
40
+ direction = rng.standard_normal(dim)
41
+ norm = float(np.linalg.norm(direction))
42
+ if norm < 1e-8:
43
+ direction = np.ones(dim)
44
+ norm = float(np.linalg.norm(direction))
45
+ return (direction / norm * _INIT_LANGEVIN_RADIUS).tolist()
46
+
28
47
 
29
48
  # ---------------------------------------------------------------------------
30
49
  # enrich_fact (was MemoryEngine._enrich_fact)
@@ -59,6 +78,10 @@ def enrich_fact(
59
78
  emotion = tag_emotion(fact.content)
60
79
  signal = infer_signal(fact.content)
61
80
 
81
+ # Strategy A: initialize Langevin position near origin (ACTIVE zone).
82
+ # New facts start as ACTIVE; dynamics will evolve them based on access patterns.
83
+ langevin_pos = _init_langevin_position(dim=8)
84
+
62
85
  return AtomicFact(
63
86
  fact_id=fact.fact_id, memory_id=record.memory_id,
64
87
  profile_id=profile_id, content=fact.content,
@@ -73,6 +96,7 @@ def enrich_fact(
73
96
  evidence_count=fact.evidence_count,
74
97
  source_turn_ids=fact.source_turn_ids, session_id=record.session_id,
75
98
  embedding=embedding, fisher_mean=fisher_mean, fisher_variance=fisher_variance,
99
+ langevin_position=langevin_pos,
76
100
  emotional_valence=emotion.valence, emotional_arousal=emotion.arousal,
77
101
  signal_type=signal, created_at=fact.created_at,
78
102
  )