superlocalmemory 3.6.14 → 3.6.15
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 +28 -0
- package/README.md +5 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +5 -4
- package/plugin/agents/slm-memory-advisor.md +5 -4
- 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-graph/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +10 -2
- package/plugin/skills/slm-remember/SKILL.md +14 -2
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +5 -4
- package/plugin-src/agents/slm-optimize-advisor.md +1 -1
- package/plugin-src/commands/slm-optimize.md +1 -1
- package/plugin-src/commands/slm-recall.md +1 -1
- package/plugin-src/commands/slm-remember.md +2 -2
- package/plugin-src/commands/slm-status.md +1 -1
- package/plugin-src/manifest.json +1 -1
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +5 -4
- package/plugin-src/rules/CLAUDE.md.fragment +5 -4
- 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 +10 -2
- package/plugin-src/skills/slm-remember/SKILL.md +14 -2
- 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/scripts/build-plugin.js +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +91 -2
- package/src/superlocalmemory/cli/main.py +45 -0
- package/src/superlocalmemory/cli/setup_wizard.py +27 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
- package/src/superlocalmemory/core/config.py +115 -0
- package/src/superlocalmemory/core/engine.py +74 -3
- package/src/superlocalmemory/core/fact_consolidator.py +20 -3
- package/src/superlocalmemory/core/platform_utils.py +8 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/recall_worker.py +7 -0
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/core/worker_pool.py +14 -2
- package/src/superlocalmemory/hooks/session_registry.py +8 -4
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
- package/src/superlocalmemory/mcp/tools_core.py +25 -0
- package/src/superlocalmemory/mcp/tools_v3.py +6 -1
- package/src/superlocalmemory/mcp/tools_v33.py +8 -4
- package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
- package/src/superlocalmemory/retrieval/engine.py +36 -3
- package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
- package/src/superlocalmemory/server/unified_daemon.py +132 -10
- package/src/superlocalmemory/storage/database.py +215 -43
- package/src/superlocalmemory/storage/migration_runner.py +17 -1
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
- package/src/superlocalmemory/storage/models.py +10 -0
- package/src/superlocalmemory/storage/schema.py +15 -10
- package/src/superlocalmemory.egg-info/PKG-INFO +6 -3
- package/src/superlocalmemory.egg-info/SOURCES.txt +1 -0
|
@@ -68,6 +68,24 @@ class RememberRequest(BaseModel):
|
|
|
68
68
|
content: str
|
|
69
69
|
tags: str = ""
|
|
70
70
|
metadata: dict | None = None # v3.4.26: pass-through from MCP pool_store
|
|
71
|
+
# v3.6.15 multi-scope: visibility of the new memory. ``None`` scope means
|
|
72
|
+
# "use the configured default_scope" (personal). shared_with is the list of
|
|
73
|
+
# profile_ids for scope='shared'.
|
|
74
|
+
scope: str | None = None
|
|
75
|
+
shared_with: list[str] | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SessionOpenRequest(BaseModel):
|
|
79
|
+
# #49: local session-open warm (no model roundtrip needed)
|
|
80
|
+
project_path: str = ""
|
|
81
|
+
query: str = ""
|
|
82
|
+
max_results: int = 10
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SessionCloseRequest(BaseModel):
|
|
86
|
+
# #49: local session-close (e.g. a Claude /quit hook). Empty session_id
|
|
87
|
+
# closes the most recent real session.
|
|
88
|
+
session_id: str = ""
|
|
71
89
|
|
|
72
90
|
|
|
73
91
|
class ObserveRequest(BaseModel):
|
|
@@ -876,8 +894,14 @@ async def lifespan(application: FastAPI):
|
|
|
876
894
|
from superlocalmemory.optimize.storage.db import CacheDB
|
|
877
895
|
application.include_router(optimize_router)
|
|
878
896
|
|
|
879
|
-
# Restore persisted metrics counters on startup
|
|
880
|
-
MetricsPersistence
|
|
897
|
+
# Restore persisted metrics counters on startup.
|
|
898
|
+
# #48 fix: build ONE CacheDB + MetricsPersistence and reuse them for the
|
|
899
|
+
# life of the daemon. The old code constructed a new CacheDB() on every
|
|
900
|
+
# 60s flush, which re-ran schema init ("Schema initialized" log spam),
|
|
901
|
+
# the corruption check, and AES-key derivation on every tick.
|
|
902
|
+
_metrics_db = CacheDB()
|
|
903
|
+
_metrics_persistence = MetricsPersistence()
|
|
904
|
+
_metrics_persistence.load(MetricsCollector.get_instance(), _metrics_db)
|
|
881
905
|
|
|
882
906
|
# Periodic flush — every 60s (OPT-005: guard + task ref for shutdown)
|
|
883
907
|
_metrics_flush_task = getattr(application.state, "_optimize_flush_task", None)
|
|
@@ -886,8 +910,8 @@ async def lifespan(application: FastAPI):
|
|
|
886
910
|
while True:
|
|
887
911
|
await asyncio.sleep(60)
|
|
888
912
|
try:
|
|
889
|
-
|
|
890
|
-
MetricsCollector.get_instance(),
|
|
913
|
+
_metrics_persistence.flush(
|
|
914
|
+
MetricsCollector.get_instance(), _metrics_db
|
|
891
915
|
)
|
|
892
916
|
except Exception as e:
|
|
893
917
|
logger.warning("metrics flush error: %s", e)
|
|
@@ -1643,6 +1667,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1643
1667
|
fast: bool = False,
|
|
1644
1668
|
full: bool = False,
|
|
1645
1669
|
include_source: bool = False,
|
|
1670
|
+
include_global: bool | None = None,
|
|
1671
|
+
include_shared: bool | None = None,
|
|
1646
1672
|
):
|
|
1647
1673
|
_update_activity()
|
|
1648
1674
|
search_query = q or query # Accept both ?q= and ?query= for compatibility
|
|
@@ -1678,6 +1704,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1678
1704
|
engine.recall,
|
|
1679
1705
|
search_query, limit=limit, session_id=effective_sid,
|
|
1680
1706
|
fast=fast,
|
|
1707
|
+
include_global=include_global,
|
|
1708
|
+
include_shared=include_shared,
|
|
1681
1709
|
)
|
|
1682
1710
|
# v3.4.26: return the same field shape as recall_worker so
|
|
1683
1711
|
# MCP processes proxying through the daemon get recall_trace-
|
|
@@ -1739,13 +1767,23 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1739
1767
|
_update_activity()
|
|
1740
1768
|
engine = _get_engine_or_503()
|
|
1741
1769
|
|
|
1770
|
+
# v3.6.15 multi-scope: resolve the write scope. ``None`` (not specified
|
|
1771
|
+
# by the caller) → the configured default_scope (personal). Shared
|
|
1772
|
+
# memory is opt-in, so the default keeps every write private.
|
|
1773
|
+
_scope_cfg = getattr(engine._config, "scope", None)
|
|
1774
|
+
scope = req.scope or getattr(_scope_cfg, "default_scope", "personal")
|
|
1775
|
+
shared_with = req.shared_with
|
|
1776
|
+
|
|
1742
1777
|
if wait:
|
|
1743
1778
|
try:
|
|
1744
1779
|
metadata = {"tags": req.tags} if req.tags else {}
|
|
1745
1780
|
extra = getattr(req, "metadata", None)
|
|
1746
1781
|
if isinstance(extra, dict):
|
|
1747
1782
|
metadata.update(extra)
|
|
1748
|
-
fact_ids = engine.store(
|
|
1783
|
+
fact_ids = engine.store(
|
|
1784
|
+
req.content, metadata=metadata,
|
|
1785
|
+
scope=scope, shared_with=shared_with,
|
|
1786
|
+
)
|
|
1749
1787
|
return {"ok": True, "fact_ids": fact_ids, "count": len(fact_ids)}
|
|
1750
1788
|
except Exception as exc:
|
|
1751
1789
|
raise HTTPException(500, detail=str(exc))
|
|
@@ -1758,13 +1796,25 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1758
1796
|
extra = getattr(req, "metadata", None)
|
|
1759
1797
|
if isinstance(extra, dict):
|
|
1760
1798
|
meta.update(extra)
|
|
1799
|
+
# v3.6.15 multi-scope: persist the resolved scope INSIDE the pending
|
|
1800
|
+
# metadata so the materializer (_process_pending_memories) replays
|
|
1801
|
+
# the write with the correct visibility instead of defaulting to
|
|
1802
|
+
# personal. Non-personal only — keeps personal rows byte-identical
|
|
1803
|
+
# to pre-3.6.15 so nothing downstream sees a new key by default.
|
|
1804
|
+
if scope and scope != "personal":
|
|
1805
|
+
meta["scope"] = scope
|
|
1806
|
+
if shared_with:
|
|
1807
|
+
meta["shared_with"] = shared_with
|
|
1761
1808
|
# v3.5.5 WRITE-THROUGH: synchronous verbatim insert → the memory is
|
|
1762
1809
|
# keyword/BM25-recallable the instant this returns (~ms). Closes the
|
|
1763
1810
|
# recall window so a parallel/next agent finds memories saved seconds
|
|
1764
1811
|
# ago. Embedding/graph enrichment is deferred to the materializer.
|
|
1765
1812
|
fact_ids: list[str] = []
|
|
1766
1813
|
try:
|
|
1767
|
-
fact_ids = engine.store_fast(
|
|
1814
|
+
fact_ids = engine.store_fast(
|
|
1815
|
+
req.content, metadata=meta,
|
|
1816
|
+
scope=scope, shared_with=shared_with,
|
|
1817
|
+
)
|
|
1768
1818
|
except Exception as fexc:
|
|
1769
1819
|
logger.warning("store_fast failed, falling back to pending-only: %s", fexc)
|
|
1770
1820
|
# Enqueue for async enrichment (embedding + entities + graph). The
|
|
@@ -1901,6 +1951,65 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1901
1951
|
os.kill(os.getpid(), signal.SIGTERM)
|
|
1902
1952
|
return {"status": "stopping"}
|
|
1903
1953
|
|
|
1954
|
+
@application.post("/session/open")
|
|
1955
|
+
async def session_open(req: SessionOpenRequest):
|
|
1956
|
+
"""#49: Open a session locally — warm recall context with no model
|
|
1957
|
+
roundtrip, so a shell/session-start hook can call it directly
|
|
1958
|
+
(`slm session open`) instead of going through the MCP tool.
|
|
1959
|
+
"""
|
|
1960
|
+
_update_activity()
|
|
1961
|
+
engine = _get_engine_or_503()
|
|
1962
|
+
if req.query:
|
|
1963
|
+
query = req.query
|
|
1964
|
+
elif req.project_path:
|
|
1965
|
+
query = f"project context {req.project_path}"
|
|
1966
|
+
else:
|
|
1967
|
+
query = "recent important decisions"
|
|
1968
|
+
try:
|
|
1969
|
+
resp = engine.recall(query, limit=req.max_results)
|
|
1970
|
+
results = (
|
|
1971
|
+
getattr(resp, "results", None)
|
|
1972
|
+
or getattr(resp, "memories", None)
|
|
1973
|
+
or []
|
|
1974
|
+
)
|
|
1975
|
+
return {"ok": True, "query": query, "warmed": len(results)}
|
|
1976
|
+
except Exception as exc:
|
|
1977
|
+
# Warming is best-effort — never fail the session-open hook.
|
|
1978
|
+
return {"ok": True, "query": query, "warmed": 0, "warning": str(exc)}
|
|
1979
|
+
|
|
1980
|
+
@application.post("/session/close")
|
|
1981
|
+
async def session_close(req: SessionCloseRequest):
|
|
1982
|
+
"""#49: Close a session locally (e.g. a Claude /quit hook calling
|
|
1983
|
+
`slm session close`). Creates per-entity temporal summary events.
|
|
1984
|
+
An empty session_id closes the most recent real session.
|
|
1985
|
+
"""
|
|
1986
|
+
_update_activity()
|
|
1987
|
+
engine = _get_engine_or_503()
|
|
1988
|
+
sid = req.session_id
|
|
1989
|
+
if not sid:
|
|
1990
|
+
# Fall back to the most recent session that has memories.
|
|
1991
|
+
try:
|
|
1992
|
+
db = getattr(engine, "_db", None) or getattr(engine, "db", None)
|
|
1993
|
+
if db is not None and hasattr(db, "execute"):
|
|
1994
|
+
rows = db.execute(
|
|
1995
|
+
"SELECT session_id FROM memories "
|
|
1996
|
+
"WHERE session_id != '' ORDER BY created_at DESC LIMIT 1",
|
|
1997
|
+
(),
|
|
1998
|
+
)
|
|
1999
|
+
if rows:
|
|
2000
|
+
sid = str(rows[0][0])
|
|
2001
|
+
except Exception as exc:
|
|
2002
|
+
logger.debug("session_close fallback lookup failed: %s", exc)
|
|
2003
|
+
if not sid:
|
|
2004
|
+
return {"ok": True, "session_id": "", "summary_events_created": 0,
|
|
2005
|
+
"message": "no session to close"}
|
|
2006
|
+
try:
|
|
2007
|
+
created = engine.close_session(sid)
|
|
2008
|
+
return {"ok": True, "session_id": sid,
|
|
2009
|
+
"summary_events_created": int(created)}
|
|
2010
|
+
except Exception as exc:
|
|
2011
|
+
raise HTTPException(500, detail=str(exc))
|
|
2012
|
+
|
|
1904
2013
|
|
|
1905
2014
|
def _update_activity():
|
|
1906
2015
|
global _last_activity
|
|
@@ -2016,10 +2125,14 @@ def _start_pending_materializer() -> None:
|
|
|
2016
2125
|
# embedding, ENRICH it in place (compute embedding +
|
|
2017
2126
|
# upsert vector store) rather than skipping — otherwise
|
|
2018
2127
|
# the fact would never be semantically searchable.
|
|
2128
|
+
# v3.6.15: scope the dedup to THIS profile. Without the
|
|
2129
|
+
# profile_id filter, a memory whose verbatim text matches
|
|
2130
|
+
# another profile's fact was treated as a duplicate and
|
|
2131
|
+
# silently dropped — cross-profile data loss + leakage.
|
|
2019
2132
|
dup = engine._db.execute(
|
|
2020
2133
|
"SELECT fact_id, embedding FROM atomic_facts "
|
|
2021
|
-
"WHERE content = ? LIMIT 1",
|
|
2022
|
-
(content,),
|
|
2134
|
+
"WHERE content = ? AND profile_id = ? LIMIT 1",
|
|
2135
|
+
(content, engine._profile_id),
|
|
2023
2136
|
)
|
|
2024
2137
|
if dup:
|
|
2025
2138
|
try:
|
|
@@ -2050,6 +2163,12 @@ def _start_pending_materializer() -> None:
|
|
|
2050
2163
|
md = {}
|
|
2051
2164
|
if item.get("tags"):
|
|
2052
2165
|
md.setdefault("tags", item["tags"])
|
|
2166
|
+
# v3.6.15: replay the scope the async /remember path
|
|
2167
|
+
# stashed in metadata, so a queued non-personal write
|
|
2168
|
+
# materializes with the right visibility (not personal).
|
|
2169
|
+
_mscope = md.get("scope") or "personal"
|
|
2170
|
+
_mshared = md.get("shared_with")
|
|
2171
|
+
_shared_json = _json.dumps(_mshared) if _mshared else None
|
|
2053
2172
|
# Create memory row (FK target for atomic_facts)
|
|
2054
2173
|
from datetime import datetime, timezone
|
|
2055
2174
|
from superlocalmemory.storage.models import (
|
|
@@ -2060,17 +2179,20 @@ def _start_pending_materializer() -> None:
|
|
|
2060
2179
|
"INSERT OR IGNORE INTO memories "
|
|
2061
2180
|
"(memory_id, profile_id, content, "
|
|
2062
2181
|
"session_id, speaker, role, created_at, "
|
|
2063
|
-
"metadata_json
|
|
2182
|
+
"metadata_json, scope, shared_with) "
|
|
2183
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
2064
2184
|
(mem_id, engine._profile_id, content,
|
|
2065
2185
|
"", "", "user",
|
|
2066
2186
|
datetime.now(timezone.utc).isoformat(),
|
|
2067
|
-
_json.dumps(md)),
|
|
2187
|
+
_json.dumps(md), _mscope, _shared_json),
|
|
2068
2188
|
)
|
|
2069
2189
|
fact = AtomicFact(
|
|
2070
2190
|
content=content,
|
|
2071
2191
|
fact_type=FactType.EPISODIC,
|
|
2072
2192
|
memory_id=mem_id,
|
|
2073
2193
|
profile_id=engine._profile_id,
|
|
2194
|
+
scope=_mscope,
|
|
2195
|
+
shared_with=_mshared,
|
|
2074
2196
|
)
|
|
2075
2197
|
engine.store_fact_direct(fact)
|
|
2076
2198
|
mark_done(item["id"])
|