superlocalmemory 3.4.63 → 3.5.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 +124 -0
- package/package.json +1 -1
- package/pyproject.toml +5 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +80 -33
- package/src/superlocalmemory/cli/daemon.py +3 -1
- package/src/superlocalmemory/cli/main.py +1 -1
- package/src/superlocalmemory/core/backend_orchestrator.py +19 -13
- package/src/superlocalmemory/core/config.py +83 -0
- package/src/superlocalmemory/core/injection.py +351 -0
- package/src/superlocalmemory/core/recall_pipeline.py +29 -0
- package/src/superlocalmemory/core/store_pipeline.py +13 -0
- package/src/superlocalmemory/hooks/auto_recall_hook.py +50 -26
- package/src/superlocalmemory/hooks/before_web_hook.py +3 -2
- package/src/superlocalmemory/hooks/user_prompt_hook.py +5 -2
- package/src/superlocalmemory/mcp/tools_active.py +130 -9
- package/src/superlocalmemory/mcp/tools_context.py +18 -4
- package/src/superlocalmemory/retrieval/bm25_channel.py +50 -0
- package/src/superlocalmemory/retrieval/engine.py +43 -9
- package/src/superlocalmemory/retrieval/hopfield_channel.py +22 -9
- package/src/superlocalmemory/retrieval/temporal_channel.py +10 -1
- package/src/superlocalmemory/server/routes/memories.py +2 -2
- package/src/superlocalmemory/server/routes/v3_api.py +40 -25
- package/src/superlocalmemory/server/unified_daemon.py +80 -0
- package/src/superlocalmemory/storage/database.py +47 -0
- package/src/superlocalmemory/storage/migration_runner.py +4 -0
- package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +40 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +1 -0
- package/src/superlocalmemory/storage/models.py +3 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +4 -2
- package/src/superlocalmemory.egg-info/SOURCES.txt +3 -0
- package/src/superlocalmemory.egg-info/requires.txt +4 -1
|
@@ -469,6 +469,36 @@ async def lifespan(application: FastAPI):
|
|
|
469
469
|
_engine = engine
|
|
470
470
|
logger.info("Unified daemon: MemoryEngine initialized (mode=%s)", config.mode.value)
|
|
471
471
|
|
|
472
|
+
# v3.5.0: Backend Orchestrator — CozoDB (graph) + LanceDB (vector) backends.
|
|
473
|
+
# Initialise AFTER engine so the retrieval channels exist to receive backends.
|
|
474
|
+
# Migrates edges/embeddings automatically; fail-soft (non-blocking).
|
|
475
|
+
_cozo_backend = None
|
|
476
|
+
_lancedb_backend = None
|
|
477
|
+
try:
|
|
478
|
+
from superlocalmemory.core.backend_orchestrator import (
|
|
479
|
+
BackendOrchestrator, set_orchestrator,
|
|
480
|
+
)
|
|
481
|
+
orch = BackendOrchestrator(config=config, db=engine._db)
|
|
482
|
+
orch.on_daemon_start()
|
|
483
|
+
set_orchestrator(orch)
|
|
484
|
+
_cozo_backend = orch.get_graph_backend()
|
|
485
|
+
_lancedb_backend = orch.get_vector_backend()
|
|
486
|
+
# Inject CozoDB into entity_graph channel (already has the param).
|
|
487
|
+
re = getattr(engine, '_retrieval_engine', None)
|
|
488
|
+
if re is not None:
|
|
489
|
+
eg = getattr(re, '_entity', None)
|
|
490
|
+
if eg is not None and _cozo_backend is not None:
|
|
491
|
+
try:
|
|
492
|
+
eg._cozo = _cozo_backend
|
|
493
|
+
logger.info("CozoDB backend wired into entity_graph channel")
|
|
494
|
+
except Exception as exc:
|
|
495
|
+
logger.warning("CozoDB channel injection failed: %s", exc)
|
|
496
|
+
logger.info("BackendOrchestrator: ready (cozo=%s, lancedb=%s)",
|
|
497
|
+
"active" if _cozo_backend else "off",
|
|
498
|
+
"active" if _lancedb_backend else "off")
|
|
499
|
+
except Exception as exc:
|
|
500
|
+
logger.warning("BackendOrchestrator init failed (non-fatal): %s", exc)
|
|
501
|
+
|
|
472
502
|
# LLD-07 §4 — deferred migrations (e.g. M006 reward column) need to
|
|
473
503
|
# run AFTER MemoryEngine.initialize() has bootstrapped runtime tables
|
|
474
504
|
# like action_outcomes. Non-fatal by contract.
|
|
@@ -603,8 +633,58 @@ async def lifespan(application: FastAPI):
|
|
|
603
633
|
except Exception as exc:
|
|
604
634
|
logger.warning("Recall warmup failed (non-fatal): %s", exc)
|
|
605
635
|
|
|
636
|
+
def _backfill_vector_store():
|
|
637
|
+
"""v3.5.0: index facts whose embeddings exist in atomic_facts but
|
|
638
|
+
are missing from the sqlite-vec store. Facts stored before dual-write
|
|
639
|
+
was complete were never indexed, so semantic + hopfield only saw a
|
|
640
|
+
fraction of the corpus (observed: 5.8k indexed of 17.2k embedded).
|
|
641
|
+
Idempotent (skips when the store is already complete), non-blocking,
|
|
642
|
+
fail-soft. Runs once after a 3.5.0 upgrade, then no-ops every restart.
|
|
643
|
+
"""
|
|
644
|
+
import time as _t
|
|
645
|
+
from pathlib import Path as _P
|
|
646
|
+
for _ in range(120):
|
|
647
|
+
if _embedding_warm:
|
|
648
|
+
break
|
|
649
|
+
_t.sleep(0.5)
|
|
650
|
+
try:
|
|
651
|
+
from superlocalmemory.retrieval.vector_store import (
|
|
652
|
+
VectorStore, VectorStoreConfig,
|
|
653
|
+
)
|
|
654
|
+
db = engine._db
|
|
655
|
+
db_path = getattr(db, "db_path", None) or getattr(db, "_db_path", None)
|
|
656
|
+
if db_path is None:
|
|
657
|
+
return
|
|
658
|
+
dim = getattr(getattr(config, "embedding", None), "dimension", 768) or 768
|
|
659
|
+
vs = VectorStore(_P(db_path), VectorStoreConfig(dimension=dim))
|
|
660
|
+
if not vs.available:
|
|
661
|
+
return
|
|
662
|
+
try:
|
|
663
|
+
profiles = list(db.list_profiles()) or ["default"]
|
|
664
|
+
except Exception:
|
|
665
|
+
profiles = ["default"]
|
|
666
|
+
for pid in profiles:
|
|
667
|
+
facts = db.get_all_facts(pid)
|
|
668
|
+
with_emb = [
|
|
669
|
+
(f.fact_id, getattr(f, "profile_id", pid) or pid, f.embedding)
|
|
670
|
+
for f in facts
|
|
671
|
+
if getattr(f, "embedding", None) and len(f.embedding) == dim
|
|
672
|
+
]
|
|
673
|
+
if not with_emb:
|
|
674
|
+
continue
|
|
675
|
+
if vs.count(pid) >= int(len(with_emb) * 0.98):
|
|
676
|
+
continue # already complete — no-op
|
|
677
|
+
n = vs.rebuild_from_facts(with_emb)
|
|
678
|
+
logger.info(
|
|
679
|
+
"VS backfill[%s]: indexed %d of %d embedded facts",
|
|
680
|
+
pid, n, len(with_emb),
|
|
681
|
+
)
|
|
682
|
+
except Exception as exc:
|
|
683
|
+
logger.warning("Vector store backfill failed (non-fatal): %s", exc)
|
|
684
|
+
|
|
606
685
|
threading.Thread(target=_warmup_embedder, daemon=True, name="embed-warmup").start()
|
|
607
686
|
threading.Thread(target=_warmup_recall, daemon=True, name="recall-warmup").start()
|
|
687
|
+
threading.Thread(target=_backfill_vector_store, daemon=True, name="vs-backfill").start()
|
|
608
688
|
|
|
609
689
|
# v3.4.37: QueueConsumer uses daemon's engine directly via adapter.
|
|
610
690
|
# Previously routed through WorkerPool → recall_worker subprocess,
|
|
@@ -239,9 +239,26 @@ class DatabaseManager:
|
|
|
239
239
|
emotional_valence=d.get("emotional_valence", 0.0),
|
|
240
240
|
emotional_arousal=d.get("emotional_arousal", 0.0),
|
|
241
241
|
signal_type=SignalType(d["signal_type"]) if d.get("signal_type") else SignalType.FACTUAL,
|
|
242
|
+
pinned=bool(d.get("pinned", 0)),
|
|
242
243
|
created_at=d["created_at"],
|
|
243
244
|
)
|
|
244
245
|
|
|
246
|
+
def set_pinned(self, fact_id: str, pinned: bool) -> None:
|
|
247
|
+
"""Set or clear the pinned flag on a fact (v3.4.65 core-memory)."""
|
|
248
|
+
self.execute(
|
|
249
|
+
"UPDATE atomic_facts SET pinned = ? WHERE fact_id = ?",
|
|
250
|
+
(1 if pinned else 0, fact_id),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def get_pinned(self, profile_id: str) -> list[AtomicFact]:
|
|
254
|
+
"""Return all pinned facts for a profile, highest-importance first."""
|
|
255
|
+
rows = self.execute(
|
|
256
|
+
"SELECT * FROM atomic_facts WHERE profile_id = ? AND pinned = 1 "
|
|
257
|
+
"ORDER BY importance DESC",
|
|
258
|
+
(profile_id,),
|
|
259
|
+
)
|
|
260
|
+
return [self._row_to_fact(r) for r in rows]
|
|
261
|
+
|
|
245
262
|
def get_all_facts(self, profile_id: str) -> list[AtomicFact]:
|
|
246
263
|
"""All facts for a profile, newest first."""
|
|
247
264
|
rows = self.execute(
|
|
@@ -606,6 +623,36 @@ class DatabaseManager:
|
|
|
606
623
|
)
|
|
607
624
|
return [self._row_to_scene(r) for r in rows]
|
|
608
625
|
|
|
626
|
+
def get_scenes_for_facts_batch(
|
|
627
|
+
self, fact_ids: list[str], profile_id: str,
|
|
628
|
+
) -> dict[str, list[MemoryScene]]:
|
|
629
|
+
"""v3.5.0: batch scene lookup — one query replaces N individual LIKE scans.
|
|
630
|
+
|
|
631
|
+
The 20 individual ``get_scenes_for_fact`` calls in the retrieval engine's
|
|
632
|
+
scene expansion path were the single largest recall latency source (~5.7s).
|
|
633
|
+
This replaces them with a single multi-LIKE OR query. Returns
|
|
634
|
+
``{fact_id: [scenes]}`` for facts that belong to at least one scene.
|
|
635
|
+
"""
|
|
636
|
+
if not fact_ids:
|
|
637
|
+
return {}
|
|
638
|
+
clauses = " OR ".join(
|
|
639
|
+
'(profile_id = ? AND fact_ids_json LIKE ?)' for _ in fact_ids
|
|
640
|
+
)
|
|
641
|
+
params: list[str] = []
|
|
642
|
+
for fid in fact_ids:
|
|
643
|
+
params.extend((profile_id, f'%"{fid}"%'))
|
|
644
|
+
rows = self.execute(
|
|
645
|
+
f"SELECT * FROM memory_scenes WHERE {clauses} ORDER BY last_updated DESC",
|
|
646
|
+
tuple(params),
|
|
647
|
+
)
|
|
648
|
+
out: dict[str, list[MemoryScene]] = {}
|
|
649
|
+
for r in rows:
|
|
650
|
+
scene = self._row_to_scene(r)
|
|
651
|
+
for fid in fact_ids:
|
|
652
|
+
if fid in (scene.fact_ids or []):
|
|
653
|
+
out.setdefault(fid, []).append(scene)
|
|
654
|
+
return out
|
|
655
|
+
|
|
609
656
|
def increment_entity_fact_count(self, entity_id: str) -> None:
|
|
610
657
|
"""Atomically increment fact_count for a canonical entity."""
|
|
611
658
|
self.execute(
|
|
@@ -50,6 +50,7 @@ from superlocalmemory.storage.migrations import (
|
|
|
50
50
|
M012_shadow_observations as _M012,
|
|
51
51
|
M013_bi_temporal_columns as _M013,
|
|
52
52
|
M014_v345_scale_ready as _M014,
|
|
53
|
+
M015_add_pinned_column as _M015,
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
# Map migration name → module (used for the optional ``verify(conn)`` hook
|
|
@@ -69,6 +70,7 @@ _MODULES = {
|
|
|
69
70
|
_M012.NAME: _M012,
|
|
70
71
|
_M013.NAME: _M013,
|
|
71
72
|
_M014.NAME: _M014,
|
|
73
|
+
_M015.NAME: _M015,
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
logger = logging.getLogger(__name__)
|
|
@@ -130,6 +132,8 @@ DEFERRED_MIGRATIONS: list[Migration] = [
|
|
|
130
132
|
# as M011.
|
|
131
133
|
Migration(name=_M013.NAME, db_target="memory", ddl=_M013.DDL),
|
|
132
134
|
Migration(name=_M014.NAME, db_target="memory", ddl=_M014.DDL),
|
|
135
|
+
# M015 adds pinned column to atomic_facts (v3.4.65 core-memory pins).
|
|
136
|
+
Migration(name=_M015.NAME, db_target="memory", ddl=_M015.DDL),
|
|
133
137
|
]
|
|
134
138
|
|
|
135
139
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory v3.4.65 — Context Injection v2
|
|
4
|
+
|
|
5
|
+
"""M015 — add `pinned` column to atomic_facts (v3.4.65, core-memory explicit pins).
|
|
6
|
+
|
|
7
|
+
Additive only — ALTER TABLE ADD COLUMN, default 0. No data loss, no type
|
|
8
|
+
changes. Idempotent via verify() + migration_log. Mirrors M001 pattern.
|
|
9
|
+
|
|
10
|
+
Backward-compat: old code that doesn't know about the column simply gets
|
|
11
|
+
DEFAULT 0 on SELECT *, which is the safe "not-pinned" state.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sqlite3
|
|
17
|
+
|
|
18
|
+
NAME = "M015_add_pinned_column"
|
|
19
|
+
DB_TARGET = "memory"
|
|
20
|
+
|
|
21
|
+
_REQUIRED_COLS = frozenset({"pinned"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
cols = {r[1] for r in conn.execute(
|
|
27
|
+
"PRAGMA table_info(atomic_facts)"
|
|
28
|
+
).fetchall()}
|
|
29
|
+
except sqlite3.Error:
|
|
30
|
+
return False
|
|
31
|
+
return _REQUIRED_COLS <= cols
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DDL = """
|
|
35
|
+
BEGIN IMMEDIATE;
|
|
36
|
+
ALTER TABLE atomic_facts ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_facts_pinned
|
|
38
|
+
ON atomic_facts(profile_id, pinned);
|
|
39
|
+
COMMIT;
|
|
40
|
+
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superlocalmemory
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Information-geometric agent memory with mathematical guarantees
|
|
5
5
|
Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -69,6 +69,8 @@ Provides-Extra: ui
|
|
|
69
69
|
Requires-Dist: fastapi[all]>=0.135.1; extra == "ui"
|
|
70
70
|
Requires-Dist: uvicorn>=0.42.0; extra == "ui"
|
|
71
71
|
Requires-Dist: python-multipart<1.0.0,>=0.0.6; extra == "ui"
|
|
72
|
+
Provides-Extra: injection
|
|
73
|
+
Requires-Dist: tiktoken>=0.8.0; extra == "injection"
|
|
72
74
|
Provides-Extra: learning
|
|
73
75
|
Requires-Dist: lightgbm>=4.0.0; extra == "learning"
|
|
74
76
|
Provides-Extra: performance
|
|
@@ -79,7 +81,7 @@ Requires-Dist: google-auth-oauthlib>=1.2.0; extra == "ingestion"
|
|
|
79
81
|
Requires-Dist: google-api-python-client>=2.100.0; extra == "ingestion"
|
|
80
82
|
Requires-Dist: icalendar>=6.0.0; extra == "ingestion"
|
|
81
83
|
Provides-Extra: full
|
|
82
|
-
Requires-Dist: superlocalmemory[ingestion,learning,performance,search,ui]; extra == "full"
|
|
84
|
+
Requires-Dist: superlocalmemory[ingestion,injection,learning,performance,search,ui]; extra == "full"
|
|
83
85
|
Provides-Extra: dev
|
|
84
86
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
85
87
|
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
@@ -86,6 +86,7 @@ src/superlocalmemory/core/graph_analyzer.py
|
|
|
86
86
|
src/superlocalmemory/core/graph_pruner.py
|
|
87
87
|
src/superlocalmemory/core/health_monitor.py
|
|
88
88
|
src/superlocalmemory/core/hooks.py
|
|
89
|
+
src/superlocalmemory/core/injection.py
|
|
89
90
|
src/superlocalmemory/core/loop_watchdog.py
|
|
90
91
|
src/superlocalmemory/core/maintenance.py
|
|
91
92
|
src/superlocalmemory/core/maintenance_scheduler.py
|
|
@@ -377,6 +378,7 @@ src/superlocalmemory/storage/migrations/M011_archive_and_merge.py
|
|
|
377
378
|
src/superlocalmemory/storage/migrations/M012_shadow_observations.py
|
|
378
379
|
src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py
|
|
379
380
|
src/superlocalmemory/storage/migrations/M014_v345_scale_ready.py
|
|
381
|
+
src/superlocalmemory/storage/migrations/M015_add_pinned_column.py
|
|
380
382
|
src/superlocalmemory/storage/migrations/__init__.py
|
|
381
383
|
src/superlocalmemory/trust/__init__.py
|
|
382
384
|
src/superlocalmemory/trust/gate.py
|
|
@@ -452,6 +454,7 @@ tests/test_final_locomo_mini.py
|
|
|
452
454
|
tests/test_hook_handlers.py
|
|
453
455
|
tests/test_ide_connector.py
|
|
454
456
|
tests/test_infra.py
|
|
457
|
+
tests/test_injection.py
|
|
455
458
|
tests/test_learning_advanced.py
|
|
456
459
|
tests/test_learning_collectors.py
|
|
457
460
|
tests/test_llm_provider.py
|
|
@@ -36,7 +36,7 @@ pytest-asyncio>=0.21
|
|
|
36
36
|
sqlite-vec>=0.1.6
|
|
37
37
|
|
|
38
38
|
[full]
|
|
39
|
-
superlocalmemory[ingestion,learning,performance,search,ui]
|
|
39
|
+
superlocalmemory[ingestion,injection,learning,performance,search,ui]
|
|
40
40
|
|
|
41
41
|
[ingestion]
|
|
42
42
|
keyring>=25.0.0
|
|
@@ -44,6 +44,9 @@ google-auth-oauthlib>=1.2.0
|
|
|
44
44
|
google-api-python-client>=2.100.0
|
|
45
45
|
icalendar>=6.0.0
|
|
46
46
|
|
|
47
|
+
[injection]
|
|
48
|
+
tiktoken>=0.8.0
|
|
49
|
+
|
|
47
50
|
[learning]
|
|
48
51
|
lightgbm>=4.0.0
|
|
49
52
|
|