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
@@ -24,6 +24,8 @@ from typing import Callable
24
24
 
25
25
  from mcp.types import ToolAnnotations
26
26
 
27
+ from superlocalmemory.core.admission import admits
28
+ from superlocalmemory.core.operation_request import OperationKind
27
29
  from superlocalmemory.mcp.shared import authorize_mcp_mutation
28
30
 
29
31
  logger = logging.getLogger(__name__)
@@ -35,6 +37,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
35
37
  """Register learning MCP tools for two-way intelligence."""
36
38
 
37
39
  @server.tool()
40
+ @admits(OperationKind.REMEMBER)
38
41
  async def log_tool_event(
39
42
  tool_name: str,
40
43
  event_type: str = "invoke",
@@ -144,6 +147,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
144
147
  return {"assertions": [], "count": 0, "error": str(exc)}
145
148
 
146
149
  @server.tool()
150
+ @admits(OperationKind.CORRECT)
147
151
  async def reinforce_assertion(assertion_id: str) -> dict:
148
152
  """Reinforce a behavioral assertion (increase confidence).
149
153
 
@@ -164,6 +168,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
164
168
  )
165
169
  result = _update_assertion_confidence(
166
170
  engine._db, assertion_id, reinforce=True,
171
+ profile_id=engine.profile_id,
167
172
  )
168
173
  if result.get("success"):
169
174
  authorization.complete()
@@ -172,6 +177,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
172
177
  return {"success": False, "error": str(exc)}
173
178
 
174
179
  @server.tool()
180
+ @admits(OperationKind.FORGET)
175
181
  async def contradict_assertion(assertion_id: str) -> dict:
176
182
  """Contradict a behavioral assertion (decrease confidence).
177
183
 
@@ -192,6 +198,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
192
198
  )
193
199
  result = _update_assertion_confidence(
194
200
  engine._db, assertion_id, reinforce=False,
201
+ profile_id=engine.profile_id,
195
202
  )
196
203
  if result.get("success"):
197
204
  authorization.complete()
@@ -200,14 +207,16 @@ def register_learning_tools(server, get_engine: Callable) -> None:
200
207
  return {"success": False, "error": str(exc)}
201
208
 
202
209
 
203
- def _update_assertion_confidence(db, assertion_id: str, reinforce: bool) -> dict:
204
- """Bayesian confidence update for behavioral assertions."""
210
+ def _update_assertion_confidence(
211
+ db, assertion_id: str, reinforce: bool, profile_id: str,
212
+ ) -> dict:
213
+ """Bayesian confidence update for behavioral assertions, scoped to one profile."""
205
214
  now = datetime.now(timezone.utc).isoformat()
206
215
  try:
207
216
  row = db.execute(
208
217
  "SELECT confidence, reinforcement_count, contradiction_count "
209
- "FROM behavioral_assertions WHERE id = ?",
210
- (assertion_id,),
218
+ "FROM behavioral_assertions WHERE id = ? AND profile_id = ?",
219
+ (assertion_id, profile_id),
211
220
  )
212
221
  rows = list(row)
213
222
  if not rows:
@@ -221,22 +230,24 @@ def _update_assertion_confidence(db, assertion_id: str, reinforce: bool) -> dict
221
230
  db.execute(
222
231
  "UPDATE behavioral_assertions SET confidence = ?, "
223
232
  "reinforcement_count = reinforcement_count + 1, "
224
- "last_reinforced_at = ?, updated_at = ? WHERE id = ?",
225
- (round(new_conf, 4), now, now, assertion_id),
233
+ "last_reinforced_at = ?, updated_at = ? "
234
+ "WHERE id = ? AND profile_id = ?",
235
+ (round(new_conf, 4), now, now, assertion_id, profile_id),
226
236
  )
227
237
  else:
228
238
  new_conf = old_conf * 0.7 # 30% decay
229
239
  db.execute(
230
240
  "UPDATE behavioral_assertions SET confidence = ?, "
231
241
  "contradiction_count = contradiction_count + 1, "
232
- "last_contradicted_at = ?, updated_at = ? WHERE id = ?",
233
- (round(new_conf, 4), now, now, assertion_id),
242
+ "last_contradicted_at = ?, updated_at = ? "
243
+ "WHERE id = ? AND profile_id = ?",
244
+ (round(new_conf, 4), now, now, assertion_id, profile_id),
234
245
  )
235
246
  # Auto-delete if confidence drops below 0.2
236
247
  if new_conf < 0.2:
237
248
  db.execute(
238
- "DELETE FROM behavioral_assertions WHERE id = ?",
239
- (assertion_id,),
249
+ "DELETE FROM behavioral_assertions WHERE id = ? AND profile_id = ?",
250
+ (assertion_id, profile_id),
240
251
  )
241
252
  return {
242
253
  "success": True, "action": "deleted",
@@ -39,9 +39,12 @@ from typing import Any, Callable
39
39
 
40
40
  from mcp.types import ToolAnnotations
41
41
 
42
+ from superlocalmemory.core.admission import admits
43
+ from superlocalmemory.core.operation_request import OperationKind
42
44
  from superlocalmemory.loops import (
43
45
  Bounds,
44
46
  LapResult,
47
+ LedgerEntry,
45
48
  Verdict,
46
49
  engine_backed_ledger,
47
50
  run_bounded_loop,
@@ -66,20 +69,6 @@ _MAX_NAME_CHARS = 128
66
69
  _MAX_QUERY_CHARS = 2000
67
70
 
68
71
 
69
- def _top_score(resp: Any) -> float:
70
- """Highest result score in a RecallResponse (0.0 when there are none)."""
71
- best = 0.0
72
- for r in getattr(resp, "results", None) or []:
73
- s = getattr(r, "score", None)
74
- if s is None:
75
- s = getattr(r, "relevance_score", 0.0) or 0.0
76
- try:
77
- best = max(best, float(s))
78
- except (TypeError, ValueError):
79
- continue
80
- return best
81
-
82
-
83
72
  def register_loop_tools(server, get_engine: Callable) -> None:
84
73
  """Register the 3 bounded-loop tools on *server*.
85
74
 
@@ -88,6 +77,7 @@ def register_loop_tools(server, get_engine: Callable) -> None:
88
77
  """
89
78
 
90
79
  @server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
80
+ @admits(OperationKind.REMEMBER)
91
81
  async def slm_loop_run(
92
82
  name: str,
93
83
  gate_query: str,
@@ -157,14 +147,35 @@ def register_loop_tools(server, get_engine: Callable) -> None:
157
147
 
158
148
  def gate(lap: int) -> Verdict:
159
149
  resp = engine.recall(gate_query, limit=3, fast=True)
160
- results = getattr(resp, "results", None) or []
150
+ all_results = getattr(resp, "results", None) or []
151
+ # Exclude the loop's own audit records from the gate evaluation.
152
+ # Each lap writes a LedgerEntry (JSON with "run_id" + "lap") into
153
+ # SLM via store_fast; the FTS5 trigger indexes every such insert.
154
+ # Without this filter BM25 finds those self-authored records on the
155
+ # next lap and the gate falsely passes. Only memories written by an
156
+ # external agent — whose content does not parse as a LedgerEntry —
157
+ # can satisfy an independent gate.
158
+ results = [
159
+ r for r in all_results
160
+ if LedgerEntry.from_json(
161
+ getattr(getattr(r, "fact", None), "content", "") or ""
162
+ ) is None
163
+ ]
161
164
  floored = bool(getattr(resp, "no_confident_match", False))
162
- top = _top_score(resp)
163
- passed = bool(results) and not floored and top >= min_score
165
+ best: float = 0.0
166
+ for r in results:
167
+ s = getattr(r, "score", None)
168
+ if s is None:
169
+ s = getattr(r, "relevance_score", 0.0) or 0.0
170
+ try:
171
+ best = max(best, float(s))
172
+ except (TypeError, ValueError):
173
+ pass
174
+ passed = bool(results) and not floored and best >= min_score
164
175
  return Verdict(
165
176
  passed,
166
177
  f"recall '{gate_query[:48]}': hits={len(results)} "
167
- f"top={top:.3f} floor={floored}",
178
+ f"top={best:.3f} floor={floored}",
168
179
  )
169
180
 
170
181
  def runner(lap: int) -> LapResult:
@@ -26,6 +26,9 @@ from typing import Callable
26
26
 
27
27
  from mcp.types import ToolAnnotations
28
28
 
29
+ from superlocalmemory.core.admission import admits
30
+ from superlocalmemory.core.operation_request import OperationKind
31
+
29
32
  logger = logging.getLogger(__name__)
30
33
 
31
34
 
@@ -221,6 +224,7 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
221
224
  """Register all 8 mesh MCP tools."""
222
225
 
223
226
  @server.tool()
227
+ @admits(OperationKind.MESH_SEND)
224
228
  async def mesh_summary(summary: str = "") -> dict:
225
229
  """Register this session and describe what you're working on.
226
230
 
@@ -269,6 +273,7 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
269
273
  }
270
274
 
271
275
  @server.tool()
276
+ @admits(OperationKind.MESH_SEND)
272
277
  async def mesh_send(to: str, message: str) -> dict:
273
278
  """Send a message to another peer session, broadcast, or project.
274
279
 
@@ -309,6 +314,7 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
309
314
  return result
310
315
 
311
316
  @server.tool()
317
+ @admits(OperationKind.MESH_SEND)
312
318
  async def mesh_inbox() -> dict:
313
319
  """Read messages sent to this session.
314
320
 
@@ -341,6 +347,7 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
341
347
  }
342
348
 
343
349
  @server.tool()
350
+ @admits(OperationKind.MESH_SEND)
344
351
  async def mesh_state(key: str = "", value: str = "", action: str = "get") -> dict:
345
352
  """Get or set shared state across all sessions.
346
353
 
@@ -370,6 +377,7 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
370
377
  return result or {"state": {}}
371
378
 
372
379
  @server.tool()
380
+ @admits(OperationKind.MESH_LOCK)
373
381
  async def mesh_lock(
374
382
  file_path: str,
375
383
  action: str = "query",
@@ -0,0 +1,115 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Wave-3 Operational Recovery & Admin Remediation MCP tools (2 tools).
6
+
7
+ list_failed_operations — Surface dead-letter, degraded, and exhausted ops.
8
+ resolve_operation — Admin retry/force-reconcile/cancel for stuck ops.
9
+
10
+ RBAC: both tools require OWNER or ADMIN role (OPS_INSPECT / OPS_RESOLVE policy).
11
+
12
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import Callable
19
+
20
+ from mcp.types import ToolAnnotations
21
+
22
+ from superlocalmemory.core.admission import admits
23
+ from superlocalmemory.core.operation_request import OperationKind
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _VALID_ACTIONS = frozenset({"retry", "force_reconcile", "cancel"})
28
+
29
+
30
+ def register_ops_tools(server, get_engine: Callable) -> None:
31
+ """Register Wave-3 operational-recovery MCP tools on *server*."""
32
+
33
+ # ------------------------------------------------------------------
34
+ # 1. list_failed_operations — surface all stuck/failed/degraded ops
35
+ # ------------------------------------------------------------------
36
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
37
+ @admits(OperationKind.OPS_INSPECT)
38
+ async def list_failed_operations(
39
+ profile_id: str = "",
40
+ ) -> dict:
41
+ """List all failed, stuck, or degraded memory operations.
42
+
43
+ Returns three categories of troubled operations:
44
+ - dead_letter: ingestion ops that exhausted all automatic retries.
45
+ - degraded_manifests: completion records where some projections failed.
46
+ - exhausted_obligations: graph/projection tasks that could not apply.
47
+
48
+ An admin can then call resolve_operation to retry, force-reconcile, or
49
+ cancel each entry. Non-technical users see these in the dashboard
50
+ Operations & Health panel.
51
+
52
+ Args:
53
+ profile_id: Filter to a specific profile (empty = all profiles).
54
+ """
55
+ try:
56
+ engine = get_engine()
57
+ db_path = getattr(getattr(engine, "_config", None), "db_path", None)
58
+ if db_path is None:
59
+ from superlocalmemory.infra.data_root import state_path
60
+ db_path = state_path("memory.db")
61
+
62
+ from superlocalmemory.core.ops_remediation import (
63
+ list_failed_operations as _list,
64
+ )
65
+ result = _list(db_path, profile_id=profile_id or None)
66
+ return {"success": True, **result}
67
+ except Exception as exc:
68
+ logger.exception("list_failed_operations tool failed")
69
+ return {"success": False, "error": str(exc)}
70
+
71
+ # ------------------------------------------------------------------
72
+ # 2. resolve_operation — admin remediation: retry / force / cancel
73
+ # ------------------------------------------------------------------
74
+ @server.tool(annotations=ToolAnnotations(destructiveHint=True))
75
+ @admits(OperationKind.OPS_RESOLVE)
76
+ async def resolve_operation(
77
+ operation_id: str,
78
+ action: str,
79
+ ) -> dict:
80
+ """Resolve a stuck, failed, or degraded operation.
81
+
82
+ Performs one of three remediation actions:
83
+ - retry: Re-queue the dead-letter entry for ingestion.
84
+ - force_reconcile: Force projection obligations to re-apply now.
85
+ - cancel: Remove the entry from failure surfaces permanently.
86
+
87
+ After a cancel, the entry no longer appears in list_failed_operations.
88
+ After retry/force_reconcile, results depend on whether the underlying
89
+ issue (e.g. missing embedding model) is now resolved.
90
+
91
+ Args:
92
+ operation_id: The operation_id string shown in list_failed_operations.
93
+ action: One of: retry, force_reconcile, cancel.
94
+ """
95
+ if action not in _VALID_ACTIONS:
96
+ return {
97
+ "success": False,
98
+ "error": f"invalid action '{action}': must be one of {sorted(_VALID_ACTIONS)}",
99
+ }
100
+ try:
101
+ engine = get_engine()
102
+ db_path = getattr(getattr(engine, "_config", None), "db_path", None)
103
+ if db_path is None:
104
+ from superlocalmemory.infra.data_root import state_path
105
+ db_path = state_path("memory.db")
106
+
107
+ from superlocalmemory.core.ops_remediation import (
108
+ resolve_operation as _resolve,
109
+ )
110
+ return _resolve(db_path, engine, operation_id, action)
111
+ except ValueError as exc:
112
+ return {"success": False, "error": str(exc)}
113
+ except Exception as exc:
114
+ logger.exception("resolve_operation tool failed op=%s action=%s", operation_id, action)
115
+ return {"success": False, "error": str(exc)}
@@ -24,6 +24,8 @@ import time
24
24
 
25
25
  from mcp.types import ToolAnnotations
26
26
 
27
+ from superlocalmemory.core.admission import admits
28
+ from superlocalmemory.core.operation_request import OperationKind
27
29
  from superlocalmemory.mcp.agent_context import get_current_agent_id
28
30
  from superlocalmemory.optimize.compress.ccr import CCRStore, _UUID4_RE
29
31
  from superlocalmemory.optimize.compress.router import CompressRouter
@@ -70,6 +72,7 @@ def register_optimize_tools(server) -> None:
70
72
  """
71
73
 
72
74
  @server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
75
+ @admits(OperationKind.CONSOLIDATE)
73
76
  async def slm_compress(
74
77
  content: str,
75
78
  mode: str = "auto",
@@ -191,6 +194,7 @@ def register_optimize_tools(server) -> None:
191
194
  }
192
195
 
193
196
  @server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
197
+ @admits(OperationKind.REMEMBER)
194
198
  async def slm_cache_set(key: str, value: str, ttl_seconds: int = 86400) -> dict:
195
199
  """Cache a result you want to reuse (tool output, file read, search result).
196
200
 
@@ -15,6 +15,10 @@ from __future__ import annotations
15
15
  import logging
16
16
  from typing import Callable
17
17
 
18
+ from mcp.types import ToolAnnotations
19
+
20
+ from superlocalmemory.core.admission import admits
21
+ from superlocalmemory.core.operation_request import OperationKind
18
22
  from superlocalmemory.mcp.shared import authorize_mcp_mutation
19
23
 
20
24
  logger = logging.getLogger(__name__)
@@ -27,6 +31,7 @@ def register_v28_tools(server, get_engine: Callable) -> None:
27
31
  # 1. report_outcome
28
32
  # ------------------------------------------------------------------
29
33
  @server.tool()
34
+ @admits(OperationKind.REMEMBER)
30
35
  async def report_outcome(
31
36
  memory_ids: str,
32
37
  outcome: str,
@@ -91,7 +96,7 @@ def register_v28_tools(server, get_engine: Callable) -> None:
91
96
  # ------------------------------------------------------------------
92
97
  # 2. get_lifecycle_status
93
98
  # ------------------------------------------------------------------
94
- @server.tool()
99
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
95
100
  async def get_lifecycle_status(limit: int = 50) -> dict:
96
101
  """Get lifecycle state distribution for stored memories.
97
102
 
@@ -127,6 +132,7 @@ def register_v28_tools(server, get_engine: Callable) -> None:
127
132
  # 3. set_retention_policy
128
133
  # ------------------------------------------------------------------
129
134
  @server.tool()
135
+ @admits(OperationKind.CONSOLIDATE)
130
136
  async def set_retention_policy(
131
137
  cold_after_days: int = 30,
132
138
  archive_after_days: int = 90,
@@ -163,6 +169,7 @@ def register_v28_tools(server, get_engine: Callable) -> None:
163
169
  # 4. compact_memories
164
170
  # ------------------------------------------------------------------
165
171
  @server.tool()
172
+ @admits(OperationKind.CONSOLIDATE)
166
173
  async def compact_memories(dry_run: bool = True) -> dict:
167
174
  """Compact memory store by archiving cold/stale facts.
168
175
 
@@ -212,7 +219,7 @@ def register_v28_tools(server, get_engine: Callable) -> None:
212
219
  # ------------------------------------------------------------------
213
220
  # 5. get_behavioral_patterns
214
221
  # ------------------------------------------------------------------
215
- @server.tool()
222
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
216
223
  async def get_behavioral_patterns(limit: int = 20) -> dict:
217
224
  """Get detected behavioral patterns for the active profile.
218
225
 
@@ -241,7 +248,7 @@ def register_v28_tools(server, get_engine: Callable) -> None:
241
248
  # ------------------------------------------------------------------
242
249
  # 6. audit_trail
243
250
  # ------------------------------------------------------------------
244
- @server.tool()
251
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
245
252
  async def audit_trail(limit: int = 50) -> dict:
246
253
  """Get compliance audit trail for the active profile.
247
254
 
@@ -14,6 +14,10 @@ from __future__ import annotations
14
14
  import logging
15
15
  from typing import Callable
16
16
 
17
+ from mcp.types import ToolAnnotations
18
+
19
+ from superlocalmemory.core.admission import admits
20
+ from superlocalmemory.core.operation_request import OperationKind
17
21
  from superlocalmemory.mcp.shared import authorize_mcp_mutation
18
22
 
19
23
  logger = logging.getLogger(__name__)
@@ -25,7 +29,7 @@ def register_v3_tools(server, get_engine: Callable) -> None:
25
29
  # ------------------------------------------------------------------
26
30
  # 0. get_version (so IDEs can check compatibility)
27
31
  # ------------------------------------------------------------------
28
- @server.tool()
32
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
29
33
  async def get_version() -> dict:
30
34
  """Get SuperLocalMemory version, Python version, and platform info."""
31
35
  try:
@@ -47,12 +51,13 @@ def register_v3_tools(server, get_engine: Callable) -> None:
47
51
  # 1. set_mode
48
52
  # ------------------------------------------------------------------
49
53
  @server.tool()
54
+ @admits(OperationKind.MODE_CHANGE)
50
55
  async def set_mode(mode: str) -> dict:
51
56
  """Switch operating mode (a, b, or c).
52
57
 
53
- Mode A: Local Guardian (zero LLM, EU AI Act full compliance).
54
- Mode B: Smart Local (local Ollama LLM, EU AI Act full).
55
- Mode C: Full Power (cloud LLM, best accuracy).
58
+ Mode A: Local Guardian (zero LLM, local embeddings only).
59
+ Mode B: Smart Local (local Ollama LLM, on-device inference).
60
+ Mode C: Full Power (configured cloud LLM provider, best accuracy).
56
61
 
57
62
  Resets the engine to apply the new mode configuration.
58
63
 
@@ -106,7 +111,7 @@ def register_v3_tools(server, get_engine: Callable) -> None:
106
111
  # ------------------------------------------------------------------
107
112
  # 2. get_mode
108
113
  # ------------------------------------------------------------------
109
- @server.tool()
114
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
110
115
  async def get_mode() -> dict:
111
116
  """Get current operating mode and its capabilities.
112
117
 
@@ -135,7 +140,7 @@ def register_v3_tools(server, get_engine: Callable) -> None:
135
140
  # ------------------------------------------------------------------
136
141
  # 3. health
137
142
  # ------------------------------------------------------------------
138
- @server.tool()
143
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
139
144
  async def health() -> dict:
140
145
  """Get system health including math layer status.
141
146
 
@@ -219,7 +224,7 @@ def register_v3_tools(server, get_engine: Callable) -> None:
219
224
  # ------------------------------------------------------------------
220
225
  # 4. consistency_check
221
226
  # ------------------------------------------------------------------
222
- @server.tool()
227
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
223
228
  async def consistency_check(limit: int = 100) -> dict:
224
229
  """Run sheaf consistency check on stored memories.
225
230
 
@@ -276,8 +281,12 @@ def register_v3_tools(server, get_engine: Callable) -> None:
276
281
  # ------------------------------------------------------------------
277
282
  # 5. recall_trace
278
283
  # ------------------------------------------------------------------
279
- @server.tool()
280
- async def recall_trace(query: str, limit: int = 10) -> dict:
284
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
285
+ async def recall_trace(
286
+ query: str,
287
+ limit: int = 10,
288
+ as_of: str | None = None,
289
+ ) -> dict:
281
290
  """Recall with per-channel score breakdown.
282
291
 
283
292
  Like recall, but returns detailed channel-by-channel scores
@@ -286,14 +295,25 @@ def register_v3_tools(server, get_engine: Callable) -> None:
286
295
  Args:
287
296
  query: Natural-language search query.
288
297
  limit: Maximum results (default 10).
298
+ as_of: Optional ISO 8601 UTC datetime for point-in-time recall
299
+ (e.g. "2024-01-01T00:00:00Z"). Omit for current-state recall.
289
300
  """
290
301
  try:
291
302
  import asyncio
292
303
  from superlocalmemory.mcp._daemon_proxy import choose_pool
304
+ from superlocalmemory.retrieval.temporal_utils import normalize_as_of
305
+
306
+ # Normalize at MCP boundary before forwarding.
307
+ _as_of: str | None = None
308
+ if as_of:
309
+ _as_of = normalize_as_of(as_of)
310
+ if _as_of is None:
311
+ return {"success": False, "error": "invalid_as_of"}
312
+
293
313
  # choose_pool().recall uses blocking urllib; run off the event loop
294
314
  # so recall_trace doesn't stall the MCP server for other tools.
295
315
  raw = await asyncio.to_thread(
296
- lambda: choose_pool().recall(query=query, limit=limit)
316
+ lambda: choose_pool().recall(query=query, limit=limit, as_of=_as_of)
297
317
  )
298
318
  items = raw.get("results", []) if isinstance(raw, dict) else []
299
319
  results = []
@@ -341,10 +361,10 @@ def register_v3_tools(server, get_engine: Callable) -> None:
341
361
  # -- Helpers ------------------------------------------------------------------
342
362
 
343
363
  def _mode_description(mode: str) -> str:
344
- """Human-readable description for a mode."""
364
+ """Human-readable capability description for a mode (never a legal claim)."""
345
365
  descriptions = {
346
- "a": "Local Guardian: zero LLM, full EU AI Act compliance",
347
- "b": "Smart Local: local Ollama LLM, full EU AI Act compliance",
348
- "c": "Full Power: cloud LLM, best accuracy, partial EU AI Act",
366
+ "a": "Local Guardian: zero LLM, local embeddings only",
367
+ "b": "Smart Local: local Ollama LLM, on-device inference",
368
+ "c": "Full Power: configured cloud LLM provider, best accuracy",
349
369
  }
350
370
  return descriptions.get(mode, "Unknown mode")