superlocalmemory 3.2.3 → 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 (51) 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 +276 -4
  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/strategy.py +16 -6
  42. package/src/superlocalmemory/server/routes/agents.py +68 -8
  43. package/src/superlocalmemory/server/routes/learning.py +18 -1
  44. package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
  45. package/src/superlocalmemory/server/routes/v3_api.py +503 -1
  46. package/src/superlocalmemory/storage/database.py +206 -0
  47. package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
  48. package/src/superlocalmemory/storage/migration_v33.py +140 -0
  49. package/src/superlocalmemory/storage/quantized_store.py +261 -0
  50. package/src/superlocalmemory/storage/schema_v32.py +137 -0
  51. 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.
@@ -233,6 +238,224 @@ 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).
@@ -340,6 +563,15 @@ class SLMConfig:
340
563
  consolidation: ConsolidationConfig = field(
341
564
  default_factory=ConsolidationConfig,
342
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
+ )
343
575
 
344
576
  def __post_init__(self) -> None:
345
577
  if self.db_path is None:
@@ -368,6 +600,22 @@ class SLMConfig:
368
600
  embedding_deployment=emb_data.get("deployment_name", ""),
369
601
  )
370
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
+
371
619
  return config
372
620
 
373
621
  def save(self, config_path: Path | None = None) -> None:
@@ -375,6 +623,14 @@ class SLMConfig:
375
623
  import json
376
624
  path = config_path or (self.base_dir / "config.json")
377
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
+
378
634
  data = {
379
635
  "mode": self.mode.value,
380
636
  "active_profile": self.active_profile,
@@ -392,7 +648,16 @@ class SLMConfig:
392
648
  "api_key": self.embedding.api_key,
393
649
  "deployment_name": self.embedding.deployment_name,
394
650
  },
651
+ "retrieval": {
652
+ "use_cross_encoder": self.retrieval.use_cross_encoder,
653
+ },
395
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
+
396
661
  path.write_text(json.dumps(data, indent=2))
397
662
 
398
663
  @staticmethod
@@ -455,11 +720,13 @@ class SLMConfig:
455
720
  embedding=EmbeddingConfig(
456
721
  model_name="nomic-ai/nomic-embed-text-v1.5",
457
722
  dimension=768,
458
- provider=embedding_provider,
723
+ # Mode A: sentence-transformers in SUBPROCESS (never in-process)
724
+ provider=embedding_provider or "sentence-transformers",
459
725
  ),
460
726
  llm=LLMConfig(), # No LLM
461
727
  retrieval=RetrievalConfig(
462
- use_cross_encoder=True,
728
+ # Mode A: no cross-encoder (saves ~1.5GB PyTorch RAM)
729
+ use_cross_encoder=False,
463
730
  ),
464
731
  math=MathConfig(
465
732
  sheaf_contradiction_threshold=0.45, # 768d threshold
@@ -473,7 +740,8 @@ class SLMConfig:
473
740
  embedding=EmbeddingConfig(
474
741
  model_name="nomic-ai/nomic-embed-text-v1.5",
475
742
  dimension=768,
476
- provider=embedding_provider,
743
+ # Mode B: Ollama HTTP API (zero PyTorch in-process)
744
+ provider=embedding_provider or "ollama",
477
745
  ),
478
746
  llm=LLMConfig(
479
747
  provider=llm_provider or "ollama",
@@ -481,7 +749,10 @@ class SLMConfig:
481
749
  api_base=llm_api_base or "http://localhost:11434",
482
750
  api_key=llm_api_key or "",
483
751
  ),
484
- retrieval=RetrievalConfig(use_cross_encoder=True),
752
+ retrieval=RetrievalConfig(
753
+ # Mode B: no cross-encoder (saves ~1.5GB PyTorch RAM)
754
+ use_cross_encoder=False,
755
+ ),
485
756
  )
486
757
 
487
758
  # Mode C — FULL POWER, UNRESTRICTED
@@ -507,6 +778,7 @@ class SLMConfig:
507
778
  entity_graph=1.3,
508
779
  temporal=1.0,
509
780
  spreading_activation=1.2, # Phase 3: SA boost in Mode C
781
+ hopfield=1.0, # Phase G: Hopfield in Mode C
510
782
  ),
511
783
  retrieval=RetrievalConfig(
512
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: if Ollama has embedding model -> use it
68
- 3. Fallback to sentence-transformers subprocess
69
- 4. If nothing works -> None (BM25-only mode)
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
- return _try_ollama_embedder(emb_cfg)
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 (fast path, <1s) ---
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 5 channels. Returns it."""
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
- return RetrievalEngine(
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)