superlocalmemory 3.6.13 → 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/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +28 -0
- package/README.md +189 -740
- package/package.json +12 -5
- package/plugin/.claude-plugin/plugin.json +20 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/CLAUDE.md +44 -0
- package/plugin/_GENERATED.md +6 -0
- package/plugin/agents/slm-memory-advisor.md +44 -0
- package/plugin/agents/slm-optimize-advisor.md +38 -0
- package/plugin/hooks/hooks.json +14 -0
- package/plugin/requirements.txt +1 -0
- package/plugin/scripts/ensure-venv.bat +122 -0
- package/plugin/scripts/ensure-venv.sh +105 -0
- package/plugin/scripts/slm-launch +15 -0
- package/plugin/scripts/slm-launch.bat +17 -0
- package/plugin/settings.json +16 -0
- package/plugin/skills/slm-cache/SKILL.md +140 -0
- package/plugin/skills/slm-compress/SKILL.md +143 -0
- package/plugin/skills/slm-graph/SKILL.md +300 -0
- package/plugin/skills/slm-recall/SKILL.md +204 -0
- package/plugin/skills/slm-remember/SKILL.md +194 -0
- package/plugin/skills/slm-session/SKILL.md +207 -0
- package/plugin/skills/slm-status/SKILL.md +149 -0
- package/plugin-src/.mcp.json +12 -0
- package/plugin-src/agents/slm-memory-advisor.md +44 -0
- package/plugin-src/agents/slm-optimize-advisor.md +38 -0
- package/plugin-src/commands/slm-optimize.md +22 -0
- package/plugin-src/commands/slm-recall.md +16 -0
- package/plugin-src/commands/slm-remember.md +16 -0
- package/plugin-src/commands/slm-status.md +15 -0
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +14 -0
- package/plugin-src/manifest.json +25 -0
- package/plugin-src/requirements.txt +1 -0
- package/plugin-src/rules/AGENTS.md +91 -0
- package/plugin-src/rules/CLAUDE.md.fragment +44 -0
- package/plugin-src/scripts/ensure-venv.bat +122 -0
- package/plugin-src/scripts/ensure-venv.sh +105 -0
- package/plugin-src/scripts/slm-launch +15 -0
- package/plugin-src/scripts/slm-launch.bat +17 -0
- package/plugin-src/settings.json +16 -0
- package/plugin-src/skills/slm-cache/SKILL.md +140 -0
- package/plugin-src/skills/slm-compress/SKILL.md +143 -0
- package/plugin-src/skills/slm-graph/SKILL.md +300 -0
- package/plugin-src/skills/slm-recall/SKILL.md +204 -0
- package/plugin-src/skills/slm-remember/SKILL.md +194 -0
- package/plugin-src/skills/slm-session/SKILL.md +207 -0
- package/plugin-src/skills/slm-status/SKILL.md +149 -0
- package/pyproject.toml +6 -2
- package/scripts/__tests__/build-plugin.test.mjs +613 -0
- package/scripts/_savings_math.py +270 -0
- package/scripts/build-plugin.js +742 -0
- package/scripts/dogfood_savings.py +490 -0
- package/scripts/install-skills.ps1 +4 -334
- package/scripts/install-skills.sh +4 -435
- package/scripts/postinstall-interactive.js +0 -27
- package/scripts/postinstall.js +21 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +115 -0
- package/src/superlocalmemory/cli/commands.py +439 -41
- package/src/superlocalmemory/cli/main.py +92 -4
- package/src/superlocalmemory/cli/setup_wizard.py +47 -6
- package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
- package/src/superlocalmemory/core/config.py +194 -9
- package/src/superlocalmemory/core/embeddings.py +10 -5
- package/src/superlocalmemory/core/engine.py +76 -5
- 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/claude_code_hooks.py +27 -3
- package/src/superlocalmemory/hooks/portable_kit.py +506 -0
- package/src/superlocalmemory/hooks/session_registry.py +8 -4
- package/src/superlocalmemory/infra/cloud_backup.py +99 -23
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
- package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
- package/src/superlocalmemory/mcp/server.py +75 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
- package/src/superlocalmemory/mcp/tools_core.py +37 -4
- package/src/superlocalmemory/mcp/tools_v3.py +6 -1
- package/src/superlocalmemory/mcp/tools_v33.py +8 -4
- package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
- package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
- package/src/superlocalmemory/optimize/cache/manager.py +92 -6
- package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
- package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
- package/src/superlocalmemory/optimize/compress/router.py +46 -13
- package/src/superlocalmemory/optimize/config/schema.py +6 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
- package/src/superlocalmemory/optimize/proxy/server.py +11 -0
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
- package/src/superlocalmemory/optimize/storage/db.py +30 -0
- 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/recall_serializer.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +156 -16
- 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/ui/css/legacy-dashboard.css +18 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
- package/src/superlocalmemory/ui/index.html +2 -2
- package/src/superlocalmemory/ui/js/core.js +98 -0
- package/src/superlocalmemory/ui/js/dashboard.js +8 -1
- package/src/superlocalmemory/ui/js/ide-status.js +16 -3
- package/src/superlocalmemory/ui/js/math-health.js +15 -3
- package/src/superlocalmemory/ui/js/optimize.js +18 -2
- package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
- package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/ide/skills/slm-build-graph/SKILL.md +0 -423
- package/ide/skills/slm-list-recent/SKILL.md +0 -348
- package/ide/skills/slm-recall/SKILL.md +0 -326
- package/ide/skills/slm-remember/SKILL.md +0 -194
- package/ide/skills/slm-show-patterns/SKILL.md +0 -224
- package/ide/skills/slm-status/SKILL.md +0 -363
- package/ide/skills/slm-switch-profile/SKILL.md +0 -442
- package/skills/slm-build-graph/SKILL.md +0 -423
- package/skills/slm-list-recent/SKILL.md +0 -348
- package/skills/slm-optimize/README.md +0 -55
- package/skills/slm-optimize/SKILL.md +0 -139
- package/skills/slm-recall/SKILL.md +0 -343
- package/skills/slm-remember/SKILL.md +0 -194
- package/skills/slm-show-patterns/SKILL.md +0 -224
- package/skills/slm-status/SKILL.md +0 -363
- package/skills/slm-switch-profile/SKILL.md +0 -442
- package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
- package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
|
@@ -50,6 +50,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
|
50
50
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
51
51
|
from pydantic import BaseModel
|
|
52
52
|
|
|
53
|
+
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
|
|
54
|
+
|
|
53
55
|
logger = logging.getLogger("superlocalmemory.unified_daemon")
|
|
54
56
|
|
|
55
57
|
_DEFAULT_PORT = 8765
|
|
@@ -66,6 +68,24 @@ class RememberRequest(BaseModel):
|
|
|
66
68
|
content: str
|
|
67
69
|
tags: str = ""
|
|
68
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 = ""
|
|
69
89
|
|
|
70
90
|
|
|
71
91
|
class ObserveRequest(BaseModel):
|
|
@@ -237,16 +257,32 @@ class ObserveBuffer:
|
|
|
237
257
|
try:
|
|
238
258
|
from superlocalmemory.hooks.auto_capture import AutoCapture
|
|
239
259
|
auto = AutoCapture(engine=self._engine)
|
|
260
|
+
captured_count = 0
|
|
261
|
+
failed_count = 0
|
|
240
262
|
for content in batch:
|
|
241
263
|
try:
|
|
242
264
|
decision = auto.evaluate(content)
|
|
243
265
|
if decision.capture:
|
|
244
266
|
auto.capture(content, category=decision.category)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
# Stage-9: count only what was actually WRITTEN to memory.
|
|
268
|
+
# The prior 'processed N' counted skipped (capture=False)
|
|
269
|
+
# items as successes — a false-positive write count.
|
|
270
|
+
captured_count += 1
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
failed_count += 1
|
|
273
|
+
logger.warning(
|
|
274
|
+
"ObserveBuffer: auto.capture failed for content %.40r: %s",
|
|
275
|
+
content,
|
|
276
|
+
exc,
|
|
277
|
+
)
|
|
278
|
+
logger.info(
|
|
279
|
+
"Observe debounce: evaluated=%d captured=%d failed=%d",
|
|
280
|
+
len(batch),
|
|
281
|
+
captured_count,
|
|
282
|
+
failed_count,
|
|
283
|
+
)
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
logger.error("ObserveBuffer: flush batch failed: %s", exc)
|
|
250
286
|
|
|
251
287
|
def flush_sync(self) -> None:
|
|
252
288
|
"""Force flush for shutdown."""
|
|
@@ -858,8 +894,14 @@ async def lifespan(application: FastAPI):
|
|
|
858
894
|
from superlocalmemory.optimize.storage.db import CacheDB
|
|
859
895
|
application.include_router(optimize_router)
|
|
860
896
|
|
|
861
|
-
# Restore persisted metrics counters on startup
|
|
862
|
-
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)
|
|
863
905
|
|
|
864
906
|
# Periodic flush — every 60s (OPT-005: guard + task ref for shutdown)
|
|
865
907
|
_metrics_flush_task = getattr(application.state, "_optimize_flush_task", None)
|
|
@@ -868,8 +910,8 @@ async def lifespan(application: FastAPI):
|
|
|
868
910
|
while True:
|
|
869
911
|
await asyncio.sleep(60)
|
|
870
912
|
try:
|
|
871
|
-
|
|
872
|
-
MetricsCollector.get_instance(),
|
|
913
|
+
_metrics_persistence.flush(
|
|
914
|
+
MetricsCollector.get_instance(), _metrics_db
|
|
873
915
|
)
|
|
874
916
|
except Exception as e:
|
|
875
917
|
logger.warning("metrics flush error: %s", e)
|
|
@@ -1620,11 +1662,13 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1620
1662
|
@application.get("/recall")
|
|
1621
1663
|
async def recall(
|
|
1622
1664
|
request: Request,
|
|
1623
|
-
q: str = "", query: str = "", limit: int =
|
|
1665
|
+
q: str = "", query: str = "", limit: int = CANONICAL_RECALL_LIMIT,
|
|
1624
1666
|
session_id: str = "",
|
|
1625
1667
|
fast: bool = False,
|
|
1626
1668
|
full: bool = False,
|
|
1627
1669
|
include_source: bool = False,
|
|
1670
|
+
include_global: bool | None = None,
|
|
1671
|
+
include_shared: bool | None = None,
|
|
1628
1672
|
):
|
|
1629
1673
|
_update_activity()
|
|
1630
1674
|
search_query = q or query # Accept both ?q= and ?query= for compatibility
|
|
@@ -1660,6 +1704,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1660
1704
|
engine.recall,
|
|
1661
1705
|
search_query, limit=limit, session_id=effective_sid,
|
|
1662
1706
|
fast=fast,
|
|
1707
|
+
include_global=include_global,
|
|
1708
|
+
include_shared=include_shared,
|
|
1663
1709
|
)
|
|
1664
1710
|
# v3.4.26: return the same field shape as recall_worker so
|
|
1665
1711
|
# MCP processes proxying through the daemon get recall_trace-
|
|
@@ -1721,13 +1767,23 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1721
1767
|
_update_activity()
|
|
1722
1768
|
engine = _get_engine_or_503()
|
|
1723
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
|
+
|
|
1724
1777
|
if wait:
|
|
1725
1778
|
try:
|
|
1726
1779
|
metadata = {"tags": req.tags} if req.tags else {}
|
|
1727
1780
|
extra = getattr(req, "metadata", None)
|
|
1728
1781
|
if isinstance(extra, dict):
|
|
1729
1782
|
metadata.update(extra)
|
|
1730
|
-
fact_ids = engine.store(
|
|
1783
|
+
fact_ids = engine.store(
|
|
1784
|
+
req.content, metadata=metadata,
|
|
1785
|
+
scope=scope, shared_with=shared_with,
|
|
1786
|
+
)
|
|
1731
1787
|
return {"ok": True, "fact_ids": fact_ids, "count": len(fact_ids)}
|
|
1732
1788
|
except Exception as exc:
|
|
1733
1789
|
raise HTTPException(500, detail=str(exc))
|
|
@@ -1740,13 +1796,25 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1740
1796
|
extra = getattr(req, "metadata", None)
|
|
1741
1797
|
if isinstance(extra, dict):
|
|
1742
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
|
|
1743
1808
|
# v3.5.5 WRITE-THROUGH: synchronous verbatim insert → the memory is
|
|
1744
1809
|
# keyword/BM25-recallable the instant this returns (~ms). Closes the
|
|
1745
1810
|
# recall window so a parallel/next agent finds memories saved seconds
|
|
1746
1811
|
# ago. Embedding/graph enrichment is deferred to the materializer.
|
|
1747
1812
|
fact_ids: list[str] = []
|
|
1748
1813
|
try:
|
|
1749
|
-
fact_ids = engine.store_fast(
|
|
1814
|
+
fact_ids = engine.store_fast(
|
|
1815
|
+
req.content, metadata=meta,
|
|
1816
|
+
scope=scope, shared_with=shared_with,
|
|
1817
|
+
)
|
|
1750
1818
|
except Exception as fexc:
|
|
1751
1819
|
logger.warning("store_fast failed, falling back to pending-only: %s", fexc)
|
|
1752
1820
|
# Enqueue for async enrichment (embedding + entities + graph). The
|
|
@@ -1883,6 +1951,65 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1883
1951
|
os.kill(os.getpid(), signal.SIGTERM)
|
|
1884
1952
|
return {"status": "stopping"}
|
|
1885
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
|
+
|
|
1886
2013
|
|
|
1887
2014
|
def _update_activity():
|
|
1888
2015
|
global _last_activity
|
|
@@ -1998,10 +2125,14 @@ def _start_pending_materializer() -> None:
|
|
|
1998
2125
|
# embedding, ENRICH it in place (compute embedding +
|
|
1999
2126
|
# upsert vector store) rather than skipping — otherwise
|
|
2000
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.
|
|
2001
2132
|
dup = engine._db.execute(
|
|
2002
2133
|
"SELECT fact_id, embedding FROM atomic_facts "
|
|
2003
|
-
"WHERE content = ? LIMIT 1",
|
|
2004
|
-
(content,),
|
|
2134
|
+
"WHERE content = ? AND profile_id = ? LIMIT 1",
|
|
2135
|
+
(content, engine._profile_id),
|
|
2005
2136
|
)
|
|
2006
2137
|
if dup:
|
|
2007
2138
|
try:
|
|
@@ -2032,6 +2163,12 @@ def _start_pending_materializer() -> None:
|
|
|
2032
2163
|
md = {}
|
|
2033
2164
|
if item.get("tags"):
|
|
2034
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
|
|
2035
2172
|
# Create memory row (FK target for atomic_facts)
|
|
2036
2173
|
from datetime import datetime, timezone
|
|
2037
2174
|
from superlocalmemory.storage.models import (
|
|
@@ -2042,17 +2179,20 @@ def _start_pending_materializer() -> None:
|
|
|
2042
2179
|
"INSERT OR IGNORE INTO memories "
|
|
2043
2180
|
"(memory_id, profile_id, content, "
|
|
2044
2181
|
"session_id, speaker, role, created_at, "
|
|
2045
|
-
"metadata_json
|
|
2182
|
+
"metadata_json, scope, shared_with) "
|
|
2183
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
2046
2184
|
(mem_id, engine._profile_id, content,
|
|
2047
2185
|
"", "", "user",
|
|
2048
2186
|
datetime.now(timezone.utc).isoformat(),
|
|
2049
|
-
_json.dumps(md)),
|
|
2187
|
+
_json.dumps(md), _mscope, _shared_json),
|
|
2050
2188
|
)
|
|
2051
2189
|
fact = AtomicFact(
|
|
2052
2190
|
content=content,
|
|
2053
2191
|
fact_type=FactType.EPISODIC,
|
|
2054
2192
|
memory_id=mem_id,
|
|
2055
2193
|
profile_id=engine._profile_id,
|
|
2194
|
+
scope=_mscope,
|
|
2195
|
+
shared_with=_mshared,
|
|
2056
2196
|
)
|
|
2057
2197
|
engine.store_fact_direct(fact)
|
|
2058
2198
|
mark_done(item["id"])
|