superlocalmemory 3.8.7 → 3.8.9
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 +49 -1
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/core/embeddings.py +128 -13
- package/src/superlocalmemory/core/engine.py +6 -1
- package/src/superlocalmemory/core/engine_ingestion.py +5 -0
- package/src/superlocalmemory/core/ingestion_command.py +36 -0
- package/src/superlocalmemory/core/materialization_control.py +20 -0
- package/src/superlocalmemory/core/ollama_embedder.py +5 -0
- package/src/superlocalmemory/core/recall_gate.py +63 -4
- package/src/superlocalmemory/core/recall_pipeline.py +40 -0
- package/src/superlocalmemory/core/store_pipeline.py +10 -0
- package/src/superlocalmemory/encoding/scene_builder.py +105 -15
- package/src/superlocalmemory/mcp/tools_core.py +16 -7
- package/src/superlocalmemory/retrieval/entity_channel.py +201 -56
- package/src/superlocalmemory/retrieval/vector_store.py +238 -123
- package/src/superlocalmemory/server/recall_health.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +106 -16
- package/src/superlocalmemory/storage/embedding_migrator.py +88 -60
|
@@ -10,6 +10,7 @@ with decay. Handles BOTH uppercase and lowercase entity mentions.
|
|
|
10
10
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
11
11
|
License: AGPL-3.0-or-later
|
|
12
12
|
"""
|
|
13
|
+
|
|
13
14
|
from __future__ import annotations
|
|
14
15
|
|
|
15
16
|
import json
|
|
@@ -24,7 +25,10 @@ from superlocalmemory.retrieval.scope_policy import (
|
|
|
24
25
|
authorized_fact_ids,
|
|
25
26
|
filter_authorized_results,
|
|
26
27
|
)
|
|
27
|
-
from superlocalmemory.storage.database import
|
|
28
|
+
from superlocalmemory.storage.database import (
|
|
29
|
+
_scope_where,
|
|
30
|
+
_unbounded_facts_ceiling,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
if TYPE_CHECKING:
|
|
30
34
|
from superlocalmemory.encoding.entity_resolver import EntityResolver
|
|
@@ -50,26 +54,126 @@ def _adj_ttl_seconds() -> float:
|
|
|
50
54
|
except (TypeError, ValueError):
|
|
51
55
|
return 3600.0
|
|
52
56
|
|
|
57
|
+
|
|
53
58
|
_PROPER_NOUN_RE = re.compile(r"\b[A-Z][a-z]{1,}\b")
|
|
54
59
|
|
|
55
|
-
_ENTITY_STOP: frozenset[str] = frozenset(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
_ENTITY_STOP: frozenset[str] = frozenset(
|
|
61
|
+
{
|
|
62
|
+
# Expanded stop list for query entity extraction
|
|
63
|
+
"what",
|
|
64
|
+
"when",
|
|
65
|
+
"where",
|
|
66
|
+
"who",
|
|
67
|
+
"which",
|
|
68
|
+
"how",
|
|
69
|
+
"does",
|
|
70
|
+
"did",
|
|
71
|
+
"the",
|
|
72
|
+
"that",
|
|
73
|
+
"this",
|
|
74
|
+
"there",
|
|
75
|
+
"then",
|
|
76
|
+
"than",
|
|
77
|
+
"they",
|
|
78
|
+
"them",
|
|
79
|
+
"have",
|
|
80
|
+
"has",
|
|
81
|
+
"had",
|
|
82
|
+
"been",
|
|
83
|
+
"being",
|
|
84
|
+
"about",
|
|
85
|
+
"after",
|
|
86
|
+
"before",
|
|
87
|
+
"from",
|
|
88
|
+
"into",
|
|
89
|
+
"with",
|
|
90
|
+
"some",
|
|
91
|
+
"other",
|
|
92
|
+
"would",
|
|
93
|
+
"could",
|
|
94
|
+
"should",
|
|
95
|
+
"will",
|
|
96
|
+
"because",
|
|
97
|
+
"also",
|
|
98
|
+
"just",
|
|
99
|
+
"like",
|
|
100
|
+
"know",
|
|
101
|
+
"think",
|
|
102
|
+
"feel",
|
|
103
|
+
"want",
|
|
104
|
+
"need",
|
|
105
|
+
"make",
|
|
106
|
+
"take",
|
|
107
|
+
"give",
|
|
108
|
+
"tell",
|
|
109
|
+
"said",
|
|
110
|
+
"wow",
|
|
111
|
+
"gonna",
|
|
112
|
+
"got",
|
|
113
|
+
"by",
|
|
114
|
+
"thanks",
|
|
115
|
+
"thank",
|
|
116
|
+
"hey",
|
|
117
|
+
"hi",
|
|
118
|
+
"hello",
|
|
119
|
+
"bye",
|
|
120
|
+
"good",
|
|
121
|
+
"great",
|
|
122
|
+
"nice",
|
|
123
|
+
"cool",
|
|
124
|
+
"right",
|
|
125
|
+
"let",
|
|
126
|
+
"can",
|
|
127
|
+
"might",
|
|
128
|
+
"much",
|
|
129
|
+
"many",
|
|
130
|
+
"more",
|
|
131
|
+
"most",
|
|
132
|
+
"something",
|
|
133
|
+
"anything",
|
|
134
|
+
"everything",
|
|
135
|
+
"nothing",
|
|
136
|
+
"someone",
|
|
137
|
+
"it",
|
|
138
|
+
"my",
|
|
139
|
+
"your",
|
|
140
|
+
"our",
|
|
141
|
+
"their",
|
|
142
|
+
"me",
|
|
143
|
+
"you",
|
|
144
|
+
"we",
|
|
145
|
+
"us",
|
|
146
|
+
"do",
|
|
147
|
+
"if",
|
|
148
|
+
"or",
|
|
149
|
+
"no",
|
|
150
|
+
"to",
|
|
151
|
+
"at",
|
|
152
|
+
"on",
|
|
153
|
+
"in",
|
|
154
|
+
"so",
|
|
155
|
+
"go",
|
|
156
|
+
"come",
|
|
157
|
+
"see",
|
|
158
|
+
"look",
|
|
159
|
+
"say",
|
|
160
|
+
"ask",
|
|
161
|
+
"try",
|
|
162
|
+
"keep",
|
|
163
|
+
"yes",
|
|
164
|
+
"yeah",
|
|
165
|
+
"sure",
|
|
166
|
+
"okay",
|
|
167
|
+
"ok",
|
|
168
|
+
"really",
|
|
169
|
+
"actually",
|
|
170
|
+
"maybe",
|
|
171
|
+
"well",
|
|
172
|
+
"still",
|
|
173
|
+
"even",
|
|
174
|
+
"very",
|
|
175
|
+
}
|
|
176
|
+
)
|
|
73
177
|
|
|
74
178
|
|
|
75
179
|
def extract_query_entities(query: str) -> list[str]:
|
|
@@ -94,10 +198,10 @@ def extract_query_entities(query: str) -> list[str]:
|
|
|
94
198
|
for m in re.finditer(r'"([^"]+)"', query):
|
|
95
199
|
_add(m.group(1).strip())
|
|
96
200
|
# Also extract multi-word capitalized sequences (e.g. "New York", "San Francisco")
|
|
97
|
-
for m in re.finditer(r
|
|
201
|
+
for m in re.finditer(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b", query):
|
|
98
202
|
_add(m.group(1))
|
|
99
203
|
# Extract all-caps abbreviations (e.g. NYU, MIT, UCLA) — min 2 chars
|
|
100
|
-
for m in re.finditer(r
|
|
204
|
+
for m in re.finditer(r"\b([A-Z]{2,})\b", query):
|
|
101
205
|
_add(m.group(1))
|
|
102
206
|
|
|
103
207
|
return candidates
|
|
@@ -113,9 +217,11 @@ class EntityGraphChannel:
|
|
|
113
217
|
"""
|
|
114
218
|
|
|
115
219
|
def __init__(
|
|
116
|
-
self,
|
|
220
|
+
self,
|
|
221
|
+
db: DatabaseManager,
|
|
117
222
|
entity_resolver: EntityResolver | None = None,
|
|
118
|
-
decay: float = 0.7,
|
|
223
|
+
decay: float = 0.7,
|
|
224
|
+
activation_threshold: float = 0.05,
|
|
119
225
|
max_hops: int = 4,
|
|
120
226
|
graph_metrics: dict[str, dict] | None = None,
|
|
121
227
|
cozo_backend: Any = None, # v3.4.5: optional CozoDB backend
|
|
@@ -174,18 +280,19 @@ class EntityGraphChannel:
|
|
|
174
280
|
# the graph without changing the count (e.g. store_edge MAX-merge), and a
|
|
175
281
|
# count-stable window would otherwise serve a stale adjacency map.
|
|
176
282
|
import time as _t_ec
|
|
283
|
+
|
|
177
284
|
_now_ec = _t_ec.monotonic()
|
|
178
285
|
_ttl = _adj_ttl_seconds()
|
|
179
286
|
# TTL=0 disables the time-based reload entirely (count-based correctness
|
|
180
287
|
# reload still applies); otherwise the cache is fresh within the TTL.
|
|
181
|
-
_fresh = _ttl <= 0.0 or (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
288
|
+
_fresh = _ttl <= 0.0 or ((_now_ec - getattr(self, "_adj_loaded_at", 0.0)) < _ttl)
|
|
289
|
+
if (
|
|
290
|
+
self._adj_scope_key == scope_key
|
|
291
|
+
and (self._adj or self._visible_fact_ids)
|
|
292
|
+
and self._adj_edge_count == current_count
|
|
293
|
+
and self._adj_fact_count == current_fact_count
|
|
294
|
+
and _fresh
|
|
295
|
+
):
|
|
189
296
|
return
|
|
190
297
|
adj: dict[str, list[tuple[str, float]]] = defaultdict(list)
|
|
191
298
|
try:
|
|
@@ -195,8 +302,7 @@ class EntityGraphChannel:
|
|
|
195
302
|
include_shared=include_shared,
|
|
196
303
|
)
|
|
197
304
|
rows = self._db.execute(
|
|
198
|
-
"SELECT source_id, target_id, weight FROM graph_edges "
|
|
199
|
-
f"WHERE {where}",
|
|
305
|
+
f"SELECT source_id, target_id, weight FROM graph_edges WHERE {where}",
|
|
200
306
|
(*params,),
|
|
201
307
|
)
|
|
202
308
|
except Exception:
|
|
@@ -234,8 +340,10 @@ class EntityGraphChannel:
|
|
|
234
340
|
|
|
235
341
|
logger.info(
|
|
236
342
|
"Loaded adjacency cache: %d nodes, %d edges, %d entity mappings for profile %s",
|
|
237
|
-
len(self._adj),
|
|
238
|
-
len(self.
|
|
343
|
+
len(self._adj),
|
|
344
|
+
sum(len(v) for v in self._adj.values()) // 2,
|
|
345
|
+
len(self._entity_to_facts),
|
|
346
|
+
profile_id,
|
|
239
347
|
)
|
|
240
348
|
|
|
241
349
|
def _get_edge_count(
|
|
@@ -272,7 +380,11 @@ class EntityGraphChannel:
|
|
|
272
380
|
"""Pre-load entity→fact and fact→entity maps into memory.
|
|
273
381
|
|
|
274
382
|
Eliminates per-entity and per-fact SQL in the spreading activation loop.
|
|
275
|
-
|
|
383
|
+
Fetch only the two columns this index consumes. Loading full AtomicFact
|
|
384
|
+
objects also deserializes every 768-d embedding and Fisher vector; on a
|
|
385
|
+
mature database that turned one new fact into a 5-second recall stall.
|
|
386
|
+
The scope predicate and configurable 50k safety ceiling are identical
|
|
387
|
+
to ``get_all_facts``; only heavyweight unused columns are omitted.
|
|
276
388
|
"""
|
|
277
389
|
# entity_id -> [fact_id, ...]
|
|
278
390
|
self._entity_to_facts: dict[str, list[str]] = defaultdict(list)
|
|
@@ -281,22 +393,41 @@ class EntityGraphChannel:
|
|
|
281
393
|
self._visible_fact_ids = set()
|
|
282
394
|
|
|
283
395
|
try:
|
|
284
|
-
|
|
396
|
+
where, params = _scope_where(
|
|
285
397
|
profile_id,
|
|
286
398
|
include_global=include_global,
|
|
287
399
|
include_shared=include_shared,
|
|
288
400
|
)
|
|
401
|
+
rows = self._db.execute(
|
|
402
|
+
"SELECT fact_id, canonical_entities_json "
|
|
403
|
+
f"FROM atomic_facts WHERE {where} "
|
|
404
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
405
|
+
(*params, _unbounded_facts_ceiling()),
|
|
406
|
+
)
|
|
289
407
|
except Exception:
|
|
290
|
-
|
|
291
|
-
for
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
408
|
+
rows = []
|
|
409
|
+
for row in rows:
|
|
410
|
+
data = dict(row)
|
|
411
|
+
fact_id = str(data.get("fact_id") or "")
|
|
412
|
+
if not fact_id:
|
|
413
|
+
continue
|
|
414
|
+
self._visible_fact_ids.add(fact_id)
|
|
415
|
+
try:
|
|
416
|
+
entity_ids = json.loads(
|
|
417
|
+
data.get("canonical_entities_json") or "[]",
|
|
418
|
+
)
|
|
419
|
+
except (TypeError, ValueError):
|
|
420
|
+
entity_ids = []
|
|
421
|
+
for entity_id in entity_ids:
|
|
422
|
+
if not isinstance(entity_id, str) or not entity_id:
|
|
423
|
+
continue
|
|
424
|
+
self._entity_to_facts[entity_id].append(fact_id)
|
|
425
|
+
self._fact_to_entities[fact_id].append(entity_id)
|
|
296
426
|
|
|
297
427
|
logger.info(
|
|
298
428
|
"Loaded entity maps: %d entities, %d facts with entities",
|
|
299
|
-
len(self._entity_to_facts),
|
|
429
|
+
len(self._entity_to_facts),
|
|
430
|
+
len(self._fact_to_entities),
|
|
300
431
|
)
|
|
301
432
|
|
|
302
433
|
def _load_graph_metrics(self, profile_id: str) -> None:
|
|
@@ -324,7 +455,8 @@ class EntityGraphChannel:
|
|
|
324
455
|
}
|
|
325
456
|
logger.info(
|
|
326
457
|
"Loaded graph metrics: %d facts for profile %s",
|
|
327
|
-
len(self._graph_metrics),
|
|
458
|
+
len(self._graph_metrics),
|
|
459
|
+
profile_id,
|
|
328
460
|
)
|
|
329
461
|
except Exception as exc:
|
|
330
462
|
logger.debug("Graph metrics load failed (graceful degradation): %s", exc)
|
|
@@ -412,7 +544,7 @@ class EntityGraphChannel:
|
|
|
412
544
|
# Spreading activation through graph edges (all in-memory O(1) lookups)
|
|
413
545
|
frontier = set(activation.keys())
|
|
414
546
|
for hop in range(1, self._max_hops):
|
|
415
|
-
hop_decay = self._decay
|
|
547
|
+
hop_decay = self._decay**hop
|
|
416
548
|
if hop_decay < self._threshold:
|
|
417
549
|
break
|
|
418
550
|
next_frontier: set[str] = set()
|
|
@@ -448,9 +580,8 @@ class EntityGraphChannel:
|
|
|
448
580
|
):
|
|
449
581
|
neighbor = edge.target_id if edge.source_id == fid else edge.source_id
|
|
450
582
|
propagated = activation[fid] * self._decay
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
and propagated > activation.get(neighbor, 0.0)
|
|
583
|
+
if propagated >= self._threshold and propagated > activation.get(
|
|
584
|
+
neighbor, 0.0
|
|
454
585
|
):
|
|
455
586
|
activation[neighbor] = propagated
|
|
456
587
|
next_frontier.add(neighbor)
|
|
@@ -490,6 +621,7 @@ class EntityGraphChannel:
|
|
|
490
621
|
# v3.4.1 P2: Community-aware boosting
|
|
491
622
|
if self._graph_metrics and use_cache:
|
|
492
623
|
from collections import Counter as _Counter
|
|
624
|
+
|
|
493
625
|
seed_communities: _Counter = _Counter()
|
|
494
626
|
for eid in canonical_ids:
|
|
495
627
|
for fid in self._entity_to_facts.get(eid, ()):
|
|
@@ -629,7 +761,7 @@ class EntityGraphChannel:
|
|
|
629
761
|
|
|
630
762
|
frontier = set(activation.keys())
|
|
631
763
|
for hop in range(1, self._max_hops):
|
|
632
|
-
hop_decay = self._decay
|
|
764
|
+
hop_decay = self._decay**hop
|
|
633
765
|
if hop_decay < self._threshold:
|
|
634
766
|
break
|
|
635
767
|
next_frontier: set[str] = set()
|
|
@@ -664,6 +796,7 @@ class EntityGraphChannel:
|
|
|
664
796
|
# Community-aware boosting (same as search)
|
|
665
797
|
if self._graph_metrics and use_cache:
|
|
666
798
|
from collections import Counter as _Counter
|
|
799
|
+
|
|
667
800
|
seed_communities: _Counter = _Counter()
|
|
668
801
|
for eid in canonical_ids:
|
|
669
802
|
for fid in self._entity_to_facts.get(eid, ()):
|
|
@@ -691,7 +824,9 @@ class EntityGraphChannel:
|
|
|
691
824
|
return scored
|
|
692
825
|
|
|
693
826
|
def _suppress_contradictions(
|
|
694
|
-
self,
|
|
827
|
+
self,
|
|
828
|
+
activation: dict[str, float],
|
|
829
|
+
profile_id: str,
|
|
695
830
|
) -> None:
|
|
696
831
|
"""P3: Penalize older fact in contradiction pairs, heavy-penalize superseded.
|
|
697
832
|
|
|
@@ -783,7 +918,10 @@ class EntityGraphChannel:
|
|
|
783
918
|
return ids
|
|
784
919
|
|
|
785
920
|
def _discover_entities(
|
|
786
|
-
self,
|
|
921
|
+
self,
|
|
922
|
+
fact_ids: set[str],
|
|
923
|
+
profile_id: str,
|
|
924
|
+
visited: set[str],
|
|
787
925
|
) -> list[str]:
|
|
788
926
|
"""Find new canonical entity IDs referenced by a set of facts."""
|
|
789
927
|
new: list[str] = []
|
|
@@ -797,7 +935,8 @@ class EntityGraphChannel:
|
|
|
797
935
|
)
|
|
798
936
|
for fid in allowed_fact_ids:
|
|
799
937
|
rows = self._db.execute(
|
|
800
|
-
"SELECT canonical_entities_json FROM atomic_facts WHERE fact_id = ?",
|
|
938
|
+
"SELECT canonical_entities_json FROM atomic_facts WHERE fact_id = ?",
|
|
939
|
+
(fid,),
|
|
801
940
|
)
|
|
802
941
|
if not rows:
|
|
803
942
|
continue
|
|
@@ -815,8 +954,11 @@ class EntityGraphChannel:
|
|
|
815
954
|
|
|
816
955
|
# v3.4.5: CozoDB-backed search (Sprint 2)
|
|
817
956
|
def _search_via_cozo(
|
|
818
|
-
self,
|
|
819
|
-
|
|
957
|
+
self,
|
|
958
|
+
query: str,
|
|
959
|
+
raw_entities: list[str],
|
|
960
|
+
profile_id: str,
|
|
961
|
+
top_k: int,
|
|
820
962
|
*,
|
|
821
963
|
include_global: bool = False,
|
|
822
964
|
include_shared: bool = False,
|
|
@@ -879,7 +1021,10 @@ class EntityGraphChannel:
|
|
|
879
1021
|
return self._search_without_cozo(query, profile_id, top_k)
|
|
880
1022
|
|
|
881
1023
|
def _search_without_cozo(
|
|
882
|
-
self,
|
|
1024
|
+
self,
|
|
1025
|
+
query: str,
|
|
1026
|
+
profile_id: str,
|
|
1027
|
+
top_k: int,
|
|
883
1028
|
) -> list[tuple[str, float]]:
|
|
884
1029
|
"""Run canonical SQLite entity recall without recursive projection use."""
|
|
885
1030
|
cozo, self._cozo = self._cozo, None
|