superlocalmemory 3.4.24 → 3.4.30

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 (47) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +8 -1
  3. package/package.json +1 -1
  4. package/pyproject.toml +3 -1
  5. package/src/superlocalmemory/__init__.py +1 -1
  6. package/src/superlocalmemory/cli/commands.py +3 -0
  7. package/src/superlocalmemory/cli/daemon.py +90 -16
  8. package/src/superlocalmemory/cli/doctor_cmd.py +152 -0
  9. package/src/superlocalmemory/cli/main.py +28 -0
  10. package/src/superlocalmemory/cli/post_install.py +15 -0
  11. package/src/superlocalmemory/cli/setup_wizard.py +20 -0
  12. package/src/superlocalmemory/cli/version_banner.py +183 -0
  13. package/src/superlocalmemory/cli/wizard_v3426_options.py +129 -0
  14. package/src/superlocalmemory/core/clock_monitor.py +45 -0
  15. package/src/superlocalmemory/core/db_pool.py +80 -0
  16. package/src/superlocalmemory/core/engine.py +75 -30
  17. package/src/superlocalmemory/core/engine_capabilities.py +24 -0
  18. package/src/superlocalmemory/core/engine_lock.py +75 -0
  19. package/src/superlocalmemory/core/error_catalog.py +113 -0
  20. package/src/superlocalmemory/core/error_envelope.py +60 -0
  21. package/src/superlocalmemory/core/file_lock.py +92 -0
  22. package/src/superlocalmemory/core/loop_watchdog.py +56 -0
  23. package/src/superlocalmemory/core/priority_queue.py +61 -0
  24. package/src/superlocalmemory/core/queue_dispatcher.py +73 -0
  25. package/src/superlocalmemory/core/rate_limit.py +151 -0
  26. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  27. package/src/superlocalmemory/core/recall_queue.py +370 -0
  28. package/src/superlocalmemory/core/recall_worker.py +10 -0
  29. package/src/superlocalmemory/core/safe_fs.py +108 -0
  30. package/src/superlocalmemory/hooks/auto_capture.py +34 -12
  31. package/src/superlocalmemory/hooks/auto_recall.py +36 -9
  32. package/src/superlocalmemory/mcp/_daemon_proxy.py +107 -0
  33. package/src/superlocalmemory/mcp/_pool_adapter.py +121 -0
  34. package/src/superlocalmemory/mcp/resources.py +8 -5
  35. package/src/superlocalmemory/mcp/server.py +38 -9
  36. package/src/superlocalmemory/mcp/tools_active.py +21 -9
  37. package/src/superlocalmemory/mcp/tools_core.py +13 -9
  38. package/src/superlocalmemory/mcp/tools_evolution.py +4 -2
  39. package/src/superlocalmemory/mcp/tools_learning.py +5 -3
  40. package/src/superlocalmemory/mcp/tools_mesh.py +5 -3
  41. package/src/superlocalmemory/mcp/tools_v3.py +18 -22
  42. package/src/superlocalmemory/mcp/tools_v33.py +65 -2
  43. package/src/superlocalmemory/migrations/__init__.py +5 -0
  44. package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +144 -0
  45. package/src/superlocalmemory/server/routes/events.py +1 -1
  46. package/src/superlocalmemory/server/routes/v3_api.py +0 -1
  47. package/src/superlocalmemory/server/unified_daemon.py +128 -12
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  import logging
10
10
  import re
11
11
  from dataclasses import dataclass
12
- from typing import Any
12
+ from typing import Any, Callable
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
@@ -41,10 +41,25 @@ class CaptureDecision:
41
41
 
42
42
 
43
43
  class AutoCapture:
44
- """Detect and classify content for automatic storage."""
45
-
46
- def __init__(self, engine=None, config: dict | None = None):
44
+ """Detect and classify content for automatic storage.
45
+
46
+ Two ways to wire the store side:
47
+
48
+ - Pass ``engine=<MemoryEngine>`` (CLI/daemon path — historical shape).
49
+ - Pass ``store_fn=<callable>`` (MCP/LIGHT path — the callable should
50
+ match ``MemoryEngine.store(content, metadata=...)`` and return a
51
+ list of fact ids). When both are supplied, ``store_fn`` wins.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ engine=None,
57
+ config: dict | None = None,
58
+ *,
59
+ store_fn: Callable[..., Any] | None = None,
60
+ ):
47
61
  self._engine = engine
62
+ self._store_fn = store_fn
48
63
  self._config = config or {}
49
64
  self._enabled = self._config.get("enabled", True)
50
65
  self._min_confidence = self._config.get("min_confidence", 0.5)
@@ -85,18 +100,25 @@ class AutoCapture:
85
100
  return CaptureDecision(False, 0.0, "none", "no patterns matched")
86
101
 
87
102
  def capture(self, content: str, category: str = "", metadata: dict | None = None) -> bool:
88
- """Store content via engine if auto-capture decides to."""
89
- if not self._engine:
103
+ """Store content via engine or store_fn if auto-capture decides to.
104
+
105
+ Never mutates the caller's metadata dict — we copy before adding
106
+ our auto-capture bookkeeping keys. This matters because
107
+ ``pool_store`` ships the dict cross-process and callers often
108
+ reuse the same dict across captures.
109
+ """
110
+ if self._store_fn is None and self._engine is None:
90
111
  return False
91
112
 
92
113
  try:
93
- meta = metadata or {}
94
- meta["source"] = "auto-capture"
95
- meta["category"] = category
96
- fact_ids = self._engine.store(content, metadata=meta)
97
- return len(fact_ids) > 0
114
+ meta = {**(metadata or {}), "source": "auto-capture", "category": category}
115
+ if self._store_fn is not None:
116
+ fact_ids = self._store_fn(content, metadata=meta)
117
+ else:
118
+ fact_ids = self._engine.store(content, metadata=meta)
119
+ return bool(fact_ids) and len(fact_ids) > 0
98
120
  except Exception as exc:
99
- logger.debug("Auto-capture store failed: %s", exc)
121
+ logger.warning("Auto-capture store failed: %s", exc)
100
122
  return False
101
123
 
102
124
  def _match_patterns(self, content: str, patterns: list[str]) -> float:
@@ -7,7 +7,7 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import logging
10
- from typing import Any
10
+ from typing import Any, Callable
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -17,30 +17,53 @@ class AutoRecall:
17
17
 
18
18
  Called at session start or before each prompt to inject
19
19
  relevant memories without user intervention.
20
+
21
+ Two ways to wire the recall side:
22
+
23
+ - Pass ``engine=<MemoryEngine>`` (CLI/daemon path — historical shape).
24
+ - Pass ``recall_fn=<callable>`` (MCP/LIGHT path — the callable adapts
25
+ a worker-pool call to the ``RecallResponse``-shaped return value).
26
+ When both are supplied, ``recall_fn`` wins.
20
27
  """
21
28
 
22
- def __init__(self, engine=None, config: dict | None = None):
29
+ def __init__(
30
+ self,
31
+ engine=None,
32
+ config: dict | None = None,
33
+ *,
34
+ recall_fn: Callable[..., Any] | None = None,
35
+ ):
23
36
  self._engine = engine
37
+ self._recall_fn = recall_fn
24
38
  self._config = config or {}
25
39
  self._enabled = self._config.get("enabled", True)
26
40
  self._max_memories = self._config.get("max_memories_injected", 10)
27
41
  self._threshold = self._config.get("relevance_threshold", 0.3)
28
42
 
43
+ def _recall(self, query: str, limit: int):
44
+ if self._recall_fn is not None:
45
+ return self._recall_fn(query, limit=limit)
46
+ if self._engine is not None:
47
+ return self._engine.recall(query, limit=limit)
48
+ return None
49
+
29
50
  def get_session_context(self, project_path: str = "", query: str = "") -> str:
30
51
  """Get relevant context for a session or query.
31
52
 
32
53
  Returns a formatted string of relevant memories suitable
33
54
  for injection into an AI's system prompt.
34
55
  """
35
- if not self._enabled or not self._engine:
56
+ if not self._enabled:
57
+ return ""
58
+ if self._recall_fn is None and self._engine is None:
36
59
  return ""
37
60
 
38
61
  try:
39
62
  # Build query from project path or explicit query
40
63
  search_query = query or f"project context {project_path}"
41
- response = self._engine.recall(search_query, limit=self._max_memories)
64
+ response = self._recall(search_query, self._max_memories)
42
65
 
43
- if not response.results:
66
+ if response is None or not response.results:
44
67
  return ""
45
68
 
46
69
  # Filter by relevance threshold
@@ -56,7 +79,7 @@ class AutoRecall:
56
79
 
57
80
  return "\n".join(lines)
58
81
  except Exception as exc:
59
- logger.debug("Auto-recall failed: %s", exc)
82
+ logger.warning("Auto-recall failed: %s", exc)
60
83
  return ""
61
84
 
62
85
  def get_query_context(self, query: str) -> list[dict]:
@@ -64,11 +87,15 @@ class AutoRecall:
64
87
 
65
88
  Returns structured data (not formatted string) for MCP tools.
66
89
  """
67
- if not self._enabled or not self._engine:
90
+ if not self._enabled:
91
+ return []
92
+ if self._recall_fn is None and self._engine is None:
68
93
  return []
69
94
 
70
95
  try:
71
- response = self._engine.recall(query, limit=self._max_memories)
96
+ response = self._recall(query, self._max_memories)
97
+ if response is None:
98
+ return []
72
99
  results = []
73
100
  for r in response.results:
74
101
  if r.score >= self._threshold:
@@ -79,7 +106,7 @@ class AutoRecall:
79
106
  })
80
107
  return results
81
108
  except Exception as exc:
82
- logger.debug("Auto-recall query failed: %s", exc)
109
+ logger.warning("Auto-recall query failed: %s", exc)
83
110
  return []
84
111
 
85
112
  @property
@@ -0,0 +1,107 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """HTTP proxy that lets MCP processes use the daemon as their worker.
6
+
7
+ Without this, every MCP process (one per IDE) would spawn its own
8
+ ``recall_worker`` subprocess through ``WorkerPool.shared()`` and load
9
+ the ONNX embedder into that subprocess. With N IDEs open the total
10
+ RSS was approximately N x 1.6 GB — the exact failure Path B was built
11
+ to avoid.
12
+
13
+ With this proxy, the MCP process opens an HTTP connection to the
14
+ single long-lived daemon (already running for dashboard / mesh /
15
+ health) and forwards ``recall`` and ``store`` calls there. Heavy
16
+ engine state exists in exactly one process: the daemon.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import logging
22
+ import urllib.parse
23
+ import urllib.request
24
+ from typing import Any
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class DaemonPoolProxy:
30
+ """:class:`WorkerPool`-shaped facade that talks to the daemon over HTTP.
31
+
32
+ The shape matches ``WorkerPool.recall`` / ``WorkerPool.store`` so that
33
+ the existing pool adapter in ``mcp/_pool_adapter.py`` can swap between
34
+ a local subprocess pool and the daemon proxy without any adapter
35
+ change. Errors are returned as ``{"ok": False, "error": "..."}``
36
+ envelopes — the adapter is responsible for surfacing those.
37
+ """
38
+
39
+ def __init__(self, port: int, *, timeout_s: float = 30.0) -> None:
40
+ self._port = port
41
+ self._timeout = timeout_s
42
+
43
+ def _url(self, path: str) -> str:
44
+ return f"http://127.0.0.1:{self._port}{path}"
45
+
46
+ def recall(
47
+ self, query: str, limit: int = 10, session_id: str = "",
48
+ ) -> dict[str, Any]:
49
+ params = urllib.parse.urlencode(
50
+ {"q": query, "limit": limit, "session_id": session_id or ""}
51
+ )
52
+ try:
53
+ with urllib.request.urlopen(
54
+ self._url(f"/recall?{params}"), timeout=self._timeout,
55
+ ) as resp:
56
+ data = json.loads(resp.read().decode() or "{}")
57
+ except Exception as exc:
58
+ logger.warning("daemon /recall failed: %s", exc)
59
+ return {"ok": False, "error": str(exc)}
60
+ if not isinstance(data, dict):
61
+ return {"ok": False, "error": "non-dict response"}
62
+ data.setdefault("ok", True)
63
+ return data
64
+
65
+ def store(
66
+ self, content: str, metadata: dict | None = None,
67
+ ) -> dict[str, Any]:
68
+ body = json.dumps({
69
+ "content": content,
70
+ "tags": (metadata or {}).get("tags", ""),
71
+ "metadata": metadata or {},
72
+ }).encode()
73
+ req = urllib.request.Request(
74
+ self._url("/remember"),
75
+ data=body,
76
+ headers={"Content-Type": "application/json"},
77
+ method="POST",
78
+ )
79
+ try:
80
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
81
+ data = json.loads(resp.read().decode() or "{}")
82
+ except Exception as exc:
83
+ logger.warning("daemon /remember failed: %s", exc)
84
+ return {"ok": False, "error": str(exc)}
85
+ if not isinstance(data, dict):
86
+ return {"ok": False, "error": "non-dict response"}
87
+ data.setdefault("ok", True)
88
+ return data
89
+
90
+
91
+ def choose_pool() -> Any:
92
+ """Return the best available pool for this MCP process.
93
+
94
+ Preference order:
95
+ 1. Running daemon — use HTTP proxy (keeps ONNX in ONE process)
96
+ 2. No daemon — fall back to ``WorkerPool.shared()`` (spawns a
97
+ local subprocess with a FULL engine). This keeps single-user
98
+ / first-launch scenarios working.
99
+ """
100
+ try:
101
+ from superlocalmemory.cli.daemon import _get_port, is_daemon_running
102
+ if is_daemon_running():
103
+ return DaemonPoolProxy(port=_get_port())
104
+ except Exception as exc:
105
+ logger.warning("daemon probe failed — falling back to subprocess pool: %s", exc)
106
+ from superlocalmemory.core.worker_pool import WorkerPool
107
+ return WorkerPool.shared()
@@ -0,0 +1,121 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """MCP-side adapters onto the pool (daemon HTTP or local subprocess).
6
+
7
+ The pool returns plain dicts with an ``ok`` flag. Hooks
8
+ (``AutoRecall`` / ``AutoCapture``) expect a ``RecallResponse``-shaped
9
+ object and a list of fact ids. These adapters bridge the two.
10
+
11
+ On ``{"ok": False, "error": "..."}`` the adapters raise
12
+ :class:`PoolError` instead of silently returning empty results — worker
13
+ death must be distinguishable from "no memories" on the user side.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from dataclasses import dataclass, field
19
+ from typing import Any
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class PoolFact:
26
+ fact_id: str = ""
27
+ content: str = ""
28
+ memory_id: str = ""
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class PoolRecallItem:
33
+ fact: PoolFact = field(default_factory=PoolFact)
34
+ score: float = 0.0
35
+ confidence: float = 0.0
36
+ trust_score: float = 0.0
37
+ channel_scores: dict[str, float] = field(default_factory=dict)
38
+ evidence_chain: list[Any] = field(default_factory=list)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class PoolRecallResponse:
43
+ results: list[PoolRecallItem] = field(default_factory=list)
44
+ query_type: str = ""
45
+ retrieval_time_ms: float = 0.0
46
+ channel_weights: dict[str, float] = field(default_factory=dict)
47
+ total_candidates: int = 0
48
+
49
+
50
+ class PoolError(RuntimeError):
51
+ """Raised when the pool returns an error envelope.
52
+
53
+ Callers that want the old silent-empty behaviour (e.g. hook paths
54
+ that must never break the user's session) catch this and log at
55
+ WARNING. Callers that want to surface the failure (e.g. dashboard
56
+ resources) let it propagate.
57
+ """
58
+
59
+
60
+ def _pool():
61
+ """Lazy pool factory — prefers the daemon HTTP proxy.
62
+
63
+ Split out so tests can monkey-patch the factory without touching
64
+ the real ``WorkerPool.shared()`` singleton.
65
+ """
66
+ from superlocalmemory.mcp._daemon_proxy import choose_pool
67
+ return choose_pool()
68
+
69
+
70
+ def _unwrap_error(raw: Any, op: str) -> None:
71
+ """Raise PoolError if the pool returned an ``{"ok": False}`` envelope."""
72
+ if isinstance(raw, dict) and raw.get("ok") is False:
73
+ reason = raw.get("error") or "pool returned ok=False"
74
+ raise PoolError(f"pool.{op} failed: {reason}")
75
+
76
+
77
+ def pool_recall(query: str, limit: int = 10, **_: Any) -> PoolRecallResponse:
78
+ """Call pool.recall and reshape its dict into a typed response.
79
+
80
+ Raises :class:`PoolError` on worker death or any non-ok envelope.
81
+ """
82
+ raw = _pool().recall(query=query, limit=limit)
83
+ _unwrap_error(raw, "recall")
84
+ items = raw.get("results", []) if isinstance(raw, dict) else []
85
+ results = [
86
+ PoolRecallItem(
87
+ fact=PoolFact(
88
+ fact_id=item.get("fact_id", ""),
89
+ content=item.get("content", ""),
90
+ memory_id=item.get("memory_id", ""),
91
+ ),
92
+ score=float(item.get("score", 0.0)),
93
+ confidence=float(item.get("confidence", 0.0)),
94
+ trust_score=float(item.get("trust_score", 0.0)),
95
+ channel_scores=item.get("channel_scores", {}) or {},
96
+ evidence_chain=list(item.get("evidence_chain", []) or []),
97
+ )
98
+ for item in items
99
+ ]
100
+ return PoolRecallResponse(
101
+ results=results,
102
+ query_type=raw.get("query_type", "") if isinstance(raw, dict) else "",
103
+ retrieval_time_ms=float(raw.get("retrieval_time_ms", 0.0))
104
+ if isinstance(raw, dict) else 0.0,
105
+ channel_weights=raw.get("channel_weights", {})
106
+ if isinstance(raw, dict) else {},
107
+ total_candidates=int(raw.get("total_candidates", 0))
108
+ if isinstance(raw, dict) else 0,
109
+ )
110
+
111
+
112
+ def pool_store(content: str, metadata: dict | None = None) -> list[str]:
113
+ """Call pool.store and return the fact id list.
114
+
115
+ Raises :class:`PoolError` on worker death or any non-ok envelope.
116
+ """
117
+ raw = _pool().store(content=content, metadata=metadata or {})
118
+ _unwrap_error(raw, "store")
119
+ if isinstance(raw, dict):
120
+ return list(raw.get("fact_ids", []))
121
+ return []
@@ -35,9 +35,9 @@ def register_resources(server, get_engine: Callable) -> None:
35
35
  """
36
36
  try:
37
37
  from superlocalmemory.hooks.auto_recall import AutoRecall
38
- engine = get_engine()
38
+ from superlocalmemory.mcp._pool_adapter import pool_recall
39
39
  auto = AutoRecall(
40
- engine=engine,
40
+ recall_fn=pool_recall,
41
41
  config={"enabled": True, "max_memories_injected": 10, "relevance_threshold": 0.3},
42
42
  )
43
43
  context = auto.get_session_context(query="recent decisions and important context")
@@ -200,7 +200,8 @@ def register_resources(server, get_engine: Callable) -> None:
200
200
  from superlocalmemory.learning.behavioral import BehavioralPatternStore
201
201
  store = BehavioralPatternStore(engine._db.db_path)
202
202
  summary = store.get_summary(pid)
203
- except Exception:
203
+ except Exception as exc:
204
+ logger.warning("learning_status behavioral summary failed: %s", exc)
204
205
  summary = {}
205
206
 
206
207
  # Outcome stats
@@ -211,7 +212,8 @@ def register_resources(server, get_engine: Callable) -> None:
211
212
  (pid,),
212
213
  )
213
214
  outcomes = {dict(r)["outcome"]: dict(r)["c"] for r in outcome_rows}
214
- except Exception:
215
+ except Exception as exc:
216
+ logger.warning("learning_status outcome query failed: %s", exc)
215
217
  outcomes = {}
216
218
 
217
219
  lines = [
@@ -245,7 +247,8 @@ def register_resources(server, get_engine: Callable) -> None:
245
247
  (pid,),
246
248
  )
247
249
  activity = {dict(r)["action"]: dict(r)["c"] for r in audit_rows}
248
- except Exception:
250
+ except Exception as exc:
251
+ logger.warning("engagement audit-trail query failed: %s", exc)
249
252
  activity = {}
250
253
 
251
254
  # Top-accessed facts
@@ -31,26 +31,42 @@ server = FastMCP("SuperLocalMemory V3")
31
31
 
32
32
  # Lazy engine singleton -------------------------------------------------------
33
33
 
34
+ import threading as _threading
34
35
  _engine = None
36
+ _engine_lock = _threading.Lock()
35
37
 
36
38
 
37
39
  def get_engine():
38
- """Return (or create) the singleton MemoryEngine."""
40
+ """Return (or create) the singleton LIGHT MemoryEngine.
41
+
42
+ FastMCP may call tools concurrently from multiple threads. The
43
+ double-checked lock keeps construction single-shot even if two
44
+ tool invocations race on a cold process — without it we would
45
+ double-run the schema migrations and build two ``AdaptiveLearner``
46
+ instances over the same DB file.
47
+ """
39
48
  global _engine
40
- if _engine is None:
49
+ if _engine is not None:
50
+ return _engine
51
+ with _engine_lock:
52
+ if _engine is not None:
53
+ return _engine
41
54
  from superlocalmemory.core.config import SLMConfig
42
55
  from superlocalmemory.core.engine import MemoryEngine
56
+ from superlocalmemory.core.engine_capabilities import Capabilities
43
57
 
44
58
  config = SLMConfig.load()
45
- _engine = MemoryEngine(config)
46
- _engine.initialize()
59
+ new_engine = MemoryEngine(config, capabilities=Capabilities.LIGHT)
60
+ new_engine.initialize()
61
+ _engine = new_engine
47
62
  return _engine
48
63
 
49
64
 
50
65
  def reset_engine():
51
66
  """Reset engine singleton (for testing or mode switch)."""
52
67
  global _engine
53
- _engine = None
68
+ with _engine_lock:
69
+ _engine = None
54
70
 
55
71
 
56
72
  # Register tools and resources -------------------------------------------------
@@ -71,6 +87,9 @@ _ESSENTIAL_TOOLS: set[str] = {
71
87
  "list_recent", "delete_memory", "update_memory", "get_status",
72
88
  # Session lifecycle (3)
73
89
  "session_init", "observe", "close_session",
90
+ # Feedback / learning signals — reachable Dash-Core path for
91
+ # thumbs-up / pin / drift signals.
92
+ "report_feedback",
74
93
  # Memory management (2)
75
94
  "forget", "run_maintenance",
76
95
  # Infinite memory + learning (4)
@@ -161,14 +180,24 @@ register_evolution_tools(_target, get_engine) # v3.4.11: Skill evolution tools
161
180
  # the first tool call arrives (1-2s later), the engine is already warm.
162
181
  # This applies to ALL IDEs: Claude Code, Cursor, Antigravity, Gemini CLI, etc.
163
182
  def _eager_warmup() -> None:
164
- """Pre-warm engine + ensure daemon is running + auto-register mesh (background thread)."""
183
+ """Pre-warm LIGHT engine + ensure daemon is running + auto-register mesh.
184
+
185
+ LIGHT engine init is cheap (DB only, ~100 ms). The real reason this
186
+ stays in a background thread is the follow-on side effects
187
+ (``ensure_daemon``, ``auto_register_mesh``) which do I/O.
188
+ """
165
189
  import logging
166
190
  _logger = logging.getLogger(__name__)
167
191
  try:
168
192
  get_engine()
169
193
  _logger.info("MCP engine pre-warmed successfully")
170
194
  except Exception as exc:
171
- _logger.debug("MCP engine pre-warmup failed (non-fatal): %s", exc)
195
+ _logger.warning("MCP engine pre-warmup failed: %s", exc)
196
+
197
+ # Measurement / test harnesses set this to skip daemon-start and
198
+ # mesh-register. The LIGHT engine init above still runs.
199
+ if _os.environ.get("SLM_DISABLE_WARMUP_SIDE_EFFECTS") == "1":
200
+ return
172
201
 
173
202
  # V3.4.4: Also ensure daemon is running for dashboard/mesh/health features.
174
203
  # This runs in background — doesn't block MCP tool registration.
@@ -177,7 +206,7 @@ def _eager_warmup() -> None:
177
206
  if ensure_daemon():
178
207
  _logger.info("Daemon auto-started by MCP server")
179
208
  except Exception as exc:
180
- _logger.debug("Daemon auto-start failed (non-fatal): %s", exc)
209
+ _logger.warning("Daemon auto-start failed: %s", exc)
181
210
 
182
211
  # V3.4.6: Auto-register this MCP session as a mesh peer immediately.
183
212
  # Previously, registration was lazy (only on first mesh tool call).
@@ -187,7 +216,7 @@ def _eager_warmup() -> None:
187
216
  auto_register_mesh()
188
217
  _logger.info("Mesh peer auto-registered at startup")
189
218
  except Exception as exc:
190
- _logger.debug("Mesh auto-register failed (non-fatal): %s", exc)
219
+ _logger.warning("Mesh auto-register failed: %s", exc)
191
220
 
192
221
  import threading
193
222
  _warmup_thread = threading.Thread(target=_eager_warmup, daemon=True, name="mcp-warmup")
@@ -28,14 +28,18 @@ DB_PATH = MEMORY_DIR / "memory.db"
28
28
 
29
29
  def _emit_event(event_type: str, payload: dict | None = None,
30
30
  source_agent: str = "mcp_client") -> None: # V3.3.12: see also mcp/shared.py
31
- """Emit an event to the EventBus (best-effort, never raises)."""
31
+ """Emit an event to the EventBus (best-effort, never raises).
32
+
33
+ Dashboard visibility is load-bearing per the v3.4.26 user contract,
34
+ so we log on failure rather than silently dropping the signal.
35
+ """
32
36
  try:
33
37
  from superlocalmemory.infra.event_bus import EventBus
34
38
  bus = EventBus.get_instance(str(DB_PATH))
35
39
  bus.emit(event_type, payload=payload, source_agent=source_agent,
36
40
  source_protocol="mcp")
37
- except Exception:
38
- pass
41
+ except Exception as exc:
42
+ logger.warning("event emit failed: type=%s err=%s", event_type, exc)
39
43
 
40
44
 
41
45
  def _register_agent(agent_id: str, profile_id: str) -> None:
@@ -45,8 +49,10 @@ def _register_agent(agent_id: str, profile_id: str) -> None:
45
49
  registry_path = MEMORY_DIR / "agents.json"
46
50
  registry = AgentRegistry(persist_path=registry_path)
47
51
  registry.register_agent(agent_id, profile_id)
48
- except Exception:
49
- pass
52
+ except Exception as exc:
53
+ logger.warning(
54
+ "agent registry write failed: agent=%s err=%s", agent_id, exc,
55
+ )
50
56
 
51
57
 
52
58
  def register_active_tools(server, get_engine: Callable) -> None:
@@ -73,6 +79,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
73
79
  try:
74
80
  from superlocalmemory.hooks.auto_recall import AutoRecall
75
81
  from superlocalmemory.hooks.rules_engine import RulesEngine
82
+ from superlocalmemory.mcp._pool_adapter import pool_recall
76
83
 
77
84
  engine = get_engine()
78
85
  rules = RulesEngine()
@@ -82,7 +89,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
82
89
 
83
90
  recall_config = rules.get_recall_config()
84
91
  auto = AutoRecall(
85
- engine=engine,
92
+ recall_fn=pool_recall,
86
93
  config={
87
94
  "enabled": True,
88
95
  "max_memories_injected": max_results,
@@ -102,8 +109,12 @@ def register_active_tools(server, get_engine: Callable) -> None:
102
109
  feedback_count = 0
103
110
  try:
104
111
  feedback_count = engine._adaptive_learner.get_feedback_count(pid)
105
- except Exception:
106
- pass
112
+ except Exception as exc:
113
+ # Feedback count is a Dash-Core signal; a silent zero
114
+ # masks wiring bugs. Log so operators see the failure.
115
+ logger.warning(
116
+ "session_init feedback_count read failed: %s", exc,
117
+ )
107
118
 
108
119
  # Register agent + emit event
109
120
  _register_agent("mcp_client", pid)
@@ -148,12 +159,13 @@ def register_active_tools(server, get_engine: Callable) -> None:
148
159
  try:
149
160
  from superlocalmemory.hooks.auto_capture import AutoCapture
150
161
  from superlocalmemory.hooks.rules_engine import RulesEngine
162
+ from superlocalmemory.mcp._pool_adapter import pool_store
151
163
 
152
164
  engine = get_engine()
153
165
  rules = RulesEngine()
154
166
 
155
167
  auto = AutoCapture(
156
- engine=engine,
168
+ store_fn=pool_store,
157
169
  config=rules.get_capture_config(),
158
170
  )
159
171