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.
- 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 +276 -4
- 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/strategy.py +16 -6
- 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.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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)
|