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 +61 -1
- package/README.md +62 -1
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/superlocalmemory/__init__.py +13 -2
- package/src/superlocalmemory/core/recall_worker.py +5 -3
- package/src/superlocalmemory/core/worker_pool.py +2 -1
- package/src/superlocalmemory/llm/backbone.py +12 -1
- package/src/superlocalmemory/mcp/_daemon_proxy.py +7 -3
- package/src/superlocalmemory/mcp/_pool_adapter.py +7 -2
- package/src/superlocalmemory/mcp/server.py +21 -3
- package/src/superlocalmemory/mcp/tools_active.py +31 -18
- package/src/superlocalmemory/mcp/tools_core.py +5 -6
- package/src/superlocalmemory/mcp/tools_v3.py +2 -2
- package/src/superlocalmemory/mesh/broker.py +64 -1
- package/src/superlocalmemory/mesh/remote_sync.py +266 -0
- package/src/superlocalmemory/server/routes/mesh.py +14 -1
- package/src/superlocalmemory/server/unified_daemon.py +2 -2
- package/src/superlocalmemory/storage/database.py +1 -1
- package/src/superlocalmemory/storage/schema.py +3 -3
- package/src/superlocalmemory.egg-info/PKG-INFO +771 -0
- package/src/superlocalmemory.egg-info/SOURCES.txt +457 -0
- package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
- package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
- package/src/superlocalmemory.egg-info/requires.txt +65 -0
- package/src/superlocalmemory.egg-info/top_level.txt +1 -0
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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, **
|
|
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(
|
|
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
|
|
151
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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.
|
|
157
|
-
pool =
|
|
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.
|
|
289
|
-
raw =
|
|
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 = "
|
|
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"}
|