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.
Files changed (66) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +5 -2
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +5 -4
  6. package/plugin/agents/slm-memory-advisor.md +5 -4
  7. package/plugin/agents/slm-optimize-advisor.md +1 -1
  8. package/plugin/requirements.txt +1 -1
  9. package/plugin/skills/slm-cache/SKILL.md +1 -1
  10. package/plugin/skills/slm-compress/SKILL.md +1 -1
  11. package/plugin/skills/slm-graph/SKILL.md +1 -1
  12. package/plugin/skills/slm-recall/SKILL.md +10 -2
  13. package/plugin/skills/slm-remember/SKILL.md +14 -2
  14. package/plugin/skills/slm-session/SKILL.md +1 -1
  15. package/plugin/skills/slm-status/SKILL.md +1 -1
  16. package/plugin-src/agents/slm-memory-advisor.md +5 -4
  17. package/plugin-src/agents/slm-optimize-advisor.md +1 -1
  18. package/plugin-src/commands/slm-optimize.md +1 -1
  19. package/plugin-src/commands/slm-recall.md +1 -1
  20. package/plugin-src/commands/slm-remember.md +2 -2
  21. package/plugin-src/commands/slm-status.md +1 -1
  22. package/plugin-src/manifest.json +1 -1
  23. package/plugin-src/requirements.txt +1 -1
  24. package/plugin-src/rules/AGENTS.md +5 -4
  25. package/plugin-src/rules/CLAUDE.md.fragment +5 -4
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-recall/SKILL.md +10 -2
  30. package/plugin-src/skills/slm-remember/SKILL.md +14 -2
  31. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  33. package/pyproject.toml +1 -1
  34. package/scripts/build-plugin.js +1 -1
  35. package/src/superlocalmemory/__init__.py +1 -1
  36. package/src/superlocalmemory/cli/commands.py +91 -2
  37. package/src/superlocalmemory/cli/main.py +45 -0
  38. package/src/superlocalmemory/cli/setup_wizard.py +27 -0
  39. package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
  40. package/src/superlocalmemory/core/config.py +115 -0
  41. package/src/superlocalmemory/core/engine.py +74 -3
  42. package/src/superlocalmemory/core/fact_consolidator.py +20 -3
  43. package/src/superlocalmemory/core/platform_utils.py +8 -0
  44. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  45. package/src/superlocalmemory/core/recall_worker.py +7 -0
  46. package/src/superlocalmemory/core/store_pipeline.py +23 -1
  47. package/src/superlocalmemory/core/worker_pool.py +14 -2
  48. package/src/superlocalmemory/hooks/session_registry.py +8 -4
  49. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
  50. package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
  51. package/src/superlocalmemory/mcp/tools_core.py +25 -0
  52. package/src/superlocalmemory/mcp/tools_v3.py +6 -1
  53. package/src/superlocalmemory/mcp/tools_v33.py +8 -4
  54. package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
  55. package/src/superlocalmemory/retrieval/engine.py +36 -3
  56. package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
  57. package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
  58. package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
  59. package/src/superlocalmemory/server/unified_daemon.py +132 -10
  60. package/src/superlocalmemory/storage/database.py +215 -43
  61. package/src/superlocalmemory/storage/migration_runner.py +17 -1
  62. package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
  63. package/src/superlocalmemory/storage/models.py +10 -0
  64. package/src/superlocalmemory/storage/schema.py +15 -10
  65. package/src/superlocalmemory.egg-info/PKG-INFO +6 -3
  66. 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().load(MetricsCollector.get_instance(), CacheDB())
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
- MetricsPersistence().flush(
890
- MetricsCollector.get_instance(), CacheDB()
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(req.content, metadata=metadata)
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(req.content, metadata=meta)
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) VALUES (?,?,?,?,?,?,?,?)",
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"])