superlocalmemory 3.4.45 → 3.4.46

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 CHANGED
@@ -9,6 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ---
11
11
 
12
+ ## [3.4.46] - 2026-05-18
13
+
14
+ ### Added
15
+ - **`SLM_MCP_TOOLS` env var** — Fine-grained MCP tool allowlist. Users can now
16
+ set `SLM_MCP_TOOLS=remember,recall,search,session_init` to expose exactly
17
+ the tools they need, reducing MCP context budget. Falls back to 25-tool
18
+ essential set when unset; `SLM_MCP_ALL_TOOLS=1` still wins for power users.
19
+ - **`KMP_DUPLICATE_LIB_OK=TRUE`** — Set at package init to prevent OpenMP
20
+ multi-library crashes when PyTorch, ONNX Runtime, and NumPy-MKL all load
21
+ their own runtimes simultaneously.
22
+
23
+ ### Fixed
24
+ - **WAL busy_timeout ordering** (PR #24, @kenyonxu) — `_enable_wal()` now
25
+ sets `busy_timeout` before `journal_mode=WAL`, ensuring the 10s configured
26
+ timeout is used instead of SQLite's default 5s during WAL initialization.
27
+ - **Engine init traceback logging** (PR #25, @kenyonxu) — `logger.exception()`
28
+ replaces `logger.warning()` on daemon engine init failure, capturing the
29
+ full traceback for root-cause diagnosis.
30
+ - **MCP `fast` recall wiring** (PR #22, @VikingOwl91) — `fast=True` recall
31
+ parameter now threads through the full MCP→daemon→worker stack.
32
+ `session_init` performs one `pool_recall(fast=True)` instead of two
33
+ redundant recalls. Tools switch from `WorkerPool.shared()` to `choose_pool()`
34
+ for daemon-first routing (avoids N×1.6 GB ONNX duplication across IDEs).
35
+ - **FTS trigger idempotency** — `CREATE TRIGGER IF NOT EXISTS` prevents race
36
+ crashes on repeated schema init.
37
+
38
+ ---
39
+
12
40
  ## [3.4.43] - 2026-05-12
13
41
 
14
42
  Smart-hook architecture release. Replaces the time-based 15-minute recall
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.45",
3
+ "version": "3.4.46",
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.45"
3
+ version = "3.4.46"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -1,6 +1,9 @@
1
1
  """SuperLocalMemory — information-geometric agent memory."""
2
2
 
3
- __version__ = "3.4.45"
3
+ import os
4
+ os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
5
+
6
+ __version__ = "3.4.46"
4
7
 
5
8
  _REQUIRED_VERSIONS = {
6
9
  "sentence_transformers": "5.3.0",
@@ -59,10 +59,12 @@ def _get_engine():
59
59
  return _engine
60
60
 
61
61
 
62
- def _handle_recall(query: str, limit: int, session_id: str = "") -> dict:
62
+ def _handle_recall(
63
+ query: str, limit: int, session_id: str = "", fast: bool = False,
64
+ ) -> dict:
63
65
  engine = _get_engine()
64
66
  response = engine.recall(
65
- query, limit=limit, session_id=session_id or None,
67
+ query, limit=limit, session_id=session_id or None, fast=bool(fast),
66
68
  )
67
69
 
68
70
  # Batch-fetch original memory text for all results
@@ -290,7 +292,7 @@ def _worker_main() -> None:
290
292
  if cmd == "recall":
291
293
  result = _handle_recall(
292
294
  req.get("query", ""), req.get("limit", 10),
293
- req.get("session_id", ""),
295
+ req.get("session_id", ""), bool(req.get("fast", False)),
294
296
  )
295
297
  _respond(result)
296
298
  elif cmd == "store":
@@ -67,6 +67,7 @@ class WorkerPool:
67
67
 
68
68
  def recall(
69
69
  self, query: str, limit: int = 10, session_id: str = "",
70
+ fast: bool = False,
70
71
  ) -> dict:
71
72
  """Run recall in worker subprocess. Returns result dict.
72
73
 
@@ -77,7 +78,7 @@ class WorkerPool:
77
78
  """
78
79
  return self._send({
79
80
  "cmd": "recall", "query": query, "limit": limit,
80
- "session_id": session_id or "",
81
+ "session_id": session_id or "", "fast": bool(fast),
81
82
  })
82
83
 
83
84
  def store(self, content: str, metadata: dict | None = None) -> dict:
@@ -45,10 +45,14 @@ class DaemonPoolProxy:
45
45
 
46
46
  def recall(
47
47
  self, query: str, limit: int = 10, session_id: str = "",
48
+ fast: bool = False,
48
49
  ) -> dict[str, Any]:
49
- params = urllib.parse.urlencode(
50
- {"q": query, "limit": limit, "session_id": session_id or ""}
51
- )
50
+ params = urllib.parse.urlencode({
51
+ "q": query,
52
+ "limit": limit,
53
+ "session_id": session_id or "",
54
+ "fast": "true" if fast else "false",
55
+ })
52
56
  try:
53
57
  with urllib.request.urlopen(
54
58
  self._url(f"/recall?{params}"), timeout=self._timeout,
@@ -74,12 +74,17 @@ def _unwrap_error(raw: Any, op: str) -> None:
74
74
  raise PoolError(f"pool.{op} failed: {reason}")
75
75
 
76
76
 
77
- def pool_recall(query: str, limit: int = 10, **_: Any) -> PoolRecallResponse:
77
+ def pool_recall(query: str, limit: int = 10, **kwargs: Any) -> PoolRecallResponse:
78
78
  """Call pool.recall and reshape its dict into a typed response.
79
79
 
80
80
  Raises :class:`PoolError` on worker death or any non-ok envelope.
81
81
  """
82
- raw = _pool().recall(query=query, limit=limit)
82
+ raw = _pool().recall(
83
+ query=query,
84
+ limit=limit,
85
+ session_id=str(kwargs.get("session_id") or ""),
86
+ fast=bool(kwargs.get("fast", False)),
87
+ )
83
88
  _unwrap_error(raw, "recall")
84
89
  items = raw.get("results", []) if isinstance(raw, dict) else []
85
90
  results = [
@@ -122,13 +122,21 @@ _ESSENTIAL_TOOLS = frozenset(_ESSENTIAL_TOOLS)
122
122
 
123
123
  _all_tools = _os_reg.environ.get("SLM_MCP_ALL_TOOLS") == "1"
124
124
 
125
+ # v3.4.45: Minimal mode — explicit user allowlist via SLM_MCP_TOOLS env var.
126
+ # Format: comma-separated tool names, e.g. "remember,recall,session_init,search"
127
+ # Use case: Claude Code consumer plans with tight context budgets where the
128
+ # 25-tool essential set is still too many. Power users override to expose
129
+ # exactly the tools they invoke. Falls back to _ESSENTIAL_TOOLS when unset.
130
+ _user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
131
+
125
132
 
126
133
  class _FilteredServer:
127
134
  """Wraps FastMCP to only register essential tools.
128
135
 
129
136
  Non-essential tools are silently skipped (not registered on the MCP
130
137
  server). They remain available via CLI. When SLM_MCP_ALL_TOOLS=1,
131
- all tools are registered (bypass filter).
138
+ all tools are registered (bypass filter). When SLM_MCP_TOOLS is set,
139
+ that user allowlist is used instead of _ESSENTIAL_TOOLS.
132
140
  """
133
141
  __slots__ = ("_server", "_allowed")
134
142
 
@@ -147,8 +155,14 @@ class _FilteredServer:
147
155
  return getattr(self._server, name)
148
156
 
149
157
 
150
- # Choose full or filtered registration target
151
- _target = server if _all_tools else _FilteredServer(server, _ESSENTIAL_TOOLS)
158
+ # Choose registration target (precedence: ALL > user allowlist > essential)
159
+ if _all_tools:
160
+ _target = server
161
+ elif _user_allowlist_str:
162
+ _user_allowlist = frozenset(t.strip() for t in _user_allowlist_str.split(",") if t.strip())
163
+ _target = _FilteredServer(server, _user_allowlist)
164
+ else:
165
+ _target = _FilteredServer(server, _ESSENTIAL_TOOLS)
152
166
 
153
167
  from superlocalmemory.mcp.tools_core import register_core_tools
154
168
  from superlocalmemory.mcp.tools_v28 import register_v28_tools
@@ -93,7 +93,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
93
93
  The AI should call this automatically before any other work.
94
94
  """
95
95
  try:
96
- from superlocalmemory.hooks.auto_recall import AutoRecall
97
96
  from superlocalmemory.hooks.rules_engine import RulesEngine
98
97
  from superlocalmemory.mcp._pool_adapter import pool_recall
99
98
 
@@ -104,21 +103,37 @@ def register_active_tools(server, get_engine: Callable) -> None:
104
103
  return {"success": True, "context": "", "memories": [], "message": "Auto-recall disabled"}
105
104
 
106
105
  recall_config = rules.get_recall_config()
107
- auto = AutoRecall(
108
- recall_fn=pool_recall,
109
- config={
110
- "enabled": True,
111
- "max_memories_injected": max_results,
112
- "relevance_threshold": recall_config.get("relevance_threshold", 0.3),
113
- },
114
- )
115
-
116
- # Get formatted context for system prompt injection
117
- context = auto.get_session_context(project_path=project_path, query=query)
118
-
119
- # Get structured results for tool response
120
- search_query = query or f"project context {project_path}" if project_path else "recent important decisions"
121
- memories = auto.get_query_context(search_query)
106
+ relevance_threshold = recall_config.get("relevance_threshold", 0.3)
107
+ if query:
108
+ search_query = query
109
+ elif project_path:
110
+ search_query = f"project context {project_path}"
111
+ else:
112
+ search_query = "recent important decisions"
113
+
114
+ response = pool_recall(search_query, limit=max_results, fast=True)
115
+ relevant = [
116
+ r for r in response.results
117
+ if r.score >= relevance_threshold
118
+ ]
119
+
120
+ # Build both return shapes from one recall. Calling recall twice
121
+ # doubles session startup latency and can return duplicate snippets.
122
+ context = ""
123
+ if relevant:
124
+ lines = ["# Relevant Memory Context", ""]
125
+ for r in relevant[:max_results]:
126
+ lines.append(f"- {r.fact.content[:200]}")
127
+ context = "\n".join(lines)
128
+
129
+ memories = [
130
+ {
131
+ "fact_id": r.fact.fact_id,
132
+ "content": r.fact.content[:300],
133
+ "score": round(r.score, 3),
134
+ }
135
+ for r in relevant[:max_results]
136
+ ]
122
137
 
123
138
  # Get learning status
124
139
  pid = engine.profile_id
@@ -184,7 +199,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
184
199
  from superlocalmemory.hooks.rules_engine import RulesEngine
185
200
  from superlocalmemory.mcp._pool_adapter import pool_store
186
201
 
187
- engine = get_engine()
188
202
  rules = RulesEngine()
189
203
 
190
204
  auto = AutoCapture(
@@ -305,7 +319,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
305
319
  """
306
320
  try:
307
321
  engine = get_engine()
308
- pid = engine.profile_id
309
322
  sid = session_id or getattr(engine, '_last_session_id', '')
310
323
  if not sid:
311
324
  return {"success": False, "error": "No session_id provided"}
@@ -13,10 +13,9 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- import json
17
16
  import logging
18
17
  from pathlib import Path
19
- from typing import Any, Callable
18
+ from typing import Callable
20
19
 
21
20
  from mcp.types import ToolAnnotations
22
21
 
@@ -111,7 +110,6 @@ def register_core_tools(server, get_engine: Callable) -> None:
111
110
  Extracts atomic facts, resolves entities, builds graph edges,
112
111
  and indexes for 4-channel retrieval.
113
112
  """
114
- import asyncio
115
113
  try:
116
114
  # v3.4.32: Store-first pattern. Write to pending.db and return
117
115
  # immediately. The daemon's pending-materializer thread drains
@@ -141,7 +139,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
141
139
  @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
142
140
  async def recall(
143
141
  query: str, limit: int = 10, agent_id: str = "mcp_client",
144
- session_id: str = "",
142
+ session_id: str = "", fast: bool = False,
145
143
  ) -> dict:
146
144
  """Search memories by semantic query with 4-channel retrieval, RRF fusion, and reranking.
147
145
 
@@ -153,8 +151,8 @@ def register_core_tools(server, get_engine: Callable) -> None:
153
151
  """
154
152
  import asyncio
155
153
  try:
156
- from superlocalmemory.core.worker_pool import WorkerPool
157
- pool = WorkerPool.shared()
154
+ from superlocalmemory.mcp._daemon_proxy import choose_pool
155
+ pool = choose_pool()
158
156
  # S9-DASH-10: priority for session_id, so engagement
159
157
  # signals land on the right pending_outcome:
160
158
  # 1. Explicit ``session_id`` tool-call argument.
@@ -199,6 +197,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
199
197
  # block behind a single threading.Lock. See worker_pool.py.
200
198
  result = await asyncio.to_thread(
201
199
  pool.recall, query, limit=limit, session_id=effective_sid,
200
+ fast=bool(fast),
202
201
  )
203
202
  if result.get("ok"):
204
203
  # Record implicit feedback: every returned result is a recall_hit
@@ -285,8 +285,8 @@ def register_v3_tools(server, get_engine: Callable) -> None:
285
285
  limit: Maximum results (default 10).
286
286
  """
287
287
  try:
288
- from superlocalmemory.core.worker_pool import WorkerPool
289
- raw = WorkerPool.shared().recall(query=query, limit=limit)
288
+ from superlocalmemory.mcp._daemon_proxy import choose_pool
289
+ raw = choose_pool().recall(query=query, limit=limit)
290
290
  items = raw.get("results", []) if isinstance(raw, dict) else []
291
291
  results = []
292
292
  for item in items[:limit]:
@@ -535,8 +535,8 @@ async def lifespan(application: FastAPI):
535
535
  application.state.queue_consumer = None
536
536
  application.state.recall_queue = None
537
537
 
538
- except Exception as exc:
539
- logger.warning("Engine init failed: %s", exc)
538
+ except Exception:
539
+ logger.exception("Engine init failed") # auto-includes traceback
540
540
  application.state.engine = None
541
541
  application.state.config = None
542
542
 
@@ -66,8 +66,8 @@ class DatabaseManager:
66
66
  def _enable_wal(self) -> None:
67
67
  conn = sqlite3.connect(str(self.db_path))
68
68
  try:
69
+ conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}") # FIRST — so WAL pragma below uses configured timeout
69
70
  conn.execute("PRAGMA journal_mode=WAL")
70
- conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
71
71
  conn.execute("PRAGMA foreign_keys=ON")
72
72
  conn.commit()
73
73
  finally:
@@ -252,7 +252,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS atomic_facts_fts
252
252
  -- left by V2 migration.
253
253
 
254
254
  -- INSERT trigger
255
- CREATE TRIGGER atomic_facts_fts_insert
255
+ CREATE TRIGGER IF NOT EXISTS atomic_facts_fts_insert
256
256
  AFTER INSERT ON atomic_facts
257
257
  BEGIN
258
258
  INSERT INTO atomic_facts_fts (rowid, fact_id, content)
@@ -260,7 +260,7 @@ BEGIN
260
260
  END;
261
261
 
262
262
  -- DELETE trigger
263
- CREATE TRIGGER atomic_facts_fts_delete
263
+ CREATE TRIGGER IF NOT EXISTS atomic_facts_fts_delete
264
264
  AFTER DELETE ON atomic_facts
265
265
  BEGIN
266
266
  INSERT INTO atomic_facts_fts (atomic_facts_fts, rowid, fact_id, content)
@@ -268,7 +268,7 @@ BEGIN
268
268
  END;
269
269
 
270
270
  -- UPDATE trigger
271
- CREATE TRIGGER atomic_facts_fts_update
271
+ CREATE TRIGGER IF NOT EXISTS atomic_facts_fts_update
272
272
  AFTER UPDATE OF content ON atomic_facts
273
273
  BEGIN
274
274
  INSERT INTO atomic_facts_fts (atomic_facts_fts, rowid, fact_id, content)