superlocalmemory 3.6.5 → 3.6.7

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.
@@ -34,11 +34,17 @@ import signal
34
34
  import sys
35
35
  import threading
36
36
  import time
37
- from contextlib import asynccontextmanager
37
+ from contextlib import asynccontextmanager, AsyncExitStack
38
38
  from datetime import datetime, timezone
39
39
  from pathlib import Path
40
40
  from typing import Optional
41
41
 
42
+ # v3.6.7: Tell mcp/server.py it is being imported inside the daemon process.
43
+ # This suppresses the three side-effect threads (mcp-warmup, parent-watchdog,
44
+ # stdin-eof-monitor) that are harmful when the MCP server runs embedded.
45
+ # Must be set BEFORE any import of superlocalmemory.mcp.server.
46
+ os.environ.setdefault("SLM_MCP_EMBEDDED", "1")
47
+
42
48
  from fastapi import FastAPI, HTTPException, Request
43
49
  from fastapi.middleware.cors import CORSMiddleware
44
50
  from fastapi.middleware.gzip import GZipMiddleware
@@ -95,32 +101,20 @@ class EngineRecallAdapter:
95
101
  self._engine._db.get_memory_content_batch(memory_ids)
96
102
  if memory_ids else {}
97
103
  )
98
- results = []
99
- for r in response.results[:limit]:
100
- fact_type = getattr(r.fact, "fact_type", None)
101
- lifecycle = getattr(r.fact, "lifecycle", None)
102
- results.append({
103
- "fact_id": r.fact.fact_id,
104
- "memory_id": r.fact.memory_id,
105
- "content": _sanitize_json_text(r.fact.content[:300]),
106
- "source_content": _sanitize_json_text(memory_map.get(r.fact.memory_id, "")),
107
- "score": round(r.score, 4),
108
- "confidence": round(r.confidence, 4),
109
- "trust_score": round(r.trust_score, 4),
110
- "channel_scores": {
111
- k: round(v, 4)
112
- for k, v in (r.channel_scores or {}).items()
113
- },
114
- "fact_type": fact_type.value
115
- if fact_type and hasattr(fact_type, "value") else "",
116
- "lifecycle": lifecycle.value
117
- if lifecycle and hasattr(lifecycle, "value") else "",
118
- "access_count": getattr(r.fact, "access_count", 0),
119
- "created_at": getattr(r.fact, "created_at", "") or "",
120
- "evidence_chain": list(
121
- getattr(r, "evidence_chain", []) or []
122
- ),
123
- })
104
+ # v3.6.6: same shared chokepoint as the HTTP route — identical output.
105
+ from superlocalmemory.server.recall_serializer import (
106
+ serialize_recall_response,
107
+ )
108
+ _rc = getattr(self._engine._config, "retrieval", None)
109
+ results, no_confident_match = serialize_recall_response(
110
+ response,
111
+ limit=limit,
112
+ memory_map={k: _sanitize_json_text(v) for k, v in memory_map.items()},
113
+ per_fact_max=getattr(_rc, "recall_per_fact_max_chars", 2400),
114
+ total_max=getattr(_rc, "recall_total_max_chars", 12000),
115
+ )
116
+ for _r in results:
117
+ _r["content"] = _sanitize_json_text(_r.get("content", ""))
124
118
  return {
125
119
  "ok": True,
126
120
  "query": query,
@@ -133,6 +127,7 @@ class EngineRecallAdapter:
133
127
  },
134
128
  "total_candidates": getattr(response, "total_candidates", 0),
135
129
  "results": results,
130
+ "no_confident_match": no_confident_match,
136
131
  }
137
132
 
138
133
 
@@ -861,7 +856,20 @@ async def lifespan(application: FastAPI):
861
856
  except Exception as e:
862
857
  logger.warning("optimize module not available: %s", e)
863
858
 
864
- yield
859
+ # v3.6.7: Start MCP Streamable-HTTP session manager (GOTCHA #1).
860
+ # streamable_http_app() carries its own Starlette lifespan that initialises
861
+ # an anyio task group inside the session manager. Without entering that
862
+ # lifespan every POST /mcp 500s with "Task group is not initialized."
863
+ # AsyncExitStack enters the context only when _mcp_app was mounted; if the
864
+ # mount failed (non-fatal) the daemon starts normally without HTTP MCP.
865
+ async with AsyncExitStack() as _mcp_stack:
866
+ if _mcp_app is not None:
867
+ await _mcp_stack.enter_async_context(
868
+ _mcp_app.router.lifespan_context(_mcp_app)
869
+ )
870
+ logger.info("MCP HTTP session manager started (Streamable-HTTP on /mcp)")
871
+
872
+ yield
865
873
 
866
874
  # Cancel optimize metrics flush loop + run final flush before shutdown
867
875
  try:
@@ -1160,6 +1168,28 @@ def create_app() -> FastAPI:
1160
1168
  # -- Daemon-specific routes --
1161
1169
  _register_daemon_routes(application)
1162
1170
 
1171
+ # -- v3.6.7: MCP Streamable-HTTP transport at /mcp --
1172
+ # Mount the FastMCP server as a Starlette ASGI sub-app so ALL clients
1173
+ # (Claude Code sessions, subagents, desktop, hermes) share ONE daemon
1174
+ # process instead of spawning an `slm mcp` subprocess per connection.
1175
+ # The session manager lifespan is started in lifespan() via AsyncExitStack.
1176
+ # Fail-open: if import or mount fails, stdio transport keeps working.
1177
+ #
1178
+ # streamable_http_path is set to "/" so that when mounted at "/mcp" the
1179
+ # effective user-facing endpoint is exactly http://127.0.0.1:8765/mcp.
1180
+ # (FastAPI strips the mount prefix before passing the request to the
1181
+ # sub-app, so the sub-app's internal route must be "/".)
1182
+ try:
1183
+ from superlocalmemory.mcp.server import server as _mcp_fastmcp
1184
+ _mcp_fastmcp.settings.streamable_http_path = "/"
1185
+ _mcp_fastmcp._session_manager = None # Defensive reset for idempotency
1186
+ global _mcp_app
1187
+ _mcp_app = _mcp_fastmcp.streamable_http_app()
1188
+ application.mount("/mcp", _mcp_app)
1189
+ logger.info("MCP HTTP transport mounted at /mcp (Streamable HTTP, port 8765)")
1190
+ except Exception as _mcp_exc: # pragma: no cover — defensive
1191
+ logger.warning("MCP HTTP mount failed (non-fatal, stdio still works): %s", _mcp_exc)
1192
+
1163
1193
  return application
1164
1194
 
1165
1195
 
@@ -1213,7 +1243,9 @@ def _register_dashboard_routes(application: FastAPI) -> None:
1213
1243
  # for Gemini), never X-SLM-API-Key. Verified: auth_middleware.py:50-82
1214
1244
  # returns False for POST when api_key file exists and X-SLM-API-Key
1215
1245
  # is absent.
1216
- _AUTH_EXEMPT_PREFIXES = ("/v1/", "/v1beta/")
1246
+ # v3.6.7: /mcp is also exempt — MCP clients negotiate their own session
1247
+ # via the MCP protocol; they have no knowledge of X-SLM-API-Key.
1248
+ _AUTH_EXEMPT_PREFIXES = ("/v1/", "/v1beta/", "/mcp")
1217
1249
 
1218
1250
  @application.middleware("http")
1219
1251
  async def auth_middleware(request, call_next):
@@ -1563,6 +1595,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
1563
1595
  q: str = "", query: str = "", limit: int = 20,
1564
1596
  session_id: str = "",
1565
1597
  fast: bool = False,
1598
+ full: bool = False,
1599
+ include_source: bool = False,
1566
1600
  ):
1567
1601
  _update_activity()
1568
1602
  search_query = q or query # Accept both ?q= and ?query= for compatibility
@@ -1610,33 +1644,23 @@ def _register_daemon_routes(application: FastAPI) -> None:
1610
1644
  engine._db.get_memory_content_batch(memory_ids)
1611
1645
  if memory_ids else {}
1612
1646
  )
1613
- results = []
1614
- for r in response.results[:limit]:
1615
- fact_type = getattr(r.fact, "fact_type", None)
1616
- lifecycle = getattr(r.fact, "lifecycle", None)
1617
- results.append({
1618
- "fact_id": r.fact.fact_id,
1619
- "memory_id": r.fact.memory_id,
1620
- "content": _sanitize_json_text(r.fact.content),
1621
- "source_content": _sanitize_json_text(memory_map.get(r.fact.memory_id, "")),
1622
- "score": round(r.score, 4),
1623
- "confidence": round(r.confidence, 4),
1624
- "trust_score": round(r.trust_score, 4),
1625
- "channel_scores": {
1626
- k: round(v, 4)
1627
- for k, v in (r.channel_scores or {}).items()
1628
- },
1629
- "fact_type": fact_type.value
1630
- if fact_type and hasattr(fact_type, "value")
1631
- else getattr(r.fact, "fact_type", ""),
1632
- "lifecycle": lifecycle.value
1633
- if lifecycle and hasattr(lifecycle, "value") else "",
1634
- "access_count": getattr(r.fact, "access_count", 0),
1635
- "created_at": getattr(r.fact, "created_at", "") or "",
1636
- "evidence_chain": list(
1637
- getattr(r, "evidence_chain", []) or []
1638
- ),
1639
- })
1647
+ # v3.6.6: single shared serialization chokepoint — budget + source
1648
+ # discipline + no_confident_match, identical across every surface.
1649
+ from superlocalmemory.server.recall_serializer import (
1650
+ serialize_recall_response,
1651
+ )
1652
+ _rc = getattr(engine._config, "retrieval", None)
1653
+ results, no_confident_match = serialize_recall_response(
1654
+ response,
1655
+ limit=limit,
1656
+ memory_map={k: _sanitize_json_text(v) for k, v in memory_map.items()},
1657
+ per_fact_max=getattr(_rc, "recall_per_fact_max_chars", 2400),
1658
+ total_max=getattr(_rc, "recall_total_max_chars", 12000),
1659
+ full=full,
1660
+ include_source=include_source,
1661
+ )
1662
+ for _r in results:
1663
+ _r["content"] = _sanitize_json_text(_r.get("content", ""))
1640
1664
  return {
1641
1665
  "ok": True,
1642
1666
  "query": search_query,
@@ -1650,6 +1674,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
1650
1674
  "total_candidates": getattr(response, "total_candidates", 0),
1651
1675
  "results": results,
1652
1676
  "count": len(results),
1677
+ "no_confident_match": no_confident_match,
1653
1678
  }
1654
1679
  except Exception as exc:
1655
1680
  raise HTTPException(500, detail=str(exc))
@@ -1838,6 +1863,10 @@ def _update_activity():
1838
1863
 
1839
1864
  _start_time: float | None = None
1840
1865
 
1866
+ # v3.6.7: Starlette app returned by mcp_server.streamable_http_app().
1867
+ # Set in create_app(); consumed by lifespan() to start the session manager.
1868
+ _mcp_app = None
1869
+
1841
1870
 
1842
1871
  # ---------------------------------------------------------------------------
1843
1872
  # Server entry point
@@ -411,3 +411,6 @@ class RecallResponse:
411
411
  channel_weights: dict[str, float] = field(default_factory=dict)
412
412
  total_candidates: int = 0
413
413
  retrieval_time_ms: float = 0.0
414
+ # v3.6.6: Evidence floor. True when floor gates out ALL results.
415
+ # Additive field — backward compatible (defaults to False).
416
+ no_confident_match: bool = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.6.5
3
+ Version: 3.6.7
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: AGPL-3.0-or-later
@@ -401,6 +401,32 @@ slm status
401
401
 
402
402
  ### MCP Integration (Claude, Cursor, Windsurf, VS Code, etc.)
403
403
 
404
+ SLM supports **two MCP transports** — use whichever fits your tool. Both expose the same 33 tools and 7 resources.
405
+
406
+ #### Option A — HTTP transport (v3.6.7+, recommended)
407
+
408
+ One shared process handles every client. RAM is flat regardless of how many IDE windows, subagents, or concurrent sessions connect. Requires the SLM daemon to be running (`slm start`).
409
+
410
+ ```json
411
+ {
412
+ "mcpServers": {
413
+ "superlocalmemory": {
414
+ "type": "http",
415
+ "url": "http://127.0.0.1:8765/mcp/"
416
+ }
417
+ }
418
+ }
419
+ ```
420
+
421
+ > **Claude Code** also accepts:
422
+ > ```bash
423
+ > claude mcp add --transport http superlocalmemory http://127.0.0.1:8765/mcp/
424
+ > ```
425
+
426
+ #### Option B — stdio transport (universal, works everywhere)
427
+
428
+ Spawns one `slm mcp` subprocess per client connection (~90–110 MB each). Works with every MCP-compatible tool including those that do not yet support HTTP transport. No daemon required.
429
+
404
430
  ```json
405
431
  {
406
432
  "mcpServers": {
@@ -412,7 +438,36 @@ slm status
412
438
  }
413
439
  ```
414
440
 
415
- 33 MCP tools by default (+42 optional behind `SLM_MCP_ALL_TOOLS=1`) + 7 resources. Works with any MCP-compatible client we ship templated configs for Claude Code, Cursor, Windsurf, VS Code Copilot, Continue, Cody, ChatGPT Desktop, Gemini CLI, JetBrains, Zed, and Antigravity (15 IDE configs in `ide/configs/`). **V3.3: Adaptive lifecycle, smart compression, and pattern learning.**
441
+ #### Option C `mcp-remote` bridge (for stdio-only tools that want HTTP)
442
+
443
+ Some CLIs (e.g. Grok CLI) only speak stdio but you still want the RAM benefit of HTTP. The [`@modelcontextprotocol/client-cli`](https://www.npmjs.com/package/@modelcontextprotocol/client-cli) package bridges them:
444
+
445
+ ```bash
446
+ npm install -g @modelcontextprotocol/client-cli
447
+ ```
448
+
449
+ ```json
450
+ {
451
+ "mcpServers": {
452
+ "superlocalmemory": {
453
+ "command": "mcp-remote",
454
+ "args": ["http://127.0.0.1:8765/mcp/", "--allow-http", "--transport", "http-only"]
455
+ }
456
+ }
457
+ }
458
+ ```
459
+
460
+ #### When to use which
461
+
462
+ | Situation | Use |
463
+ |-----------|-----|
464
+ | Claude Code / Claude Desktop (v3.6.7+) | **HTTP** — zero new processes per session |
465
+ | Cursor, Windsurf, Gemini CLI, Antigravity | **HTTP** — native support |
466
+ | Grok CLI, tools that only support stdio | **`mcp-remote` bridge** |
467
+ | Offline / daemon-free usage | **stdio** |
468
+ | Any tool, any version | **stdio** always works as fallback |
469
+
470
+ See [`docs/ide-setup.md`](docs/ide-setup.md) for per-IDE configs. 33 MCP tools by default (+42 optional behind `SLM_MCP_ALL_TOOLS=1`) + 7 resources. Works with any MCP-compatible client — we ship templated configs for Claude Code, Cursor, Windsurf, VS Code Copilot, Continue, Cody, ChatGPT Desktop, Gemini CLI, JetBrains, Zed, and Antigravity (15 IDE configs in `ide/configs/`).
416
471
 
417
472
  ### Dual Interface: MCP + CLI
418
473
 
@@ -73,6 +73,7 @@ src/superlocalmemory/compliance/retention.py
73
73
  src/superlocalmemory/compliance/scheduler.py
74
74
  src/superlocalmemory/core/__init__.py
75
75
  src/superlocalmemory/core/backend_orchestrator.py
76
+ src/superlocalmemory/core/block_hygiene.py
76
77
  src/superlocalmemory/core/clock_monitor.py
77
78
  src/superlocalmemory/core/config.py
78
79
  src/superlocalmemory/core/consolidation_engine.py
@@ -92,6 +93,7 @@ src/superlocalmemory/core/graph_analyzer.py
92
93
  src/superlocalmemory/core/graph_pruner.py
93
94
  src/superlocalmemory/core/health_monitor.py
94
95
  src/superlocalmemory/core/hooks.py
96
+ src/superlocalmemory/core/ingest_gate.py
95
97
  src/superlocalmemory/core/injection.py
96
98
  src/superlocalmemory/core/loop_watchdog.py
97
99
  src/superlocalmemory/core/maintenance.py
@@ -358,6 +360,7 @@ src/superlocalmemory/retrieval/vector_store.py
358
360
  src/superlocalmemory/server/__init__.py
359
361
  src/superlocalmemory/server/api.py
360
362
  src/superlocalmemory/server/bandit_loops.py
363
+ src/superlocalmemory/server/recall_serializer.py
361
364
  src/superlocalmemory/server/security_middleware.py
362
365
  src/superlocalmemory/server/ui.py
363
366
  src/superlocalmemory/server/unified_daemon.py