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.
- package/CHANGELOG.md +43 -1
- package/README.md +106 -71
- package/package.json +1 -2
- package/pyproject.toml +16 -1
- package/src/superlocalmemory/cli/commands.py +309 -0
- package/src/superlocalmemory/cli/main.py +44 -0
- package/src/superlocalmemory/core/config.py +282 -11
- package/src/superlocalmemory/core/consolidation_engine.py +37 -0
- package/src/superlocalmemory/core/engine.py +21 -0
- package/src/superlocalmemory/core/engine_wiring.py +58 -8
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +374 -0
- package/src/superlocalmemory/dynamics/eap_scheduler.py +276 -0
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +171 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +804 -0
- package/src/superlocalmemory/hooks/auto_invoker.py +46 -8
- package/src/superlocalmemory/hooks/auto_parameterize.py +147 -0
- package/src/superlocalmemory/infra/heartbeat_monitor.py +140 -0
- package/src/superlocalmemory/infra/pid_manager.py +193 -0
- package/src/superlocalmemory/infra/process_reaper.py +572 -0
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +115 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +263 -0
- package/src/superlocalmemory/learning/quantization_scheduler.py +320 -0
- package/src/superlocalmemory/math/ebbinghaus.py +309 -0
- package/src/superlocalmemory/math/fisher_quantized.py +251 -0
- package/src/superlocalmemory/math/hopfield.py +279 -0
- package/src/superlocalmemory/math/polar_quant.py +379 -0
- package/src/superlocalmemory/math/qjl.py +115 -0
- package/src/superlocalmemory/mcp/server.py +2 -0
- package/src/superlocalmemory/mcp/tools_v3.py +10 -0
- package/src/superlocalmemory/mcp/tools_v33.py +351 -0
- package/src/superlocalmemory/parameterization/__init__.py +47 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +534 -0
- package/src/superlocalmemory/parameterization/pii_filter.py +106 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +216 -0
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +275 -0
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +425 -0
- package/src/superlocalmemory/retrieval/engine.py +21 -3
- package/src/superlocalmemory/retrieval/forgetting_filter.py +145 -0
- package/src/superlocalmemory/retrieval/hopfield_channel.py +335 -0
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +133 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +1 -1
- package/src/superlocalmemory/retrieval/strategy.py +16 -6
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/routes/agents.py +68 -8
- package/src/superlocalmemory/server/routes/learning.py +18 -1
- package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
- package/src/superlocalmemory/server/routes/v3_api.py +503 -1
- package/src/superlocalmemory/storage/database.py +206 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
- package/src/superlocalmemory/storage/migration_v33.py +140 -0
- package/src/superlocalmemory/storage/quantized_store.py +261 -0
- package/src/superlocalmemory/storage/schema_v32.py +137 -0
- package/conftest.py +0 -5
|
@@ -92,6 +92,7 @@ class ChannelWeights:
|
|
|
92
92
|
entity_graph: float = 1.3
|
|
93
93
|
temporal: float = 1.0
|
|
94
94
|
spreading_activation: float = 1.0 # Phase 3: 5th channel (BC-08: default value)
|
|
95
|
+
hopfield: float = 0.8 # Phase G: 6th channel (Hopfield associative memory)
|
|
95
96
|
|
|
96
97
|
def as_dict(self) -> dict[str, float]:
|
|
97
98
|
return {
|
|
@@ -100,6 +101,7 @@ class ChannelWeights:
|
|
|
100
101
|
"entity_graph": self.entity_graph,
|
|
101
102
|
"temporal": self.temporal,
|
|
102
103
|
"spreading_activation": self.spreading_activation,
|
|
104
|
+
"hopfield": self.hopfield,
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
|
|
@@ -162,6 +164,9 @@ class RetrievalConfig:
|
|
|
162
164
|
spreading_activation_decay: float = 0.7
|
|
163
165
|
spreading_activation_threshold: float = 0.1
|
|
164
166
|
|
|
167
|
+
# Hopfield (Phase G: 6th channel)
|
|
168
|
+
hopfield_top_k: int = 50
|
|
169
|
+
|
|
165
170
|
# Trust weighting — apply Bayesian trust scores to retrieval ranking.
|
|
166
171
|
# When enabled, each fact's score is multiplied by a trust weight in [0.5, 1.5].
|
|
167
172
|
# Low-trust facts are demoted; high-trust facts are promoted.
|
|
@@ -217,10 +222,10 @@ class MathConfig:
|
|
|
217
222
|
class ConsolidationConfig:
|
|
218
223
|
"""Configuration for sleep-time consolidation (Phase 5).
|
|
219
224
|
|
|
220
|
-
|
|
225
|
+
Ships enabled by default. Users can disable via slm config.
|
|
221
226
|
"""
|
|
222
227
|
|
|
223
|
-
enabled: bool =
|
|
228
|
+
enabled: bool = True
|
|
224
229
|
step_count_trigger: int = 50 # Lightweight consolidation every N stores (L7)
|
|
225
230
|
session_trigger: bool = True # Run on session end
|
|
226
231
|
idle_timeout_seconds: int = 300 # 5 min inactivity
|
|
@@ -233,14 +238,232 @@ class ConsolidationConfig:
|
|
|
233
238
|
decay_days_threshold: int = 30 # Edge decay after N days
|
|
234
239
|
|
|
235
240
|
|
|
241
|
+
@dataclass(frozen=True)
|
|
242
|
+
class ForgettingConfig:
|
|
243
|
+
"""Ebbinghaus forgetting configuration."""
|
|
244
|
+
|
|
245
|
+
enabled: bool = True
|
|
246
|
+
# Strength coefficients
|
|
247
|
+
alpha: float = 2.0 # Access frequency weight (log scale)
|
|
248
|
+
beta: float = 1.5 # Importance weight (PageRank)
|
|
249
|
+
gamma: float = 1.0 # Confirmation count weight
|
|
250
|
+
delta: float = 0.5 # Emotional salience weight
|
|
251
|
+
# Strength bounds
|
|
252
|
+
min_strength: float = 0.1 # Floor (prevents instant forgetting)
|
|
253
|
+
max_strength: float = 100.0 # Ceiling (numerical stability)
|
|
254
|
+
# Zone thresholds
|
|
255
|
+
archive_threshold: float = 0.2 # Below this -> ARCHIVE
|
|
256
|
+
forget_threshold: float = 0.05 # Below this -> FORGOTTEN
|
|
257
|
+
# Spaced repetition
|
|
258
|
+
learning_rate: float = 1.0 # eta in spaced repetition update
|
|
259
|
+
# Coupling
|
|
260
|
+
forgetting_drift_scale: float = 0.5 # How strongly forgetting affects Langevin drift
|
|
261
|
+
# Scheduler
|
|
262
|
+
scheduler_interval_minutes: int = 30 # How often to recompute retentions
|
|
263
|
+
# Immunity
|
|
264
|
+
core_memory_immune: bool = True # Core Memory blocks never forget
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass(frozen=True)
|
|
268
|
+
class HopfieldConfig:
|
|
269
|
+
"""Modern Continuous Hopfield Network configuration (Ramsauer et al., 2020).
|
|
270
|
+
|
|
271
|
+
Energy: E(xi) = -log(sum_i exp(B * xi' * x_i)) + B/2 * ||xi||^2
|
|
272
|
+
Update: xi_new = X' @ softmax(B * X @ xi)
|
|
273
|
+
Beta: B = 1/sqrt(d) where d = dimension
|
|
274
|
+
Storage capacity: O(e^{d/2}) -- exponential in dimension.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
enabled: bool = True
|
|
278
|
+
dimension: int = 768
|
|
279
|
+
max_iterations: int = 1
|
|
280
|
+
convergence_epsilon: float = 1e-6
|
|
281
|
+
prefilter_threshold: int = 10_000
|
|
282
|
+
prefilter_candidates: int = 1000
|
|
283
|
+
skip_threshold: int = 100_000
|
|
284
|
+
cache_ttl_seconds: float = 60.0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass(frozen=True)
|
|
288
|
+
class ReaperConfig:
|
|
289
|
+
"""Process health & stale reaper configuration (Phase H0).
|
|
290
|
+
|
|
291
|
+
Prevents zombie SLM processes from exhausting RAM.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
enabled: bool = True
|
|
295
|
+
heartbeat_interval_seconds: int = 60
|
|
296
|
+
orphan_age_threshold_hours: float = 4.0
|
|
297
|
+
pid_file_path: str = ""
|
|
298
|
+
graceful_timeout_seconds: float = 5.0
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass(frozen=True)
|
|
302
|
+
class PolarQuantConfig:
|
|
303
|
+
"""PolarQuant embedding quantization configuration.
|
|
304
|
+
|
|
305
|
+
Random orthogonal rotation + recursive polar + scalar quantization.
|
|
306
|
+
Reference: TurboQuant (ICLR 2026), PolarQuant (arXiv 2502.02617).
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
dimension: int = 768
|
|
310
|
+
rotation_matrix_path: str = "" # empty = ~/.superlocalmemory/polar_rotation.npy
|
|
311
|
+
seed: int = 42 # reproducible rotation matrix
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(frozen=True)
|
|
315
|
+
class QJLConfig:
|
|
316
|
+
"""QJL 1-bit residual correction configuration.
|
|
317
|
+
|
|
318
|
+
Random projection + sign-bit quantization for asymmetric IP estimation.
|
|
319
|
+
Reference: QJL (AAAI 2025, arXiv 2406.03482).
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
projection_dim: int = 128
|
|
323
|
+
seed: int = 43 # separate from PolarQuant
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@dataclass(frozen=True)
|
|
327
|
+
class QuantizationConfig:
|
|
328
|
+
"""Memory-aware embedding quantization (EAP + LP2E).
|
|
329
|
+
|
|
330
|
+
Couples Ebbinghaus retention to embedding precision.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
enabled: bool = True
|
|
334
|
+
polar: PolarQuantConfig = field(default_factory=PolarQuantConfig)
|
|
335
|
+
qjl: QJLConfig = field(default_factory=QJLConfig)
|
|
336
|
+
default_bit_width: int = 32
|
|
337
|
+
eap_enabled: bool = True
|
|
338
|
+
keep_float32_backup: bool = True
|
|
339
|
+
auto_compact_interval_hours: int = 6
|
|
340
|
+
polar_search_penalty: float = 0.95
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@dataclass(frozen=True)
|
|
344
|
+
class CCQConfig:
|
|
345
|
+
"""Cognitive Consolidation Quantization configuration (Phase E).
|
|
346
|
+
|
|
347
|
+
Ships enabled by default. CCQ runs as Step 7 of the consolidation cycle.
|
|
348
|
+
Biological analogy: sleep-time hippocampal-neocortical transfer.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
enabled: bool = True
|
|
352
|
+
|
|
353
|
+
# Candidate identification
|
|
354
|
+
retention_threshold: float = 0.5
|
|
355
|
+
max_candidates_per_run: int = 200
|
|
356
|
+
|
|
357
|
+
# Clustering
|
|
358
|
+
min_entity_overlap: int = 2
|
|
359
|
+
temporal_window_days: int = 7
|
|
360
|
+
min_cluster_size: int = 3
|
|
361
|
+
max_cluster_size: int = 20
|
|
362
|
+
|
|
363
|
+
# Gist extraction
|
|
364
|
+
use_llm_gist: bool = True
|
|
365
|
+
max_gist_chars: int = 500
|
|
366
|
+
min_entity_coverage: float = 0.5
|
|
367
|
+
|
|
368
|
+
# Embedding compression
|
|
369
|
+
target_bit_width: int = 2
|
|
370
|
+
compress_embeddings: bool = True
|
|
371
|
+
|
|
372
|
+
# Scheduling
|
|
373
|
+
store_count_trigger: int = 100
|
|
374
|
+
run_on_session_end: bool = True
|
|
375
|
+
|
|
376
|
+
# Safety
|
|
377
|
+
core_memory_immune: bool = True
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@dataclass(frozen=True)
|
|
381
|
+
class SAGQConfig:
|
|
382
|
+
"""Spreading Activation-Guided Quantization configuration.
|
|
383
|
+
|
|
384
|
+
Centrality formula:
|
|
385
|
+
centrality(i) = w_pagerank * pr_norm + w_degree * deg_norm + w_sa_freq * sa_freq_norm
|
|
386
|
+
|
|
387
|
+
SAGQ precision:
|
|
388
|
+
sagq_bw = b_min + (b_max - b_min) * centrality, snapped to valid_bit_widths
|
|
389
|
+
|
|
390
|
+
Combined precision (with Phase A EAP):
|
|
391
|
+
final_bw = max(eap_bw, sagq_bw)
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
enabled: bool = True
|
|
395
|
+
|
|
396
|
+
# Centrality weights (MUST sum to 1.0 -- validated in __post_init__)
|
|
397
|
+
w_pagerank: float = 0.5 # PageRank structural importance
|
|
398
|
+
w_degree: float = 0.3 # Degree centrality (connection count)
|
|
399
|
+
w_sa_freq: float = 0.2 # Spreading activation frequency (7-day window)
|
|
400
|
+
|
|
401
|
+
# Bit-width range
|
|
402
|
+
b_min: int = 2 # Minimum bit-width (most aggressive quantization)
|
|
403
|
+
b_max: int = 32 # Maximum bit-width (full float32 precision)
|
|
404
|
+
|
|
405
|
+
# Valid bit-widths (snapping targets) -- must be sorted ascending
|
|
406
|
+
valid_bit_widths: tuple[int, ...] = (2, 4, 8, 32)
|
|
407
|
+
|
|
408
|
+
# SA frequency window (days to look back in activation_cache)
|
|
409
|
+
sa_frequency_window_days: int = 7
|
|
410
|
+
|
|
411
|
+
# Scheduler
|
|
412
|
+
scheduler_interval_hours: float = 6.0 # How often to run combined scheduler
|
|
413
|
+
|
|
414
|
+
def __post_init__(self) -> None:
|
|
415
|
+
weight_sum = self.w_pagerank + self.w_degree + self.w_sa_freq
|
|
416
|
+
if abs(weight_sum - 1.0) > 1e-6:
|
|
417
|
+
raise ValueError(
|
|
418
|
+
f"SAGQConfig centrality weights must sum to 1.0, got {weight_sum:.6f}"
|
|
419
|
+
)
|
|
420
|
+
if not self.valid_bit_widths:
|
|
421
|
+
raise ValueError("SAGQConfig.valid_bit_widths must not be empty")
|
|
422
|
+
if self.b_min < 1:
|
|
423
|
+
raise ValueError(f"SAGQConfig.b_min must be >= 1, got {self.b_min}")
|
|
424
|
+
if self.b_max < self.b_min:
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f"SAGQConfig.b_max ({self.b_max}) must be >= b_min ({self.b_min})"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclass(frozen=True)
|
|
431
|
+
class ParameterizationConfig:
|
|
432
|
+
"""Soft prompt parameterization configuration (Phase F: The Learning Brain).
|
|
433
|
+
|
|
434
|
+
Controls pattern extraction, prompt generation, injection, and lifecycle.
|
|
435
|
+
Ships enabled by default. Pure text soft prompts — no LoRA, no weights.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
enabled: bool = True
|
|
439
|
+
|
|
440
|
+
# Pattern extraction
|
|
441
|
+
min_confidence: float = 0.7 # Minimum pattern confidence [0.3, 1.0]
|
|
442
|
+
min_evidence: int = 5 # Minimum evidence count for behavioral/workflow
|
|
443
|
+
cross_project_boost: float = 1.2 # 20% confidence boost for cross-project patterns
|
|
444
|
+
|
|
445
|
+
# Prompt generation
|
|
446
|
+
max_prompt_tokens: int = 500 # Token budget for soft prompts
|
|
447
|
+
max_memory_tokens: int = 1500 # Token budget for regular memories
|
|
448
|
+
categories_enabled: tuple[str, ...] = (
|
|
449
|
+
"identity", "tech_preference", "communication_style",
|
|
450
|
+
"workflow_pattern", "project_context", "decision_history",
|
|
451
|
+
"avoidance",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Lifecycle
|
|
455
|
+
refresh_interval_hours: float = 24.0 # Min hours between parameterization runs
|
|
456
|
+
effectiveness_tracking: bool = True # Track prompt effectiveness via feedback
|
|
457
|
+
|
|
458
|
+
|
|
236
459
|
@dataclass(frozen=True)
|
|
237
460
|
class TemporalValidatorConfig:
|
|
238
461
|
"""Configuration for temporal intelligence (Phase 4).
|
|
239
462
|
|
|
240
|
-
|
|
463
|
+
Ships enabled by default. Users can disable via slm config.
|
|
241
464
|
"""
|
|
242
465
|
|
|
243
|
-
enabled: bool =
|
|
466
|
+
enabled: bool = True
|
|
244
467
|
mode: str = "a" # "a" (sheaf), "b"/"c" (LLM)
|
|
245
468
|
|
|
246
469
|
# Sheaf contradiction threshold
|
|
@@ -263,8 +486,7 @@ class TemporalValidatorConfig:
|
|
|
263
486
|
class AutoInvokeConfig:
|
|
264
487
|
"""Configuration for the Auto-Invoke Engine (Phase 2).
|
|
265
488
|
|
|
266
|
-
|
|
267
|
-
after MRR validation passes (Rule 12).
|
|
489
|
+
Ships enabled by default. Users can disable via slm config.
|
|
268
490
|
|
|
269
491
|
References:
|
|
270
492
|
- SYNAPSE: FOK gating (fok_threshold = 0.12)
|
|
@@ -272,7 +494,7 @@ class AutoInvokeConfig:
|
|
|
272
494
|
- Zep/Hindsight: multi-signal ranking consensus
|
|
273
495
|
"""
|
|
274
496
|
|
|
275
|
-
enabled: bool =
|
|
497
|
+
enabled: bool = True
|
|
276
498
|
profile_id: str = "default"
|
|
277
499
|
|
|
278
500
|
# Scoring weights (4-signal default) -- must sum to 1.0
|
|
@@ -341,6 +563,15 @@ class SLMConfig:
|
|
|
341
563
|
consolidation: ConsolidationConfig = field(
|
|
342
564
|
default_factory=ConsolidationConfig,
|
|
343
565
|
)
|
|
566
|
+
forgetting: ForgettingConfig = field(default_factory=ForgettingConfig)
|
|
567
|
+
hopfield: HopfieldConfig = field(default_factory=HopfieldConfig)
|
|
568
|
+
reaper: ReaperConfig = field(default_factory=ReaperConfig)
|
|
569
|
+
quantization: QuantizationConfig = field(default_factory=QuantizationConfig)
|
|
570
|
+
sagq: SAGQConfig = field(default_factory=SAGQConfig)
|
|
571
|
+
ccq: CCQConfig = field(default_factory=CCQConfig)
|
|
572
|
+
parameterization: ParameterizationConfig = field(
|
|
573
|
+
default_factory=ParameterizationConfig,
|
|
574
|
+
)
|
|
344
575
|
|
|
345
576
|
def __post_init__(self) -> None:
|
|
346
577
|
if self.db_path is None:
|
|
@@ -369,6 +600,22 @@ class SLMConfig:
|
|
|
369
600
|
embedding_deployment=emb_data.get("deployment_name", ""),
|
|
370
601
|
)
|
|
371
602
|
config.active_profile = data.get("active_profile", "default")
|
|
603
|
+
|
|
604
|
+
# V3.3 config fields (additive — defaults work if missing from JSON)
|
|
605
|
+
fg = data.get("forgetting", {})
|
|
606
|
+
if fg:
|
|
607
|
+
config.forgetting = ForgettingConfig(**{
|
|
608
|
+
k: v for k, v in fg.items()
|
|
609
|
+
if k in ForgettingConfig.__dataclass_fields__
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
rt = data.get("retrieval", {})
|
|
613
|
+
if rt:
|
|
614
|
+
config.retrieval = RetrievalConfig(**{
|
|
615
|
+
k: v for k, v in rt.items()
|
|
616
|
+
if k in RetrievalConfig.__dataclass_fields__
|
|
617
|
+
})
|
|
618
|
+
|
|
372
619
|
return config
|
|
373
620
|
|
|
374
621
|
def save(self, config_path: Path | None = None) -> None:
|
|
@@ -376,6 +623,14 @@ class SLMConfig:
|
|
|
376
623
|
import json
|
|
377
624
|
path = config_path or (self.base_dir / "config.json")
|
|
378
625
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
626
|
+
# Read existing config to preserve V3.3 fields not in this save
|
|
627
|
+
existing = {}
|
|
628
|
+
if path.exists():
|
|
629
|
+
try:
|
|
630
|
+
existing = json.loads(path.read_text())
|
|
631
|
+
except (json.JSONDecodeError, OSError):
|
|
632
|
+
pass
|
|
633
|
+
|
|
379
634
|
data = {
|
|
380
635
|
"mode": self.mode.value,
|
|
381
636
|
"active_profile": self.active_profile,
|
|
@@ -393,7 +648,16 @@ class SLMConfig:
|
|
|
393
648
|
"api_key": self.embedding.api_key,
|
|
394
649
|
"deployment_name": self.embedding.deployment_name,
|
|
395
650
|
},
|
|
651
|
+
"retrieval": {
|
|
652
|
+
"use_cross_encoder": self.retrieval.use_cross_encoder,
|
|
653
|
+
},
|
|
396
654
|
}
|
|
655
|
+
|
|
656
|
+
# Preserve existing V3.3 config sections that aren't in for_mode()
|
|
657
|
+
for key in ("forgetting", "quantization", "sagq", "embedding_signature", "auto_invoke"):
|
|
658
|
+
if key in existing:
|
|
659
|
+
data[key] = existing[key]
|
|
660
|
+
|
|
397
661
|
path.write_text(json.dumps(data, indent=2))
|
|
398
662
|
|
|
399
663
|
@staticmethod
|
|
@@ -456,11 +720,13 @@ class SLMConfig:
|
|
|
456
720
|
embedding=EmbeddingConfig(
|
|
457
721
|
model_name="nomic-ai/nomic-embed-text-v1.5",
|
|
458
722
|
dimension=768,
|
|
459
|
-
|
|
723
|
+
# Mode A: sentence-transformers in SUBPROCESS (never in-process)
|
|
724
|
+
provider=embedding_provider or "sentence-transformers",
|
|
460
725
|
),
|
|
461
726
|
llm=LLMConfig(), # No LLM
|
|
462
727
|
retrieval=RetrievalConfig(
|
|
463
|
-
|
|
728
|
+
# Mode A: no cross-encoder (saves ~1.5GB PyTorch RAM)
|
|
729
|
+
use_cross_encoder=False,
|
|
464
730
|
),
|
|
465
731
|
math=MathConfig(
|
|
466
732
|
sheaf_contradiction_threshold=0.45, # 768d threshold
|
|
@@ -474,7 +740,8 @@ class SLMConfig:
|
|
|
474
740
|
embedding=EmbeddingConfig(
|
|
475
741
|
model_name="nomic-ai/nomic-embed-text-v1.5",
|
|
476
742
|
dimension=768,
|
|
477
|
-
|
|
743
|
+
# Mode B: Ollama HTTP API (zero PyTorch in-process)
|
|
744
|
+
provider=embedding_provider or "ollama",
|
|
478
745
|
),
|
|
479
746
|
llm=LLMConfig(
|
|
480
747
|
provider=llm_provider or "ollama",
|
|
@@ -482,7 +749,10 @@ class SLMConfig:
|
|
|
482
749
|
api_base=llm_api_base or "http://localhost:11434",
|
|
483
750
|
api_key=llm_api_key or "",
|
|
484
751
|
),
|
|
485
|
-
retrieval=RetrievalConfig(
|
|
752
|
+
retrieval=RetrievalConfig(
|
|
753
|
+
# Mode B: no cross-encoder (saves ~1.5GB PyTorch RAM)
|
|
754
|
+
use_cross_encoder=False,
|
|
755
|
+
),
|
|
486
756
|
)
|
|
487
757
|
|
|
488
758
|
# Mode C — FULL POWER, UNRESTRICTED
|
|
@@ -508,6 +778,7 @@ class SLMConfig:
|
|
|
508
778
|
entity_graph=1.3,
|
|
509
779
|
temporal=1.0,
|
|
510
780
|
spreading_activation=1.2, # Phase 3: SA boost in Mode C
|
|
781
|
+
hopfield=1.0, # Phase G: Hopfield in Mode C
|
|
511
782
|
),
|
|
512
783
|
retrieval=RetrievalConfig(
|
|
513
784
|
use_cross_encoder=True,
|
|
@@ -40,6 +40,9 @@ if TYPE_CHECKING:
|
|
|
40
40
|
BehavioralPatternStore,
|
|
41
41
|
BehavioralTracker,
|
|
42
42
|
)
|
|
43
|
+
from superlocalmemory.learning.consolidation_quantization_worker import (
|
|
44
|
+
CCQWorker,
|
|
45
|
+
)
|
|
43
46
|
from superlocalmemory.storage.database import DatabaseManager
|
|
44
47
|
|
|
45
48
|
logger = logging.getLogger(__name__)
|
|
@@ -72,6 +75,7 @@ class ConsolidationEngine:
|
|
|
72
75
|
graph_analyzer: GraphAnalyzer | None = None,
|
|
73
76
|
temporal_validator: TemporalValidator | None = None,
|
|
74
77
|
slm_config: SLMConfig | None = None,
|
|
78
|
+
ccq_worker: CCQWorker | None = None,
|
|
75
79
|
) -> None:
|
|
76
80
|
self._db = db
|
|
77
81
|
self._config = config
|
|
@@ -81,6 +85,7 @@ class ConsolidationEngine:
|
|
|
81
85
|
self._graph_analyzer = graph_analyzer
|
|
82
86
|
self._temporal_validator = temporal_validator
|
|
83
87
|
self._slm_config = slm_config
|
|
88
|
+
self._ccq_worker = ccq_worker
|
|
84
89
|
self._mode = slm_config.mode.value if slm_config else "a"
|
|
85
90
|
self._store_count: int = 0 # For step-count trigger (L7)
|
|
86
91
|
|
|
@@ -119,6 +124,8 @@ class ConsolidationEngine:
|
|
|
119
124
|
results["new_associations"] = self._step6_derive_associations(
|
|
120
125
|
profile_id,
|
|
121
126
|
)
|
|
127
|
+
# Step 7: Cognitive Consolidation Quantization (Phase E)
|
|
128
|
+
results["ccq"] = self._step7_ccq(profile_id)
|
|
122
129
|
results["success"] = True
|
|
123
130
|
except Exception as exc:
|
|
124
131
|
logger.warning(
|
|
@@ -463,6 +470,36 @@ class ConsolidationEngine:
|
|
|
463
470
|
pass
|
|
464
471
|
return {"summary_facts_linked": linked}
|
|
465
472
|
|
|
473
|
+
# ------------------------------------------------------------------
|
|
474
|
+
# Step 7: Cognitive Consolidation Quantization (Phase E)
|
|
475
|
+
# ------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
def _step7_ccq(self, profile_id: str) -> dict[str, Any]:
|
|
478
|
+
"""Run CCQ pipeline after existing 6-step consolidation.
|
|
479
|
+
|
|
480
|
+
CCQ is step 7 because it depends on retention data from Phase A
|
|
481
|
+
and benefits from running after standard consolidation cleanup.
|
|
482
|
+
"""
|
|
483
|
+
if self._ccq_worker is None:
|
|
484
|
+
return {"enabled": False}
|
|
485
|
+
|
|
486
|
+
if not self._ccq_worker.should_run(
|
|
487
|
+
self._store_count, is_session_end=False,
|
|
488
|
+
):
|
|
489
|
+
return {"skipped": True, "reason": "trigger not met"}
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
result = self._ccq_worker.run(profile_id)
|
|
493
|
+
return {
|
|
494
|
+
"clusters": result.clusters_processed,
|
|
495
|
+
"blocks": result.blocks_created,
|
|
496
|
+
"archived": result.facts_archived,
|
|
497
|
+
"compression_ratio": result.compression_ratio,
|
|
498
|
+
}
|
|
499
|
+
except Exception as exc:
|
|
500
|
+
logger.warning("CCQ step failed (non-fatal): %s", exc)
|
|
501
|
+
return {"error": str(exc)}
|
|
502
|
+
|
|
466
503
|
# ------------------------------------------------------------------
|
|
467
504
|
# Core Memory Block Storage
|
|
468
505
|
# ------------------------------------------------------------------
|
|
@@ -191,6 +191,9 @@ class MemoryEngine:
|
|
|
191
191
|
behavioral_store=None,
|
|
192
192
|
)
|
|
193
193
|
|
|
194
|
+
# V3.3: Check for embedding model migration on mode switch
|
|
195
|
+
self._check_embedding_migration()
|
|
196
|
+
|
|
194
197
|
self._initialized = True
|
|
195
198
|
logger.info(
|
|
196
199
|
"MemoryEngine initialized: mode=%s profile=%s",
|
|
@@ -320,6 +323,24 @@ class MemoryEngine:
|
|
|
320
323
|
|
|
321
324
|
# -- Internal -----------------------------------------------------------
|
|
322
325
|
|
|
326
|
+
def _check_embedding_migration(self) -> None:
|
|
327
|
+
"""Detect embedding model change and re-index if needed."""
|
|
328
|
+
try:
|
|
329
|
+
from superlocalmemory.storage.embedding_migrator import (
|
|
330
|
+
check_embedding_migration,
|
|
331
|
+
run_embedding_migration,
|
|
332
|
+
)
|
|
333
|
+
if check_embedding_migration(self._config):
|
|
334
|
+
count = run_embedding_migration(
|
|
335
|
+
self._config, self._db, self._embedder,
|
|
336
|
+
)
|
|
337
|
+
if count > 0:
|
|
338
|
+
logger.info(
|
|
339
|
+
"Embedding migration: %d facts re-embedded", count,
|
|
340
|
+
)
|
|
341
|
+
except Exception as exc:
|
|
342
|
+
logger.warning("Embedding migration check failed: %s", exc)
|
|
343
|
+
|
|
323
344
|
def _ensure_init(self) -> None:
|
|
324
345
|
if not self._initialized:
|
|
325
346
|
self.initialize()
|
|
@@ -64,34 +64,52 @@ def init_embedder(config: SLMConfig) -> Any | None:
|
|
|
64
64
|
|
|
65
65
|
Priority order:
|
|
66
66
|
1. Explicit provider in config (ollama / cloud / sentence-transformers)
|
|
67
|
-
2. Auto-detect:
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
2. Auto-detect: Ollama first (lightweight), then sentence-transformers
|
|
68
|
+
subprocess (NEVER in-process for Mode A/B)
|
|
69
|
+
3. If nothing works -> None (BM25-only mode)
|
|
70
|
+
|
|
71
|
+
Memory safety: Mode A/B NEVER load sentence-transformers in-process.
|
|
72
|
+
EmbeddingService uses subprocess isolation — the main process stays
|
|
73
|
+
at ~60MB and never imports torch.
|
|
70
74
|
"""
|
|
71
75
|
from superlocalmemory.core.embeddings import EmbeddingService
|
|
76
|
+
from superlocalmemory.storage.models import Mode
|
|
72
77
|
|
|
73
78
|
emb_cfg = config.embedding
|
|
74
79
|
provider = emb_cfg.provider
|
|
75
80
|
|
|
76
81
|
# --- Explicit ollama provider ---
|
|
77
82
|
if provider == "ollama":
|
|
78
|
-
|
|
83
|
+
result = _try_ollama_embedder(emb_cfg)
|
|
84
|
+
if result is not None:
|
|
85
|
+
return result
|
|
86
|
+
# Mode B explicitly wants Ollama — if unavailable, fall through
|
|
87
|
+
# to subprocess (still safe, never in-process)
|
|
88
|
+
if config.mode == Mode.B:
|
|
89
|
+
logger.warning(
|
|
90
|
+
"Ollama unavailable for Mode B. Falling back to "
|
|
91
|
+
"sentence-transformers subprocess."
|
|
92
|
+
)
|
|
93
|
+
return _try_service_embedder(EmbeddingService, emb_cfg)
|
|
94
|
+
return None
|
|
79
95
|
|
|
80
96
|
# --- Explicit cloud provider ---
|
|
81
97
|
if provider == "cloud" or emb_cfg.is_cloud:
|
|
82
98
|
return _try_service_embedder(EmbeddingService, emb_cfg)
|
|
83
99
|
|
|
84
|
-
# --- Explicit sentence-transformers ---
|
|
100
|
+
# --- Explicit sentence-transformers (subprocess-isolated) ---
|
|
85
101
|
if provider == "sentence-transformers":
|
|
86
102
|
return _try_service_embedder(EmbeddingService, emb_cfg)
|
|
87
103
|
|
|
88
|
-
# --- Auto-detect: try Ollama first (
|
|
104
|
+
# --- Auto-detect: try Ollama first (lightweight, <1s) ---
|
|
89
105
|
ollama_emb = _try_ollama_embedder(emb_cfg)
|
|
90
106
|
if ollama_emb is not None:
|
|
91
107
|
logger.info("Auto-detected Ollama embeddings (fast path)")
|
|
92
108
|
return ollama_emb
|
|
93
109
|
|
|
94
110
|
# --- Fallback: sentence-transformers subprocess ---
|
|
111
|
+
# EmbeddingService ALWAYS uses subprocess isolation (see embeddings.py).
|
|
112
|
+
# The main process never imports torch — safe for Mode A/B.
|
|
95
113
|
return _try_service_embedder(EmbeddingService, emb_cfg)
|
|
96
114
|
|
|
97
115
|
|
|
@@ -358,6 +376,24 @@ def _init_auto_invoker(
|
|
|
358
376
|
# init_retrieval (was MemoryEngine._init_retrieval)
|
|
359
377
|
# ---------------------------------------------------------------------------
|
|
360
378
|
|
|
379
|
+
def _init_hopfield_channel(
|
|
380
|
+
db: DatabaseManager,
|
|
381
|
+
vector_store: Any,
|
|
382
|
+
config: SLMConfig,
|
|
383
|
+
) -> Any | None:
|
|
384
|
+
"""Create HopfieldChannel for Phase G 6th retrieval channel."""
|
|
385
|
+
if not config.hopfield.enabled:
|
|
386
|
+
return None
|
|
387
|
+
try:
|
|
388
|
+
from superlocalmemory.retrieval.hopfield_channel import HopfieldChannel
|
|
389
|
+
return HopfieldChannel(
|
|
390
|
+
db=db, vector_store=vector_store, config=config.hopfield,
|
|
391
|
+
)
|
|
392
|
+
except Exception as exc:
|
|
393
|
+
logger.debug("HopfieldChannel init failed: %s", exc)
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
|
|
361
397
|
def init_retrieval(
|
|
362
398
|
config: SLMConfig,
|
|
363
399
|
db: DatabaseManager,
|
|
@@ -366,7 +402,7 @@ def init_retrieval(
|
|
|
366
402
|
trust_scorer: Any,
|
|
367
403
|
vector_store: Any = None,
|
|
368
404
|
) -> Any:
|
|
369
|
-
"""Create the RetrievalEngine with
|
|
405
|
+
"""Create the RetrievalEngine with 6 channels. Returns it."""
|
|
370
406
|
from superlocalmemory.retrieval.engine import RetrievalEngine
|
|
371
407
|
from superlocalmemory.retrieval.semantic_channel import SemanticChannel
|
|
372
408
|
from superlocalmemory.retrieval.bm25_channel import BM25Channel
|
|
@@ -394,6 +430,11 @@ def init_retrieval(
|
|
|
394
430
|
if sa_channel is not None:
|
|
395
431
|
channels["spreading_activation"] = sa_channel
|
|
396
432
|
|
|
433
|
+
# Phase G: Register Hopfield as 6th channel
|
|
434
|
+
hopfield_channel = _init_hopfield_channel(db, vector_store, config)
|
|
435
|
+
if hopfield_channel is not None:
|
|
436
|
+
channels["hopfield"] = hopfield_channel
|
|
437
|
+
|
|
397
438
|
reranker = None
|
|
398
439
|
if config.retrieval.use_cross_encoder:
|
|
399
440
|
reranker = CrossEncoderReranker(config.retrieval.cross_encoder_model)
|
|
@@ -401,7 +442,7 @@ def init_retrieval(
|
|
|
401
442
|
profile_ch = ProfileChannel(db)
|
|
402
443
|
bridge = BridgeDiscovery(db)
|
|
403
444
|
|
|
404
|
-
|
|
445
|
+
engine = RetrievalEngine(
|
|
405
446
|
db=db, config=config.retrieval, channels=channels,
|
|
406
447
|
embedder=embedder, reranker=reranker,
|
|
407
448
|
base_weights=config.channel_weights,
|
|
@@ -410,6 +451,15 @@ def init_retrieval(
|
|
|
410
451
|
trust_scorer=trust_scorer,
|
|
411
452
|
)
|
|
412
453
|
|
|
454
|
+
# Phase A: Register forgetting filter into the channel registry
|
|
455
|
+
try:
|
|
456
|
+
from superlocalmemory.retrieval.forgetting_filter import register_forgetting_filter
|
|
457
|
+
register_forgetting_filter(engine._registry, db, config.forgetting)
|
|
458
|
+
except Exception as exc:
|
|
459
|
+
logger.debug("Forgetting filter registration failed: %s", exc)
|
|
460
|
+
|
|
461
|
+
return engine
|
|
462
|
+
|
|
413
463
|
|
|
414
464
|
# ---------------------------------------------------------------------------
|
|
415
465
|
# wire_hooks (was MemoryEngine._wire_hooks)
|