superlocalmemory 3.8.13 → 4.0.0

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 (212) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -121
  3. package/README.md +65 -63
  4. package/docs/pi-dev-integration.md +1 -1
  5. package/package.json +6 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +1 -1
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +1 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +1 -1
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  29. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  31. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  32. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +11 -4
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/cli/commands.py +125 -11
  41. package/src/superlocalmemory/cli/daemon.py +5 -1
  42. package/src/superlocalmemory/cli/main.py +35 -2
  43. package/src/superlocalmemory/cli/ops_cmd.py +281 -0
  44. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  45. package/src/superlocalmemory/compliance/audit.py +65 -0
  46. package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
  47. package/src/superlocalmemory/compliance/gdpr.py +416 -20
  48. package/src/superlocalmemory/compliance/retention.py +74 -22
  49. package/src/superlocalmemory/compliance/scheduler.py +78 -9
  50. package/src/superlocalmemory/core/actor_context.py +166 -0
  51. package/src/superlocalmemory/core/admission.py +549 -0
  52. package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
  53. package/src/superlocalmemory/core/config.py +202 -24
  54. package/src/superlocalmemory/core/consolidation_engine.py +13 -13
  55. package/src/superlocalmemory/core/context_cache.py +28 -0
  56. package/src/superlocalmemory/core/embeddings.py +64 -2
  57. package/src/superlocalmemory/core/engine.py +7 -2
  58. package/src/superlocalmemory/core/engine_ingestion.py +65 -3
  59. package/src/superlocalmemory/core/engine_wiring.py +36 -9
  60. package/src/superlocalmemory/core/ingest_policy.py +38 -0
  61. package/src/superlocalmemory/core/maintenance.py +255 -0
  62. package/src/superlocalmemory/core/modes.py +40 -13
  63. package/src/superlocalmemory/core/mutations.py +437 -44
  64. package/src/superlocalmemory/core/operation_policy.py +92 -0
  65. package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
  66. package/src/superlocalmemory/core/operation_request.py +127 -0
  67. package/src/superlocalmemory/core/ops_remediation.py +542 -0
  68. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  69. package/src/superlocalmemory/core/remember_runtime.py +202 -4
  70. package/src/superlocalmemory/core/remote_mode.py +20 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +150 -0
  72. package/src/superlocalmemory/core/topic_signature.py +19 -4
  73. package/src/superlocalmemory/core/transactions/__init__.py +78 -0
  74. package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
  75. package/src/superlocalmemory/core/transactions/erasure.py +825 -0
  76. package/src/superlocalmemory/core/transactions/manifest.py +255 -0
  77. package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
  78. package/src/superlocalmemory/core/transactions/obligations.py +272 -0
  79. package/src/superlocalmemory/core/transactions/owners.py +114 -0
  80. package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
  81. package/src/superlocalmemory/core/transactions/service.py +330 -0
  82. package/src/superlocalmemory/core/worker_pool.py +33 -5
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
  84. package/src/superlocalmemory/encoding/emotional.py +75 -14
  85. package/src/superlocalmemory/encoding/scene_builder.py +115 -13
  86. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  87. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  88. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  89. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  90. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  91. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  92. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  93. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  94. package/src/superlocalmemory/evolution/types.py +30 -8
  95. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  96. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  97. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  98. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  99. package/src/superlocalmemory/infra/backup.py +434 -7
  100. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  101. package/src/superlocalmemory/infra/self_heal.py +401 -0
  102. package/src/superlocalmemory/learning/feedback.py +52 -9
  103. package/src/superlocalmemory/loops/engine.py +10 -0
  104. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  105. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  106. package/src/superlocalmemory/mcp/profiles.py +5 -0
  107. package/src/superlocalmemory/mcp/resources.py +8 -0
  108. package/src/superlocalmemory/mcp/server.py +51 -4
  109. package/src/superlocalmemory/mcp/shared.py +19 -0
  110. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  111. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  112. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  113. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  114. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  115. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  116. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  117. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  118. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  119. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  120. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  121. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  122. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  123. package/src/superlocalmemory/mesh/broker.py +124 -46
  124. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  125. package/src/superlocalmemory/mesh/discovery.py +365 -0
  126. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  127. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  128. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  129. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  130. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  131. package/src/superlocalmemory/optimize/config/store.py +45 -0
  132. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  133. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  134. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  135. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  136. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  137. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  138. package/src/superlocalmemory/retrieval/engine.py +19 -4
  139. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  140. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  141. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  142. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  143. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  144. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  145. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  146. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  147. package/src/superlocalmemory/server/api.py +5 -5
  148. package/src/superlocalmemory/server/egress_policy.py +258 -0
  149. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  150. package/src/superlocalmemory/server/route_mutations.py +20 -0
  151. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  152. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  153. package/src/superlocalmemory/server/routes/events.py +15 -0
  154. package/src/superlocalmemory/server/routes/memories.py +56 -3
  155. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  156. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  157. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  158. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  159. package/src/superlocalmemory/server/routes/ws.py +86 -0
  160. package/src/superlocalmemory/server/ui.py +6 -6
  161. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  162. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  163. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  164. package/src/superlocalmemory/storage/database.py +329 -24
  165. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  166. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  167. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  168. package/src/superlocalmemory/storage/migration_runner.py +140 -417
  169. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  170. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  171. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  172. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  173. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  174. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  175. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  176. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
  177. package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
  178. package/src/superlocalmemory/storage/schema.py +67 -0
  179. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  180. package/src/superlocalmemory/trust/scorer.py +28 -4
  181. package/src/superlocalmemory/ui/index.html +14 -3
  182. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  183. package/src/superlocalmemory/ui/js/brain.js +6 -4
  184. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  185. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  186. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  187. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  188. package/src/superlocalmemory/ui/js/modal.js +272 -5
  189. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  190. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  191. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  192. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  193. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  194. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  195. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  196. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  197. package/src/superlocalmemory/ui/js/settings.js +7 -1
  198. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  199. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  200. package/src/superlocalmemory/cli/post_install.py +0 -114
  201. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  202. package/src/superlocalmemory/core/db_pool.py +0 -80
  203. package/src/superlocalmemory/core/error_catalog.py +0 -113
  204. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  205. package/src/superlocalmemory/core/priority_queue.py +0 -61
  206. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  207. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  208. package/src/superlocalmemory/core/slmignore.py +0 -125
  209. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  210. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  211. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  212. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -1,63 +1,33 @@
1
1
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
2
  # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
3
+ # Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
4
4
 
5
- """Resource-safe FastMCP Streamable-HTTP integration Workstream E additions.
5
+ """Resource-safe Streamable-HTTP integration for mcp==2.0.0.
6
6
 
7
- MCP SDK 1.27.1 passes an AnyIO ``MemoryObjectReceiveStream`` directly to
8
- ``EventSourceResponse`` for every JSON-RPC POST. The response consumes that
9
- stream but does not close it after normal iteration, leaving one receive
10
- endpoint per request for the garbage collector. The response is the owner of
11
- that per-request iterator, so SLM closes it at the response boundary.
7
+ mcp 2.0.0 deleted ``mcp.server.fastmcp.FastMCP``. Replacement is
8
+ ``mcp.server.mcpserver.MCPServer`` with the same ``.tool()`` decorator and
9
+ ``run(transport="stdio")``.
12
10
 
13
- Workstream E (3.8.4): MCP Connection Resilience
14
- ------------------------------------------------
15
- Two root causes of frequent MCP disconnects are addressed here:
11
+ Fully-stateless is the default (see ``remote_mode.mcp_stateless`` and
12
+ ``unified_daemon._configure_mcp_transport_settings``). Under
13
+ ``stateless_http=True``:
16
14
 
17
- RC-2: No session idle timeout zombie sessions accumulate forever.
18
- Fix: ``SLMFastMCP.streamable_http_app()`` pre-creates the
19
- ``StreamableHTTPSessionManager`` with a finite ``session_idle_timeout``
20
- (default 600 s, overridable via ``SLM_MCP_SESSION_IDLE_TIMEOUT_S``).
15
+ * ``session_idle_timeout`` is illegal (SDK raises RuntimeError) and unused
16
+ there are no transport sessions to reap.
17
+ * the EventStore (SSE Last-Event-ID resumability) is not used.
18
+ * therefore the old ``SLMFastMCP.streamable_http_app()`` override that
19
+ pre-created a ``StreamableHTTPSessionManager`` is gone — kwargs go
20
+ straight to ``MCPServer.streamable_http_app(...)``.
21
21
 
22
- RC-3: No EventStore every SSE drop requires a full re-initialize.
23
- Fix: ``SLMInMemoryEventStore`` (bounded, in-memory, async-safe) is
24
- injected as the session manager's event store. A dropped SSE stream can
25
- resume via ``Last-Event-ID`` instead of forcing a new ``initialize``
26
- handshake.
27
-
28
- KNOWN LIMITATIONS (not fixed here, documented for future work):
29
- * RC-1 (mcp-remote orphan test-sessions): The mcp-remote v0.1.38 bug
30
- creates a ``testTransport``/``testClient`` that is never closed. This
31
- leaks one zombie session per mcp-remote startup. Tracked upstream.
32
- Session idle-timeout mitigates the accumulation.
33
- * Client reconnect after daemon restart: ``SLMInMemoryEventStore`` is
34
- in-memory only and does not survive daemon process restart. After a
35
- restart, stale ``Last-Event-ID`` values are unknown to the new store;
36
- ``replay_events_after`` returns ``None`` and the client must
37
- re-initialize. A SQLite-backed EventStore is planned for v3.9.
38
- * Claude Code client bug Anthropic #48557: Claude Code may send a stale
39
- MCP session ID after server restart. This is a client-side defect;
40
- SLM cannot fix it from the server.
41
- * Event-loop stall during background maintenance (RC-6): The pruner-lock
42
- stall fix lives in Workstream A+F (already merged into this branch).
43
- This module depends on that fix; see fix/3.8.4 merge commit.
22
+ Application-level ``session_init`` / ``close_session`` are orthogonal
23
+ (session_id is a str param persisted in the memories table) and unchanged.
44
24
  """
45
25
 
46
26
  from __future__ import annotations
47
27
 
48
28
  import logging
49
- import os
50
- from collections import OrderedDict, deque
51
- from typing import Any
52
29
 
53
- from mcp.server.fastmcp import FastMCP
54
- from mcp.server.streamable_http import (
55
- EventCallback,
56
- EventId,
57
- EventMessage,
58
- EventStore,
59
- StreamId,
60
- )
30
+ from mcp.server.mcpserver import MCPServer
61
31
  from sse_starlette.sse import EventSourceResponse
62
32
  from starlette.types import Receive, Scope, Send
63
33
 
@@ -65,196 +35,9 @@ from superlocalmemory import __version__
65
35
 
66
36
  logger = logging.getLogger(__name__)
67
37
 
68
- # ---------------------------------------------------------------------------
69
- # Session idle timeout — configurable default
70
- # ---------------------------------------------------------------------------
71
-
72
- #: Default session idle timeout in seconds.
73
- #:
74
- #: Rationale for 600 s (10 minutes), not the SDK's suggested 1800 s:
75
- #: * At 1800 s, with mcp-remote leaking 3 zombie sessions per conversation
76
- #: start and a typical conversation cadence of one every 15 min,
77
- #: up to (1800/15) × 3 = 360 zombie sessions can accumulate in the worst
78
- #: case before the first one expires. 600 s bounds this to ~6.
79
- #: * 600 s is well above the 15 s SSE keepalive interval, the typical
80
- #: in-conversation idle gap (<5 min), and the mcp-remote reconnect window.
81
- #: * Active sessions push their idle deadline forward on every tool call,
82
- #: so a user making any request within 10 min never loses their session.
83
- #: * A session idle for exactly 10 min (user stepped away) is reaped and
84
- #: recreated transparently on next tool call via mcp-remote reconnect.
85
- #:
86
- #: CRIT — anti-patterns to avoid:
87
- #: Too aggressive (<60 s): frequent idle-deadline checks waste CPU in
88
- #: AnyIO's CancelScope machinery; legitimate quiet users are interrupted.
89
- #: Too lenient (>7200 s = 2 h): zombie sessions accumulate at rates that
90
- #: can reach 100+ entries, degrading AnyIO task-group scheduling.
91
- _DEFAULT_SESSION_IDLE_TIMEOUT: float = 600.0
92
-
93
-
94
- def _slm_session_idle_timeout() -> float:
95
- """Return the configured MCP session idle timeout in seconds.
96
-
97
- Reads ``SLM_MCP_SESSION_IDLE_TIMEOUT_S`` from the environment.
98
- Falls back to :data:`_DEFAULT_SESSION_IDLE_TIMEOUT` on missing or
99
- non-numeric values. A value ≤ 0 is also treated as the default.
100
- """
101
- raw = os.environ.get("SLM_MCP_SESSION_IDLE_TIMEOUT_S", "").strip()
102
- if raw:
103
- try:
104
- val = float(raw)
105
- if val > 0:
106
- return val
107
- except ValueError:
108
- logger.warning(
109
- "SLM_MCP_SESSION_IDLE_TIMEOUT_S=%r is not a valid number; "
110
- "using default %s s",
111
- raw,
112
- _DEFAULT_SESSION_IDLE_TIMEOUT,
113
- )
114
- return _DEFAULT_SESSION_IDLE_TIMEOUT
115
-
116
38
 
117
39
  # ---------------------------------------------------------------------------
118
- # SLMInMemoryEventStore bounded, in-memory, async-safe
119
- # ---------------------------------------------------------------------------
120
-
121
-
122
- class SLMInMemoryEventStore(EventStore):
123
- """Bounded in-memory event store for MCP SSE resumability.
124
-
125
- Enables clients to reconnect to the GET /mcp SSE endpoint and replay
126
- missed events via the ``Last-Event-ID`` header instead of performing a
127
- full ``initialize`` handshake (which creates a new session and adds to
128
- the zombie-session count).
129
-
130
- Design choices:
131
- ---------------
132
- * **Per-stream bounded deque** — each stream_id maps to a
133
- ``deque(maxlen=max_events_per_stream)``. Oldest events are evicted
134
- automatically (FIFO) when the cap is hit.
135
- * **Global stream count cap** — the store tracks at most
136
- ``max_streams`` distinct stream_ids. When exceeded, the least-recently
137
- used stream is evicted from the dict. This bounds total memory usage.
138
- * **Async-safe, lock-free** — all access happens on a single asyncio event
139
- loop; no ``asyncio.Lock`` is needed.
140
- * **Priming events (message=None) are stored but skipped during replay** —
141
- the SDK mints a fresh priming event in the replay path (see
142
- ``StreamableHTTPServerTransport._replay_events``).
143
-
144
- Memory bound:
145
- -------------
146
- ``max_events_per_stream`` × ~2 KB (avg JSONRPCMessage) × ``max_streams``
147
- = 200 × 2 KB × 100 = ~40 MB in the absolute worst case.
148
- At steady state with idle-timeout reaping sessions after 10 min, the
149
- typical load is 3–6 active streams × 200 events × 2 KB ≈ 1.2–2.4 MB.
150
-
151
- Known limitation:
152
- -----------------
153
- This store is in-memory only. It does not survive a daemon process
154
- restart. After a restart, any ``Last-Event-ID`` from a previous process
155
- is unknown; ``replay_events_after`` returns ``None`` and the client must
156
- re-initialize. A SQLite-backed EventStore is planned for v3.9.
157
- """
158
-
159
- def __init__(
160
- self,
161
- max_events_per_stream: int = 200,
162
- max_streams: int = 100,
163
- ) -> None:
164
- """Initialise the bounded event store.
165
-
166
- Args:
167
- max_events_per_stream: Maximum number of events stored per
168
- stream before oldest events are dropped. Default 200
169
- (~400 KB per stream at 2 KB/event).
170
- max_streams: Maximum number of distinct stream_ids tracked.
171
- When exceeded, the least-recently used stream is evicted
172
- (all its events are lost). Default 100.
173
- """
174
- self._max_events = max_events_per_stream
175
- self._max_streams = max_streams
176
- # OrderedDict maintains insertion/access order for LRU eviction.
177
- # stream_id → deque[(event_id, message)]
178
- self._store: OrderedDict[str, deque] = OrderedDict()
179
- # Monotonically incrementing counter; single event loop → no lock needed.
180
- self._counter: int = 0
181
-
182
- # ------------------------------------------------------------------
183
- # EventStore ABC implementation
184
- # ------------------------------------------------------------------
185
-
186
- async def store_event(
187
- self,
188
- stream_id: StreamId,
189
- message: Any, # JSONRPCMessage | None
190
- ) -> EventId:
191
- """Store an event for the given stream and return its unique event_id.
192
-
193
- Args:
194
- stream_id: The stream (GET /mcp SSE connection) this event belongs to.
195
- message: The JSON-RPC message, or ``None`` for priming events.
196
-
197
- Returns:
198
- A monotonically incrementing string event_id.
199
- """
200
- self._counter += 1
201
- event_id: EventId = str(self._counter)
202
-
203
- if stream_id not in self._store:
204
- # Evict oldest stream if at capacity
205
- if len(self._store) >= self._max_streams:
206
- oldest_stream, _ = self._store.popitem(last=False)
207
- logger.debug(
208
- "SLMInMemoryEventStore: evicted oldest stream %r "
209
- "(max_streams=%d reached)",
210
- oldest_stream,
211
- self._max_streams,
212
- )
213
- self._store[stream_id] = deque(maxlen=self._max_events)
214
- else:
215
- # Move to "most recently used" end so LRU eviction works correctly
216
- self._store.move_to_end(stream_id)
217
-
218
- self._store[stream_id].append((event_id, message))
219
- return event_id
220
-
221
- async def replay_events_after(
222
- self,
223
- last_event_id: EventId,
224
- send_callback: EventCallback,
225
- ) -> StreamId | None:
226
- """Replay events that occurred after ``last_event_id``.
227
-
228
- Searches all tracked streams for ``last_event_id`` and forwards every
229
- subsequent non-None event to ``send_callback``. Priming events
230
- (``message=None``) are skipped; the SDK mints a fresh priming event
231
- in the replay path.
232
-
233
- Args:
234
- last_event_id: The ID of the last event the client received.
235
- send_callback: Async callback that receives each missed
236
- ``EventMessage`` in order.
237
-
238
- Returns:
239
- The stream_id that contained ``last_event_id``, or ``None`` if
240
- the event was not found (client must re-initialize).
241
- """
242
- for stream_id, events in self._store.items():
243
- found = False
244
- for event_id, message in events:
245
- if found:
246
- if message is not None:
247
- await send_callback(EventMessage(message=message, event_id=event_id))
248
- elif event_id == last_event_id:
249
- found = True
250
- if found:
251
- return stream_id
252
- # last_event_id not in any stream (evicted or never seen)
253
- return None
254
-
255
-
256
- # ---------------------------------------------------------------------------
257
- # SSE resource guard (unchanged from pre-E)
40
+ # SSE resource guard (still useful if a host opts into non-json SSE responses)
258
41
  # ---------------------------------------------------------------------------
259
42
 
260
43
 
@@ -278,109 +61,25 @@ def install_streamable_http_resource_guard() -> None:
278
61
 
279
62
 
280
63
  # ---------------------------------------------------------------------------
281
- # SLMFastMCP — session lifecycle + SSE cleanup
64
+ # SLMFastMCP — thin MCPServer wrapper (name kept for import stability)
282
65
  # ---------------------------------------------------------------------------
283
66
 
284
67
 
285
- class SLMFastMCP(FastMCP):
286
- """FastMCP with SLM release identity, deterministic SSE cleanup, and
287
- session lifecycle management (Workstream E).
288
-
289
- Additions over the base FastMCP:
68
+ class SLMFastMCP(MCPServer):
69
+ """MCPServer with SLM release identity.
290
70
 
291
- 1. **Session idle timeout**: the ``StreamableHTTPSessionManager`` is
292
- pre-created with a finite ``session_idle_timeout`` so zombie sessions
293
- (from mcp-remote orphan test-connects, or abandoned conversations) are
294
- automatically reaped instead of accumulating forever.
71
+ Named ``SLMFastMCP`` for backward-compatible imports. Behaviour is
72
+ fully-stateless by default at the *call site* via
73
+ ``streamable_http_app(stateless_http=True, json_response=True, ...)``
74
+ this class does not override that method.
295
75
 
296
- 2. **Bounded event store**: a ``SLMInMemoryEventStore`` is injected so
297
- that a dropped SSE stream can resume via ``Last-Event-ID`` without a
298
- full ``initialize`` round-trip.
299
-
300
- 3. **Ordering guard**: ``streamable_http_app()`` reads the ``stateless_http``
301
- flag from ``self.settings`` at call time. If the flag is ``True``
302
- (set by ``_configure_mcp_transport_settings()`` in unified_daemon.py),
303
- ``session_idle_timeout`` is suppressed — the MCP SDK raises
304
- ``RuntimeError`` if both are set simultaneously. If
305
- ``streamable_http_app()`` is called before settings are configured
306
- (e.g. in unit tests), the default ``stateless_http=False`` applies
307
- safely.
76
+ ``version`` is passed to ``MCPServer.__init__`` directly (no private
77
+ ``_mcp_server.version`` poke that attribute is gone in mcp 2.0.0).
308
78
  """
309
79
 
310
80
  def __init__(self, *args, product_version: str = __version__, **kwargs) -> None:
81
+ # MCPServer takes version= as a first-class kwarg.
82
+ kwargs.setdefault("version", product_version)
311
83
  super().__init__(*args, **kwargs)
312
- # FastMCP delegates the initialize response to the low-level MCP
313
- # server. Without an explicit value it reports the installed ``mcp``
314
- # library version, which makes an SLM 3.7 server identify itself as
315
- # (for example) 1.27.1 to every IDE client.
316
- self._mcp_server.version = product_version
317
-
318
- def streamable_http_app(self): # type: ignore[override]
319
- """Return the Streamable-HTTP Starlette app.
320
-
321
- Pre-creates the ``StreamableHTTPSessionManager`` with SLM-specific
322
- parameters before delegating to ``super()`` (which skips re-creation
323
- because the manager is already set).
324
-
325
- Ordering guarantee
326
- ------------------
327
- In production ``unified_daemon.py`` always calls
328
- ``_configure_mcp_transport_settings(fastmcp)`` *before* this method,
329
- so ``self.settings.stateless_http`` reflects the correct runtime value.
330
- If this method is called first (unit tests, embedded hosts), the
331
- default ``stateless_http=False`` is used — safe because the stateless
332
- guard only matters when ``stateless_http=True`` (avoid SDK RuntimeError).
333
-
334
- Dependency note (A+F)
335
- ---------------------
336
- This method assumes that ``fix/3.8.4-A+F`` (already merged into this
337
- branch) has eliminated the pruner-lock stall that caused event-loop
338
- starvation during background maintenance. Session idle-timeout and
339
- the event store improve resilience to transient drops, but they cannot
340
- compensate for a fully stalled event loop.
341
- """
84
+ # Optional SSE resource guard for hosts that still request event-stream.
342
85
  install_streamable_http_resource_guard()
343
-
344
- if self._session_manager is None:
345
- from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
346
-
347
- # Ordering guard: read stateless_http defensively.
348
- # ``_configure_mcp_transport_settings()`` may not have been called
349
- # yet; ``getattr`` with a False default ensures we never pass
350
- # session_idle_timeout=<value> into a stateless manager (SDK raises
351
- # RuntimeError if both are set simultaneously).
352
- is_stateless: bool = getattr(self.settings, "stateless_http", False)
353
-
354
- # Idle timeout: finite for stateful sessions; None for stateless
355
- # (stateless sessions have no identity — there is nothing to reap).
356
- idle_timeout: float | None = (
357
- None if is_stateless else _slm_session_idle_timeout()
358
- )
359
-
360
- # Event store: inject SLMInMemoryEventStore for stateful mode.
361
- # Stateless mode must not receive an event store (SDK limitation).
362
- # If the caller already configured an event store (via FastMCP
363
- # constructor argument), respect it — do not replace with ours.
364
- if is_stateless:
365
- event_store: EventStore | None = None
366
- else:
367
- event_store = self._event_store or SLMInMemoryEventStore()
368
-
369
- self._session_manager = StreamableHTTPSessionManager(
370
- app=self._mcp_server,
371
- event_store=event_store,
372
- retry_interval=self._retry_interval,
373
- json_response=self.settings.json_response,
374
- stateless=is_stateless,
375
- security_settings=self.settings.transport_security,
376
- session_idle_timeout=idle_timeout,
377
- )
378
- logger.info(
379
- "SLM MCP session manager created: stateless=%s, "
380
- "idle_timeout=%ss, event_store=%s",
381
- is_stateless,
382
- idle_timeout,
383
- type(event_store).__name__ if event_store else "None",
384
- )
385
-
386
- return super().streamable_http_app()
@@ -49,6 +49,11 @@ _PROFILE_FULL: frozenset[str] = frozenset({ # 34 base — EXPLICIT literal, NOT
49
49
  "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
50
50
  # v3.8.0: bounded-loop tools (CLI + /slm-loop command + MCP).
51
51
  "slm_loop_run", "slm_loop_history", "slm_loop_show",
52
+ # v4: prestage_context IS registered (see mcp/server.py) but is deliberately
53
+ # NOT in full/power. The profile names are a user-facing config contract
54
+ # (SLM_MCP_PROFILE=full42) and the published tool-count table depends on
55
+ # them; adding a tool here would make "full42" serve 43 tools. New tools
56
+ # reach users through the `whole` profile until a profile rename is shipped.
52
57
  }) | _PROFILE_FULL_MESH # 42
53
58
 
54
59
  _PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 54
@@ -36,8 +36,16 @@ def register_resources(server, get_engine: Callable) -> None:
36
36
  try:
37
37
  from superlocalmemory.hooks.auto_recall import AutoRecall
38
38
  from superlocalmemory.mcp._pool_adapter import pool_recall
39
+ # Pass engine so the soft-prompt bridge (engine->AutoInvoker) fires on
40
+ # this real MCP session-context surface; recall_fn still drives recall.
41
+ # Fail-soft: if the engine is unavailable, recall via pool_recall alone.
42
+ try:
43
+ _eng = get_engine()
44
+ except Exception:
45
+ _eng = None
39
46
  auto = AutoRecall(
40
47
  recall_fn=pool_recall,
48
+ engine=_eng,
41
49
  config={"enabled": True, "max_memories_injected": 10, "relevance_threshold": 0.3},
42
50
  )
43
51
  context = auto.get_session_context(query="recent decisions and important context")
@@ -1,8 +1,8 @@
1
1
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
2
  # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
3
+ # Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
4
4
 
5
- """SuperLocalMemory V3 — MCP Server.
5
+ """SuperLocalMemory V4 — MCP Server.
6
6
 
7
7
  Clean MCP server calling V3 MemoryEngine. Supports all MCP-compatible IDEs.
8
8
 
@@ -27,11 +27,13 @@ _os.environ.setdefault('SLM_SKIP_DEP_CHECK', '1')
27
27
  import logging
28
28
  import sys
29
29
 
30
+ from superlocalmemory import __version__ as _slm_version
30
31
  from superlocalmemory.mcp.http_transport import SLMFastMCP
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
 
34
- server = SLMFastMCP("SuperLocalMemory V3")
35
+ # mcp 2.0.0: MCPServer takes version= directly (no private _mcp_server poke).
36
+ server = SLMFastMCP("SuperLocalMemory V4", version=_slm_version)
35
37
 
36
38
  # Lazy engine singleton -------------------------------------------------------
37
39
 
@@ -81,7 +83,7 @@ def reset_engine():
81
83
  # Antigravity, Windsurf) and a maximal SLM registration crowds out
82
84
  # other MCP servers the user may have installed.
83
85
  # Admin/diagnostics tools remain available via CLI (`slm <command>`).
84
- # Set SLM_MCP_ALL_TOOLS=1 to enable all 84 tools (power users).
86
+ # Set SLM_MCP_ALL_TOOLS=1 to enable all 87 tools (power users).
85
87
 
86
88
  import os as _os_reg
87
89
 
@@ -97,6 +99,11 @@ _ESSENTIAL_TOOLS: set[str] = {
97
99
  "report_feedback",
98
100
  # Memory management (2)
99
101
  "forget", "run_maintenance",
102
+ # NOTE: prestage_context IS registered (see register_prestage_tool below)
103
+ # but is deliberately absent from the default surface. _ESSENTIAL_TOOLS must
104
+ # stay at 42 to match the full42 profile: the profile names are a
105
+ # user-facing config contract and the published tool-count table depends on
106
+ # them. New tools reach users via the `whole` profile until a rename ships.
100
107
  # Infinite memory + learning (4)
101
108
  "consolidate_cognitive", "get_soft_prompts",
102
109
  "set_mode", "report_outcome",
@@ -263,6 +270,46 @@ from superlocalmemory.mcp.tools_optimize import register_optimize_tools
263
270
  register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-free)
264
271
  from superlocalmemory.mcp.tools_loops import register_loop_tools
265
272
  register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
273
+ from superlocalmemory.mcp.tools_ops import register_ops_tools
274
+ register_ops_tools(_target, get_engine) # Wave-3: operational recovery & admin remediation
275
+ from superlocalmemory.mcp.tools_context import register_prestage_tool
276
+
277
+
278
+ def _prestage_recall(query: str, limit: int, profile_id: str, as_of: str | None = None):
279
+ """Bridge engine.recall → prestage_context recall_fn shape."""
280
+ engine = get_engine()
281
+ if hasattr(engine, "profile_id") and profile_id:
282
+ try:
283
+ engine.profile_id = profile_id
284
+ except Exception:
285
+ pass
286
+ try:
287
+ if as_of is not None:
288
+ results = engine.recall(query, limit=limit, as_of=as_of)
289
+ else:
290
+ results = engine.recall(query, limit=limit)
291
+ except TypeError:
292
+ results = engine.recall(query, limit=limit)
293
+ out: list[dict] = []
294
+ for r in results or []:
295
+ if isinstance(r, dict):
296
+ out.append({
297
+ "id": str(r.get("fact_id") or r.get("id") or ""),
298
+ "text": str(r.get("content") or r.get("text") or ""),
299
+ "score": float(r.get("score") or r.get("relevance_score") or 0.0),
300
+ "source": str(r.get("source") or "recall"),
301
+ })
302
+ else:
303
+ out.append({
304
+ "id": str(getattr(r, "fact_id", "") or ""),
305
+ "text": str(getattr(r, "content", "") or ""),
306
+ "score": float(getattr(r, "score", 0.0) or 0.0),
307
+ "source": "recall",
308
+ })
309
+ return out
310
+
311
+
312
+ register_prestage_tool(_target, _prestage_recall)
266
313
 
267
314
 
268
315
  # Keep stdio MCP processes thin until a tool truly needs a local LIGHT engine.
@@ -63,13 +63,32 @@ def authorize_mcp_mutation(
63
63
  MCP URL/env agent identifiers remain audit attribution only. The trusted
64
64
  principal is derived from the private local install capability, matching
65
65
  the canonical ingestion and recall-worker mutation boundaries.
66
+
67
+ Phase 1: Routes through the admission registry so enterprise policy applies
68
+ even when the caller has local trust (trust-hook alone cannot bypass registry).
69
+ AdmissionDenied propagates to the caller; personal OWNER is always admitted.
66
70
  """
67
71
  if operation not in {"update", "delete"}:
68
72
  raise ValueError("MCP mutations must use update or delete policy")
69
73
 
74
+ from superlocalmemory.core.actor_context import Transport
75
+ from superlocalmemory.core.admission import (
76
+ AdmissionDenied,
77
+ _resolve_deployment,
78
+ admit,
79
+ resolve_actor,
80
+ )
70
81
  from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
82
+ from superlocalmemory.core.operation_request import OperationKind
71
83
  from superlocalmemory.mcp.agent_context import get_current_agent_id
72
84
 
85
+ deployment = _resolve_deployment()
86
+ tier = "enterprise" if deployment.is_enterprise else "personal"
87
+ mode = "company" if deployment.is_enterprise else "local"
88
+ actor = resolve_actor(Transport.MCP, tier=tier, mode=mode)
89
+ kind = OperationKind.FORGET if operation == "delete" else OperationKind.CORRECT
90
+ admit(kind, actor, mode=mode) # raises AdmissionDenied on deny
91
+
73
92
  source_agent_id = get_current_agent_id(env_fallback=True)
74
93
  context = {
75
94
  "operation": operation,
@@ -22,6 +22,10 @@ import logging
22
22
  import uuid
23
23
  from typing import TYPE_CHECKING, Callable
24
24
 
25
+ from mcp.types import ToolAnnotations
26
+
27
+ from superlocalmemory.core.admission import admits
28
+ from superlocalmemory.core.operation_request import OperationKind
25
29
  from superlocalmemory.infra.data_root import state_path
26
30
  from superlocalmemory.mcp.shared import authorize_mcp_mutation
27
31
  from superlocalmemory.storage.read_connection import ReadConnectionFactory
@@ -257,7 +261,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
257
261
  # ------------------------------------------------------------------
258
262
  # 1. session_init — Auto-recall project context at session start
259
263
  # ------------------------------------------------------------------
260
- @server.tool()
264
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
261
265
  async def session_init(
262
266
  project_path: str = "",
263
267
  query: str = "",
@@ -289,7 +293,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
289
293
  from superlocalmemory.mcp._pool_adapter import pool_recall
290
294
 
291
295
  engine = get_engine()
292
- rules = RulesEngine()
296
+ rules = RulesEngine(config_path=state_path("config.json"))
293
297
 
294
298
  if not rules.should_recall("session_start"):
295
299
  return {
@@ -436,6 +440,20 @@ def register_active_tools(server, get_engine: Callable) -> None:
436
440
  # weaker ad-hoc path when the mandatory renderer fails.
437
441
  context = ""
438
442
 
443
+ # Live soft-prompt injection (Phase 5): prepend the profile's behavioral
444
+ # soft prompt so it reaches the session_init context agents actually
445
+ # consume (same engine->AutoInvoker bridge AutoRecall uses). Fail-soft.
446
+ try:
447
+ _sp_getter = getattr(
448
+ getattr(engine, "_auto_invoker", None),
449
+ "_get_soft_prompt_text", None,
450
+ )
451
+ _soft_prompt = _sp_getter() if callable(_sp_getter) else ""
452
+ if _soft_prompt:
453
+ context = f"{_soft_prompt}\n\n{context}" if context else _soft_prompt
454
+ except Exception as exc:
455
+ logger.warning("session_init soft-prompt injection failed: %s", exc)
456
+
439
457
  # GAP-FIX (v3.4.65 delivery-lead): the memories[] array is part of
440
458
  # the MCP response Claude Code ingests — it MUST be bounded too, not
441
459
  # just the rendered `context` string. Previously full unclamped
@@ -544,6 +562,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
544
562
  # 2. observe — Auto-capture decisions/bugs/preferences
545
563
  # ------------------------------------------------------------------
546
564
  @server.tool()
565
+ @admits(OperationKind.REMEMBER)
547
566
  async def observe(
548
567
  content: str,
549
568
  agent_id: str | None = None,
@@ -568,7 +587,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
568
587
  from superlocalmemory.hooks.rules_engine import RulesEngine
569
588
  from superlocalmemory.mcp._pool_adapter import pool_store
570
589
 
571
- rules = RulesEngine()
590
+ rules = RulesEngine(config_path=state_path("config.json"))
572
591
 
573
592
  auto = AutoCapture(
574
593
  store_fn=pool_store,
@@ -626,6 +645,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
626
645
  # 3. report_feedback — Explicit feedback for learning
627
646
  # ------------------------------------------------------------------
628
647
  @server.tool()
648
+ @admits(OperationKind.REMEMBER)
629
649
  async def report_feedback(
630
650
  fact_id: str,
631
651
  feedback: str = "relevant",
@@ -741,6 +761,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
741
761
  # ------------------------------------------------------------------
742
762
 
743
763
  @server.tool()
764
+ @admits(OperationKind.CONSOLIDATE)
744
765
  async def close_session(session_id: str = "") -> dict:
745
766
  """Close the current session and create temporal summary events.
746
767
 
@@ -794,10 +815,10 @@ def register_active_tools(server, get_engine: Callable) -> None:
794
815
  # ------------------------------------------------------------------
795
816
 
796
817
  @server.tool()
818
+ @admits(OperationKind.CORRECT)
797
819
  async def core_memory(
798
820
  action: str,
799
821
  fact_id: str = "",
800
- profile_id: str = "default",
801
822
  ) -> dict:
802
823
  """Manage the explicit Core Memory pin set (v3.4.65).
803
824