superlocalmemory 3.4.45 → 3.4.48

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
@@ -5,7 +5,67 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ### [Unreleased]
8
+ ## [3.4.48] - 2026-05-21
9
+
10
+ **Multi-Machine Mesh Coordination — M4 & M5 now work as one.**
11
+
12
+ ### Added
13
+ - **`RemoteSyncClient` — cross-machine peer sync** (NEW in v3.4.48)
14
+ - HTTP-based sync with remote SLM instances
15
+ - Populates `broker._remote_peers` from remote `/mesh/peers` endpoint every 30s
16
+ - Environment variables:
17
+ - `SLM_MESH_PEER_URL`: Full URL of remote SLM (e.g. `http://192.168.1.100:8765`)
18
+ - `SLM_MESH_SHARED_SECRET`: Shared auth secret (required for remote mode)
19
+ - `SLM_MESH_DISCOVERY`: `'on'` (default) or `'off'` for mDNS discovery
20
+ - **mDNS discovery (optional)**: Auto-discovers remote SLM on LAN via `_slm-mesh._tcp` service
21
+ - **Message proxying**: `broker.send_message()` now proxies direct messages to remote peers
22
+ - **Graceful fallback**: Network errors logged but don't crash; optional `zeroconf` dependency
23
+ - Uses `httpx` (already in core deps) + `zeroconf>=0.140` (new, pure Python, cross-platform)
24
+
25
+ - **Auth guard on `/mesh/peers`** — Remote queries must include `Authorization: Bearer {SLM_MESH_SHARED_SECRET}`
26
+
27
+ ### Changed
28
+ - `MeshBroker` now instantiates `RemoteSyncClient` when `SLM_MESH_PEER_URL` is set or in remote mode
29
+ - `broker.send_message()` checks `to_peer in broker._remote_peers` before DB lookup
30
+ - If remote, proxies via `sync_client.send_to_remote()`
31
+ - If local or not found, uses existing DB logic
32
+ - `broker.list_all_peers()` returns local + remote peers merged
33
+
34
+ ### Tests
35
+ - 13 new tests in `tests/integration/test_remote_sync.py`
36
+ - Init, peer sync, stale peer removal, send proxy, error handling
37
+ - mDNS discovery callback stubs
38
+ - Integration: broker routes sends to remote peers
39
+
40
+ All existing tests pass. No breaking changes.
41
+
42
+ ---
43
+
44
+ ## [3.4.46] - 2026-05-18
45
+
46
+ ### Added
47
+ - **`SLM_MCP_TOOLS` env var** — Fine-grained MCP tool allowlist. Users can now
48
+ set `SLM_MCP_TOOLS=remember,recall,search,session_init` to expose exactly
49
+ the tools they need, reducing MCP context budget. Falls back to 25-tool
50
+ essential set when unset; `SLM_MCP_ALL_TOOLS=1` still wins for power users.
51
+ - **`KMP_DUPLICATE_LIB_OK=TRUE`** — Set at package init to prevent OpenMP
52
+ multi-library crashes when PyTorch, ONNX Runtime, and NumPy-MKL all load
53
+ their own runtimes simultaneously.
54
+
55
+ ### Fixed
56
+ - **WAL busy_timeout ordering** (PR #24, @kenyonxu) — `_enable_wal()` now
57
+ sets `busy_timeout` before `journal_mode=WAL`, ensuring the 10s configured
58
+ timeout is used instead of SQLite's default 5s during WAL initialization.
59
+ - **Engine init traceback logging** (PR #25, @kenyonxu) — `logger.exception()`
60
+ replaces `logger.warning()` on daemon engine init failure, capturing the
61
+ full traceback for root-cause diagnosis.
62
+ - **MCP `fast` recall wiring** (PR #22, @VikingOwl91) — `fast=True` recall
63
+ parameter now threads through the full MCP→daemon→worker stack.
64
+ `session_init` performs one `pool_recall(fast=True)` instead of two
65
+ redundant recalls. Tools switch from `WorkerPool.shared()` to `choose_pool()`
66
+ for daemon-first routing (avoids N×1.6 GB ONNX duplication across IDEs).
67
+ - **FTS trigger idempotency** — `CREATE TRIGGER IF NOT EXISTS` prevents race
68
+ crashes on repeated schema init.
9
69
 
10
70
  ---
11
71
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h1 align="center">SuperLocalMemory V3.4</h1>
6
6
  <p align="center"><strong>Every other AI forgets. Yours won't.</strong><br/><em>Infinite memory for Claude Code, Cursor, Windsurf, and any MCP-compatible AI client.</em></p>
7
- <p align="center"><code>v3.4.25</code> — Install once. Every session remembers the last. Automatically.</p>
7
+ <p align="center"><code>v3.4.48</code> — Install once. Every session remembers the last. Automatically.<br><strong>Now with multi-machine agent mesh — M4 and M5 coordinate as one.</strong></p>
8
8
  <p align="center"><strong>Backed by 3 published research papers</strong> (arXiv preprints + Zenodo-archived) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
9
9
 
10
10
  <p align="center">
@@ -416,6 +416,67 @@ Auto-capture hooks: `slm hooks install` + `slm observe` + `slm session-context`.
416
416
 
417
417
  ---
418
418
 
419
+ ## Multi-Machine Mesh Coordination (New in v3.4.48)
420
+
421
+ Run SLM on multiple machines (M4 + M5) and have your agents coordinate as one team without any disruption.
422
+
423
+ ### Setup
424
+
425
+ **M4 (broker):**
426
+ ```bash
427
+ export SLM_MESH_HOST=192.168.1.100
428
+ export SLM_MESH_SHARED_SECRET=my-secret-key
429
+ slm init # Starts SLM at http://192.168.1.100:8765
430
+ ```
431
+
432
+ **M5 (client):**
433
+ ```bash
434
+ export SLM_MESH_PEER_URL=http://192.168.1.100:8765
435
+ export SLM_MESH_SHARED_SECRET=my-secret-key
436
+ slm init # Syncs M4's agents every 30s, proxies messages to M4
437
+ ```
438
+
439
+ ### How It Works
440
+
441
+ - **HTTP-based sync** — M5 queries M4's `/mesh/peers` endpoint every 30 seconds
442
+ - **Message proxying** — When M5's agent sends a message to an M4 agent, it's routed automatically
443
+ - **mDNS discovery (optional)** — M5 can auto-discover M4 on the LAN via `_slm-mesh._tcp` (enable with `SLM_MESH_DISCOVERY=on`, default)
444
+ - **Graceful fallback** — Network errors logged but don't crash; offline agents queue messages for delivery
445
+ - **Shared secret** — `SLM_MESH_SHARED_SECRET` gates remote peer discovery (required for remote mode)
446
+
447
+ ### Environment Variables
448
+
449
+ | Variable | Default | Purpose |
450
+ |:---------|:--------|:--------|
451
+ | `SLM_MESH_HOST` | `127.0.0.1` | Host this SLM listens on (set to IP for remote) |
452
+ | `SLM_MESH_PEER_URL` | unset | Full URL of remote SLM (e.g., `http://192.168.1.100:8765`) |
453
+ | `SLM_MESH_SHARED_SECRET` | unset | Auth secret (required when remote) |
454
+ | `SLM_MESH_DISCOVERY` | `on` | mDNS discovery (`on`/`off`) |
455
+ | `SLM_MESH_WS_PORT` | `7900` | WebSocket port for mesh (internal use) |
456
+
457
+ ### Dependencies
458
+
459
+ - `zeroconf>=0.140` (new in v3.4.48, optional, pure Python, auto-installed)
460
+ - `httpx==0.28.1` (already in core deps)
461
+ - No Docker. No external broker. Works on WiFi + LAN.
462
+
463
+ ### MCP Tools
464
+
465
+ All 8 mesh tools work seamlessly across machines:
466
+
467
+ | Tool | Description |
468
+ |:-----|:------------|
469
+ | `mesh_peers` | List local + remote peers merged |
470
+ | `mesh_send` | Send message to any peer (local or remote) |
471
+ | `mesh_broadcast` | Send to all agents (across machines) |
472
+ | `mesh_project` | Send to all agents in a project (across machines) |
473
+ | `mesh_inbox` | Get messages for this agent |
474
+ | `mesh_pending` | Get offline messages (broadcast/project) |
475
+ | `mesh_state` | Get/set shared state (replicated) |
476
+ | `mesh_lock` | Acquire/release distributed file locks |
477
+
478
+ ---
479
+
419
480
  ## Features
420
481
 
421
482
  ### Retrieval
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.45",
3
+ "version": "3.4.48",
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.48"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -45,6 +45,7 @@ dependencies = [
45
45
  "fastapi[all]==0.136.1",
46
46
  "uvicorn==0.46.0",
47
47
  "websockets==16.0",
48
+ "zeroconf>=0.140",
48
49
  "lightgbm==4.6.0",
49
50
  "orjson==3.11.9",
50
51
  "tree-sitter==0.25.2",
@@ -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",
@@ -26,4 +29,12 @@ def _check_critical_deps() -> None:
26
29
  pass
27
30
 
28
31
 
29
- _check_critical_deps()
32
+ # Only run the dep check when a full (non-LIGHT) engine is in use.
33
+ # The MCP server runs in LIGHT mode — importing onnxruntime here
34
+ # breaks the LIGHT engine contract (ONNX_LOADED must stay False).
35
+ # Skip when SLM_SKIP_DEP_CHECK=1 or SLM_DISABLE_WARMUP_SIDE_EFFECTS=1.
36
+ if not (
37
+ os.environ.get("SLM_SKIP_DEP_CHECK") == "1"
38
+ or os.environ.get("SLM_DISABLE_WARMUP_SIDE_EFFECTS") == "1"
39
+ ):
40
+ _check_critical_deps()
@@ -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:
@@ -273,6 +273,10 @@ class LLMBackbone:
273
273
  headers = {
274
274
  "x-api-key": self._api_key,
275
275
  "anthropic-version": _ANTHROPIC_API_VERSION,
276
+ # Enable prompt caching — system prompt cached as ephemeral block.
277
+ # Requires ≥1024 tokens in the cached block to activate.
278
+ # Savings: ~90% cost reduction on cached input tokens.
279
+ "anthropic-beta": "prompt-caching-2024-07-31",
276
280
  "Content-Type": "application/json",
277
281
  }
278
282
  payload: dict[str, Any] = {
@@ -282,7 +286,14 @@ class LLMBackbone:
282
286
  "messages": [{"role": "user", "content": prompt}],
283
287
  }
284
288
  if system:
285
- payload["system"] = system
289
+ # Structured block with cache_control — plain string disables caching.
290
+ payload["system"] = [
291
+ {
292
+ "type": "text",
293
+ "text": system,
294
+ "cache_control": {"type": "ephemeral"},
295
+ }
296
+ ]
286
297
  return _ANTHROPIC_URL, headers, payload
287
298
 
288
299
  def _build_azure(
@@ -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 = [
@@ -19,6 +19,10 @@ _os.environ.setdefault('PYTORCH_MPS_MEM_LIMIT', '0')
19
19
  _os.environ.setdefault('PYTORCH_ENABLE_MPS_FALLBACK', '1')
20
20
  _os.environ.setdefault('TOKENIZERS_PARALLELISM', 'false')
21
21
  _os.environ.setdefault('TORCH_DEVICE', 'cpu')
22
+ # LIGHT engine contract: suppress the top-level dep check in __init__.py
23
+ # that unconditionally imports onnxruntime. MCP runs LIGHT-only — ONNX
24
+ # must never load in this process.
25
+ _os.environ.setdefault('SLM_SKIP_DEP_CHECK', '1')
22
26
 
23
27
  import logging
24
28
  import sys
@@ -122,13 +126,21 @@ _ESSENTIAL_TOOLS = frozenset(_ESSENTIAL_TOOLS)
122
126
 
123
127
  _all_tools = _os_reg.environ.get("SLM_MCP_ALL_TOOLS") == "1"
124
128
 
129
+ # v3.4.45: Minimal mode — explicit user allowlist via SLM_MCP_TOOLS env var.
130
+ # Format: comma-separated tool names, e.g. "remember,recall,session_init,search"
131
+ # Use case: Claude Code consumer plans with tight context budgets where the
132
+ # 25-tool essential set is still too many. Power users override to expose
133
+ # exactly the tools they invoke. Falls back to _ESSENTIAL_TOOLS when unset.
134
+ _user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
135
+
125
136
 
126
137
  class _FilteredServer:
127
138
  """Wraps FastMCP to only register essential tools.
128
139
 
129
140
  Non-essential tools are silently skipped (not registered on the MCP
130
141
  server). They remain available via CLI. When SLM_MCP_ALL_TOOLS=1,
131
- all tools are registered (bypass filter).
142
+ all tools are registered (bypass filter). When SLM_MCP_TOOLS is set,
143
+ that user allowlist is used instead of _ESSENTIAL_TOOLS.
132
144
  """
133
145
  __slots__ = ("_server", "_allowed")
134
146
 
@@ -147,8 +159,14 @@ class _FilteredServer:
147
159
  return getattr(self._server, name)
148
160
 
149
161
 
150
- # Choose full or filtered registration target
151
- _target = server if _all_tools else _FilteredServer(server, _ESSENTIAL_TOOLS)
162
+ # Choose registration target (precedence: ALL > user allowlist > essential)
163
+ if _all_tools:
164
+ _target = server
165
+ elif _user_allowlist_str:
166
+ _user_allowlist = frozenset(t.strip() for t in _user_allowlist_str.split(",") if t.strip())
167
+ _target = _FilteredServer(server, _user_allowlist)
168
+ else:
169
+ _target = _FilteredServer(server, _ESSENTIAL_TOOLS)
152
170
 
153
171
  from superlocalmemory.mcp.tools_core import register_core_tools
154
172
  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]:
@@ -19,8 +19,19 @@ import time
19
19
  import uuid
20
20
  from datetime import datetime, timezone
21
21
  from pathlib import Path
22
+ from typing import Any
22
23
 
23
24
  logger = logging.getLogger("superlocalmemory.mesh")
25
+ import os as _os
26
+
27
+ # Remote sync support (optional, try/except to avoid import issues)
28
+ try:
29
+ from .remote_sync import RemoteSyncClient
30
+ except ImportError:
31
+ RemoteSyncClient = None # type: ignore
32
+
33
+ LOCAL_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
34
+
24
35
 
25
36
 
26
37
  MAX_MESSAGE_SIZE = 4096 # 4KB cap — mesh messages are notifications, not data dumps
@@ -42,6 +53,39 @@ class MeshBroker:
42
53
  self._started_at = time.monotonic()
43
54
  self._cleanup_thread: threading.Thread | None = None
44
55
  self._stop_event = threading.Event()
56
+ self._host = _os.environ.get("SLM_MESH_HOST", "127.0.0.1")
57
+ self._shared_secret = _os.environ.get("SLM_MESH_SHARED_SECRET", "") or None
58
+ self._is_remote = self._host not in LOCAL_HOSTS
59
+ self._ws_port = int(_os.environ.get("SLM_MESH_WS_PORT", "7900"))
60
+ self._discovery_enabled = self._is_remote and _os.environ.get("SLM_MESH_DISCOVERY", "on") != "off"
61
+ self._remote_peers: dict[str, dict] = {}
62
+ self._peer_url: str | None = _os.environ.get("SLM_MESH_PEER_URL", "") or None
63
+ self._sync_client: Any = None
64
+ if self._is_remote and not self._shared_secret:
65
+ raise RuntimeError(
66
+ "SLM_MESH_SHARED_SECRET is required when SLM_MESH_HOST is not localhost"
67
+ )
68
+
69
+
70
+ # -- Remote / Multi-Machine support (v3.4.47) --
71
+
72
+ def get_remote_peers(self) -> list[dict]:
73
+ """Return peers from discovered remote brokers."""
74
+ return list(self._remote_peers.values())
75
+
76
+ def add_remote_peer(self, peer_id: str, info: dict) -> None:
77
+ """Register a peer from a remote broker."""
78
+ self._remote_peers[peer_id] = info
79
+
80
+ def remove_remote_peer(self, peer_id: str) -> None:
81
+ """Remove a remote peer."""
82
+ self._remote_peers.pop(peer_id, None)
83
+
84
+ def list_all_peers(self) -> list[dict]:
85
+ """Return local + remote peers merged."""
86
+ local = self.list_peers()
87
+ remote = self.get_remote_peers()
88
+ return local + remote
45
89
 
46
90
  def start_cleanup(self) -> None:
47
91
  """Start background cleanup thread for stale peers/messages."""
@@ -50,8 +94,17 @@ class MeshBroker:
50
94
  )
51
95
  self._cleanup_thread.start()
52
96
 
97
+ # Start remote sync client if peer URL configured or remote mode
98
+ if RemoteSyncClient and (
99
+ self._peer_url or (self._is_remote and self._host not in LOCAL_HOSTS)
100
+ ):
101
+ self._sync_client = RemoteSyncClient(self)
102
+ self._sync_client.start()
103
+
53
104
  def stop(self) -> None:
54
105
  self._stop_event.set()
106
+ if self._sync_client:
107
+ self._sync_client.stop()
55
108
 
56
109
  # -- Connection helper --
57
110
 
@@ -65,11 +118,13 @@ class MeshBroker:
65
118
  # -- Peers --
66
119
 
67
120
  def register_peer(self, session_id: str, summary: str = "",
68
- host: str = "127.0.0.1", port: int = 0,
121
+ host: str = "", port: int = 0,
69
122
  project_path: str = "", agent_type: str = "unknown") -> dict:
70
123
  conn = self._conn()
71
124
  try:
72
125
  now = datetime.now(timezone.utc).isoformat()
126
+ if not host:
127
+ host = self._host
73
128
  # Idempotent: update if same session_id exists
74
129
  existing = conn.execute(
75
130
  "SELECT peer_id FROM mesh_peers WHERE session_id = ?",
@@ -178,6 +233,14 @@ class MeshBroker:
178
233
  to_peer = "project"
179
234
  else:
180
235
  target_type = "peer"
236
+ # Check if this is a remote peer — proxy to remote SLM
237
+ if to_peer in self._remote_peers and self._sync_client:
238
+ return self._sync_client.send_to_remote(to_peer, {
239
+ "from_peer": from_peer,
240
+ "to": to_peer,
241
+ "content": content,
242
+ "type": msg_type,
243
+ })
181
244
  # Verify recipient exists for direct messages
182
245
  if not conn.execute("SELECT 1 FROM mesh_peers WHERE peer_id=?", (to_peer,)).fetchone():
183
246
  return {"ok": False, "error": "recipient peer not found"}