superlocalmemory 3.5.4 → 3.5.5
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 +21 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/core/engine.py +85 -0
- package/src/superlocalmemory/mcp/tools_core.py +31 -13
- package/src/superlocalmemory/server/unified_daemon.py +41 -5
- package/src/superlocalmemory.egg-info/PKG-INFO +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.5.5] - 2026-05-31 — Write-Through Remember (instant cross-session recall)
|
|
9
|
+
|
|
10
|
+
### Fixed (CRITICAL — closes the remember→recall window)
|
|
11
|
+
- **Write-through store** (`engine.store_fast`): `remember` now does a synchronous
|
|
12
|
+
verbatim insert (memory + atomic_fact, FTS5 auto-populated via trigger) **plus a
|
|
13
|
+
single ~22ms embedding**, so a stored memory is **recallable at rank #1 within
|
|
14
|
+
~240ms** — across MCP, CLI, and Dashboard. Previously memories went to `pending.db`
|
|
15
|
+
and were unrecallable for 1–180s until the async materializer caught up; a
|
|
16
|
+
parallel/next agent recalling a just-stored memory would miss it.
|
|
17
|
+
- Slow enrichment (LLM fact-extraction, graph edges, entity resolution) stays async
|
|
18
|
+
in the materializer — only the fast path (verbatim + embedding) is synchronous.
|
|
19
|
+
- Materializer now **enriches** the write-through verbatim fact in place (adds graph/
|
|
20
|
+
entities) instead of skipping it as a duplicate.
|
|
21
|
+
- MCP `remember` routes through the daemon's write-through `/remember`; falls back to
|
|
22
|
+
`pending.db` only when the daemon is offline.
|
|
23
|
+
- CLI `slm remember` and Dashboard already route through the daemon → same write-through.
|
|
24
|
+
|
|
25
|
+
### Tests
|
|
26
|
+
- `test_mcp_remember_tool.py`: updated for write-through (daemon-online → fact_ids;
|
|
27
|
+
daemon-offline → pending fallback) + new write-through path test. Suite: 4,489 passed.
|
|
28
|
+
|
|
8
29
|
## [3.5.0] - 2026-05-31 — Backend Migration + Recall Performance + Context Injection
|
|
9
30
|
|
|
10
31
|
### Perf (recall 13.6s → <1s warm)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.5",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -369,6 +369,91 @@ class MemoryEngine:
|
|
|
369
369
|
vector_store=self._vector_store,
|
|
370
370
|
)
|
|
371
371
|
|
|
372
|
+
def store_fast(self, content: str, metadata: dict[str, Any] | None = None) -> list[str]:
|
|
373
|
+
"""v3.5.5 WRITE-THROUGH: synchronous verbatim insert for IMMEDIATE recall.
|
|
374
|
+
|
|
375
|
+
Full ``store()`` blocks 30-180s on LLM fact-extraction + Ollama embedding
|
|
376
|
+
+ graph building. That created a recall window: a memory stored via the
|
|
377
|
+
async path sat in pending.db, unrecallable, until the background
|
|
378
|
+
materializer caught up. An agent storing a decision then immediately
|
|
379
|
+
recalling it (same session, or a parallel/next session) would miss it.
|
|
380
|
+
|
|
381
|
+
store_fast inserts a verbatim AtomicFact (+ memory row) synchronously.
|
|
382
|
+
The FTS5 ``atomic_facts_fts`` trigger auto-populates on INSERT, so the
|
|
383
|
+
memory is **keyword/BM25-recallable the instant this returns** (~ms, no
|
|
384
|
+
LLM, no embedding). Embedding + entities + graph are enriched async by
|
|
385
|
+
the materializer (which detects facts with NULL embedding).
|
|
386
|
+
|
|
387
|
+
Returns real fact_ids immediately. Quality gate rejects template junk.
|
|
388
|
+
"""
|
|
389
|
+
self._require_full("store_fast")
|
|
390
|
+
self._ensure_init()
|
|
391
|
+
import re as _re
|
|
392
|
+
import uuid as _uuid
|
|
393
|
+
from datetime import datetime, timezone
|
|
394
|
+
from superlocalmemory.storage.models import (
|
|
395
|
+
AtomicFact, FactType, MemoryRecord,
|
|
396
|
+
)
|
|
397
|
+
if not content or not content.strip():
|
|
398
|
+
return []
|
|
399
|
+
try:
|
|
400
|
+
from superlocalmemory.core.injection import is_low_quality
|
|
401
|
+
if is_low_quality(content):
|
|
402
|
+
return []
|
|
403
|
+
except Exception:
|
|
404
|
+
pass
|
|
405
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
406
|
+
record = MemoryRecord(
|
|
407
|
+
profile_id=self._profile_id, content=content,
|
|
408
|
+
session_date=now[:10], metadata=metadata or {},
|
|
409
|
+
)
|
|
410
|
+
self._db.store_memory(record)
|
|
411
|
+
# Lightweight regex entities (matches store_pipeline verbatim path) so
|
|
412
|
+
# the entity_graph channel has something to work with before enrichment.
|
|
413
|
+
ents = sorted(
|
|
414
|
+
{m.group(1) for m in _re.finditer(
|
|
415
|
+
r"\b([A-Z][a-z]+(?:\s[A-Z][a-z]+){0,3})\b", content)}
|
|
416
|
+
| {m.group(1) for m in _re.finditer(r"\b([A-Z]{2,})\b", content)}
|
|
417
|
+
)
|
|
418
|
+
# v3.5.5: compute the embedding SYNCHRONOUSLY. A single warm embed is
|
|
419
|
+
# ~22ms (the 30-180s of full store() was LLM fact-extraction + graph,
|
|
420
|
+
# NOT embedding). With the embedding present, the semantic channel
|
|
421
|
+
# scores this fact correctly so it ranks properly IMMEDIATELY — not
|
|
422
|
+
# just keyword-findable but top-ranked. Graph/entity enrichment stays
|
|
423
|
+
# async. Embed failure → fact still inserted (keyword-recallable).
|
|
424
|
+
emb = None
|
|
425
|
+
fmean = fvar = None
|
|
426
|
+
try:
|
|
427
|
+
emb = self._embedder.embed(content) if self._embedder else None
|
|
428
|
+
if emb:
|
|
429
|
+
fmean, fvar = self._embedder.compute_fisher_params(emb)
|
|
430
|
+
except Exception:
|
|
431
|
+
emb = None
|
|
432
|
+
fact = AtomicFact(
|
|
433
|
+
fact_id=_uuid.uuid4().hex[:16], memory_id=record.memory_id,
|
|
434
|
+
profile_id=self._profile_id, content=content,
|
|
435
|
+
fact_type=FactType.EPISODIC, entities=ents,
|
|
436
|
+
observation_date=now[:10], confidence=0.7, importance=0.5,
|
|
437
|
+
embedding=emb, fisher_mean=fmean, fisher_variance=fvar,
|
|
438
|
+
created_at=now,
|
|
439
|
+
)
|
|
440
|
+
self._db.store_fact(fact) # FTS5 trigger → immediately BM25-recallable
|
|
441
|
+
# Upsert to vector store so the semantic channel finds it now.
|
|
442
|
+
try:
|
|
443
|
+
vs = getattr(self, "_vector_store", None)
|
|
444
|
+
if emb and vs and getattr(vs, "available", False):
|
|
445
|
+
vs.upsert(fact.fact_id, self._profile_id, emb)
|
|
446
|
+
except Exception:
|
|
447
|
+
pass
|
|
448
|
+
# Persist BM25 tokens too (covers the in-memory rank_bm25 fallback path).
|
|
449
|
+
try:
|
|
450
|
+
bm25 = getattr(self._retrieval_engine, "_bm25", None)
|
|
451
|
+
if bm25:
|
|
452
|
+
bm25.add(fact.fact_id, content, self._profile_id)
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
return [fact.fact_id]
|
|
456
|
+
|
|
372
457
|
# -- Recall operations --------------------------------------------------
|
|
373
458
|
|
|
374
459
|
def recall(
|
|
@@ -110,27 +110,45 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
110
110
|
Extracts atomic facts, resolves entities, builds graph edges,
|
|
111
111
|
and indexes for 4-channel retrieval.
|
|
112
112
|
"""
|
|
113
|
+
meta = {
|
|
114
|
+
"project": project,
|
|
115
|
+
"importance": importance,
|
|
116
|
+
"agent_id": agent_id,
|
|
117
|
+
"session_id": session_id,
|
|
118
|
+
}
|
|
119
|
+
# v3.5.5 WRITE-THROUGH: route through the daemon's /remember, which does
|
|
120
|
+
# a synchronous verbatim insert (memory is keyword/BM25-recallable the
|
|
121
|
+
# instant this returns) and enqueues async enrichment. This closes the
|
|
122
|
+
# recall window so a parallel/next agent finds memories saved seconds ago.
|
|
123
|
+
# Falls back to pending.db only if the daemon is unreachable.
|
|
113
124
|
try:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
126
|
+
if is_daemon_running():
|
|
127
|
+
resp = daemon_request("POST", "/remember", {
|
|
128
|
+
"content": content, "tags": tags, "metadata": meta,
|
|
129
|
+
})
|
|
130
|
+
if resp and (resp.get("fact_ids") is not None or resp.get("ok")):
|
|
131
|
+
fids = resp.get("fact_ids") or []
|
|
132
|
+
return {
|
|
133
|
+
"success": True,
|
|
134
|
+
"fact_ids": fids or [f"pending:{resp.get('pending_id','')}"],
|
|
135
|
+
"count": len(fids) if fids else 1,
|
|
136
|
+
"pending": not fids,
|
|
137
|
+
"message": "Stored (recallable now; enriching async).",
|
|
138
|
+
}
|
|
139
|
+
except Exception as dexc:
|
|
140
|
+
logger.debug("MCP remember via daemon failed, pending fallback: %s", dexc)
|
|
126
141
|
|
|
142
|
+
try:
|
|
143
|
+
from superlocalmemory.cli.pending_store import store_pending
|
|
144
|
+
pending_id = store_pending(content, tags=tags, metadata=meta)
|
|
127
145
|
return {
|
|
128
146
|
"success": True,
|
|
129
147
|
"fact_ids": [f"pending:{pending_id}"],
|
|
130
148
|
"count": 1,
|
|
131
149
|
"pending": True,
|
|
132
150
|
"pending_id": pending_id,
|
|
133
|
-
"message": "Stored — facts will appear
|
|
151
|
+
"message": "Stored — facts will appear shortly (daemon offline).",
|
|
134
152
|
}
|
|
135
153
|
except Exception as exc:
|
|
136
154
|
logger.exception("remember failed")
|
|
@@ -1582,14 +1582,28 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1582
1582
|
extra = getattr(req, "metadata", None)
|
|
1583
1583
|
if isinstance(extra, dict):
|
|
1584
1584
|
meta.update(extra)
|
|
1585
|
+
# v3.5.5 WRITE-THROUGH: synchronous verbatim insert → the memory is
|
|
1586
|
+
# keyword/BM25-recallable the instant this returns (~ms). Closes the
|
|
1587
|
+
# recall window so a parallel/next agent finds memories saved seconds
|
|
1588
|
+
# ago. Embedding/graph enrichment is deferred to the materializer.
|
|
1589
|
+
fact_ids: list[str] = []
|
|
1590
|
+
try:
|
|
1591
|
+
fact_ids = engine.store_fast(req.content, metadata=meta)
|
|
1592
|
+
except Exception as fexc:
|
|
1593
|
+
logger.warning("store_fast failed, falling back to pending-only: %s", fexc)
|
|
1594
|
+
# Enqueue for async enrichment (embedding + entities + graph). The
|
|
1595
|
+
# materializer detects the already-inserted verbatim fact and enriches
|
|
1596
|
+
# it in place rather than duplicating.
|
|
1585
1597
|
pending_id = store_pending(
|
|
1586
1598
|
req.content, tags=req.tags or "", metadata=meta,
|
|
1587
1599
|
)
|
|
1588
1600
|
return {
|
|
1589
1601
|
"ok": True,
|
|
1602
|
+
"fact_ids": fact_ids,
|
|
1603
|
+
"count": len(fact_ids),
|
|
1590
1604
|
"pending_id": pending_id,
|
|
1591
|
-
"status": "queued",
|
|
1592
|
-
"note": "
|
|
1605
|
+
"status": "stored" if fact_ids else "queued",
|
|
1606
|
+
"note": "write-through: recallable now; enriching async",
|
|
1593
1607
|
}
|
|
1594
1608
|
except Exception as exc:
|
|
1595
1609
|
raise HTTPException(500, detail=str(exc))
|
|
@@ -1816,14 +1830,36 @@ def _start_pending_materializer() -> None:
|
|
|
1816
1830
|
try:
|
|
1817
1831
|
import hashlib
|
|
1818
1832
|
content = item["content"]
|
|
1819
|
-
# Dedup: skip if identical content already stored.
|
|
1820
1833
|
content_hash = hashlib.md5(content.encode()).hexdigest()
|
|
1834
|
+
# v3.5.5: the write-through path already inserted a
|
|
1835
|
+
# verbatim fact (recallable via BM25). If it lacks an
|
|
1836
|
+
# embedding, ENRICH it in place (compute embedding +
|
|
1837
|
+
# upsert vector store) rather than skipping — otherwise
|
|
1838
|
+
# the fact would never be semantically searchable.
|
|
1821
1839
|
dup = engine._db.execute(
|
|
1822
|
-
"SELECT
|
|
1823
|
-
"content = ? LIMIT 1",
|
|
1840
|
+
"SELECT fact_id, embedding FROM atomic_facts "
|
|
1841
|
+
"WHERE content = ? LIMIT 1",
|
|
1824
1842
|
(content,),
|
|
1825
1843
|
)
|
|
1826
1844
|
if dup:
|
|
1845
|
+
try:
|
|
1846
|
+
row = dict(dup[0])
|
|
1847
|
+
if not row.get("embedding") and engine._embedder:
|
|
1848
|
+
emb = engine._embedder.embed(content)
|
|
1849
|
+
if emb:
|
|
1850
|
+
upd = {"embedding": emb}
|
|
1851
|
+
try:
|
|
1852
|
+
fm, fv = engine._embedder.compute_fisher_params(emb)
|
|
1853
|
+
upd["fisher_mean"] = fm
|
|
1854
|
+
upd["fisher_variance"] = fv
|
|
1855
|
+
except Exception:
|
|
1856
|
+
pass
|
|
1857
|
+
engine._db.update_fact(row["fact_id"], upd)
|
|
1858
|
+
vs = getattr(engine, "_vector_store", None)
|
|
1859
|
+
if vs and getattr(vs, "available", False):
|
|
1860
|
+
vs.upsert(row["fact_id"], engine._profile_id, emb)
|
|
1861
|
+
except Exception as eexc:
|
|
1862
|
+
logger.debug("enrichment of write-through fact failed: %s", eexc)
|
|
1827
1863
|
mark_done(item["id"])
|
|
1828
1864
|
continue
|
|
1829
1865
|
import json as _json
|