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
@@ -19,6 +19,10 @@ import time
19
19
  from pathlib import Path
20
20
  from typing import Any, Callable
21
21
 
22
+ from mcp.types import ToolAnnotations
23
+
24
+ from superlocalmemory.core.admission import admits
25
+ from superlocalmemory.core.operation_request import OperationKind
22
26
  from superlocalmemory.core.security_primitives import (
23
27
  PathTraversalError,
24
28
  safe_resolve,
@@ -108,6 +112,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
108
112
  # ==================================================================
109
113
 
110
114
  @server.tool()
115
+ @admits(OperationKind.CORRECT)
111
116
  async def build_code_graph(
112
117
  repo_path: str,
113
118
  languages: str = "",
@@ -242,6 +247,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
242
247
  # ==================================================================
243
248
 
244
249
  @server.tool()
250
+ @admits(OperationKind.CORRECT)
245
251
  async def update_code_graph(
246
252
  repo_path: str = "",
247
253
  changed_files: str = "",
@@ -390,7 +396,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
390
396
  # Tool 3: get_blast_radius
391
397
  # ==================================================================
392
398
 
393
- @server.tool()
399
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
394
400
  async def get_blast_radius(
395
401
  changed_files: str,
396
402
  max_depth: int = 2,
@@ -452,7 +458,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
452
458
  # Tool 4: get_review_context
453
459
  # ==================================================================
454
460
 
455
- @server.tool()
461
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
456
462
  async def get_review_context(
457
463
  changed_files: str,
458
464
  include_source: bool = True,
@@ -509,7 +515,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
509
515
  "tests_for", "inherits_from", "inherited_by", "contains",
510
516
  })
511
517
 
512
- @server.tool()
518
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
513
519
  async def query_graph(
514
520
  pattern: str,
515
521
  target: str = "",
@@ -608,7 +614,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
608
614
  # Tool 6: semantic_search_code
609
615
  # ==================================================================
610
616
 
611
- @server.tool()
617
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
612
618
  async def semantic_search_code(
613
619
  query: str,
614
620
  kind: str = "",
@@ -658,7 +664,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
658
664
  # Tool 7: list_graph_stats
659
665
  # ==================================================================
660
666
 
661
- @server.tool()
667
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
662
668
  async def list_graph_stats() -> dict:
663
669
  """Get code graph size and health metrics."""
664
670
  try:
@@ -705,7 +711,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
705
711
  # Tool 8: find_large_functions
706
712
  # ==================================================================
707
713
 
708
- @server.tool()
714
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
709
715
  async def find_large_functions(
710
716
  threshold: int = 50,
711
717
  limit: int = 20,
@@ -752,7 +758,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
752
758
  # Tool 9: list_flows
753
759
  # ==================================================================
754
760
 
755
- @server.tool()
761
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
756
762
  async def list_flows(
757
763
  sort_by: str = "criticality",
758
764
  limit: int = 20,
@@ -809,7 +815,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
809
815
  # Tool 10: get_flow
810
816
  # ==================================================================
811
817
 
812
- @server.tool()
818
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
813
819
  async def get_flow(
814
820
  flow_name: str,
815
821
  ) -> dict:
@@ -860,7 +866,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
860
866
  # Tool 11: get_affected_flows
861
867
  # ==================================================================
862
868
 
863
- @server.tool()
869
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
864
870
  async def get_affected_flows(
865
871
  changed_files: str,
866
872
  ) -> dict:
@@ -920,7 +926,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
920
926
  # Tool 12: list_communities
921
927
  # ==================================================================
922
928
 
923
- @server.tool()
929
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
924
930
  async def list_communities(
925
931
  sort_by: str = "cohesion",
926
932
  limit: int = 20,
@@ -967,7 +973,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
967
973
  # Tool 13: get_community
968
974
  # ==================================================================
969
975
 
970
- @server.tool()
976
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
971
977
  async def get_community(
972
978
  community_id: int,
973
979
  ) -> dict:
@@ -1014,7 +1020,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1014
1020
  # Tool 14: get_architecture_overview
1015
1021
  # ==================================================================
1016
1022
 
1017
- @server.tool()
1023
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
1018
1024
  async def get_architecture_overview() -> dict:
1019
1025
  """Get high-level architecture map showing communities and their relationships."""
1020
1026
  try:
@@ -1063,7 +1069,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1063
1069
  # Tool 15: detect_changes
1064
1070
  # ==================================================================
1065
1071
 
1066
- @server.tool()
1072
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
1067
1073
  async def detect_changes(
1068
1074
  base: str = "HEAD~1",
1069
1075
  ) -> dict:
@@ -1122,7 +1128,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1122
1128
  # Tool 16: refactor_preview
1123
1129
  # ==================================================================
1124
1130
 
1125
- @server.tool()
1131
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
1126
1132
  async def refactor_preview(
1127
1133
  action: str,
1128
1134
  target: str,
@@ -1216,6 +1222,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1216
1222
  # ==================================================================
1217
1223
 
1218
1224
  @server.tool()
1225
+ @admits(OperationKind.CORRECT)
1219
1226
  async def apply_refactor(
1220
1227
  action: str,
1221
1228
  target: str,
@@ -1250,7 +1257,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1250
1257
  # Tool 18 (BRIDGE): code_memory_search
1251
1258
  # ==================================================================
1252
1259
 
1253
- @server.tool()
1260
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
1254
1261
  async def code_memory_search(
1255
1262
  code_entity: str,
1256
1263
  link_type: str = "",
@@ -1320,7 +1327,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1320
1327
  # Tool 19 (BRIDGE): code_entity_history
1321
1328
  # ==================================================================
1322
1329
 
1323
- @server.tool()
1330
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
1324
1331
  async def code_entity_history(
1325
1332
  code_entity: str,
1326
1333
  ) -> dict:
@@ -1390,7 +1397,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1390
1397
  # Tool 20 (BRIDGE): enrich_blast_radius
1391
1398
  # ==================================================================
1392
1399
 
1393
- @server.tool()
1400
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
1394
1401
  async def enrich_blast_radius(
1395
1402
  changed_files: str,
1396
1403
  max_depth: int = 2,
@@ -1464,7 +1471,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1464
1471
  # Tool 21 (BRIDGE): code_stale_check
1465
1472
  # ==================================================================
1466
1473
 
1467
- @server.tool()
1474
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
1468
1475
  async def code_stale_check(
1469
1476
  scope: str = "all",
1470
1477
  ) -> dict:
@@ -1538,6 +1545,7 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
1538
1545
  })
1539
1546
 
1540
1547
  @server.tool()
1548
+ @admits(OperationKind.CORRECT)
1541
1549
  async def link_memory_to_code(
1542
1550
  fact_id: str,
1543
1551
  code_entity: str,
@@ -21,6 +21,8 @@ from datetime import datetime, timezone
21
21
  from threading import Lock
22
22
  from typing import Callable
23
23
 
24
+ from mcp.types import ToolAnnotations
25
+
24
26
  from superlocalmemory.core.security_primitives import redact_secrets
25
27
 
26
28
  logger = logging.getLogger(__name__)
@@ -64,7 +66,10 @@ _DEFAULT_LIMITER = _RateLimiter()
64
66
 
65
67
 
66
68
  # RecallFn for the tool. Callers inject a real recall engine; tests inject fakes.
67
- PrestageRecallFn = Callable[[str, int, str], list[dict]]
69
+ # Phase 4b: extended to accept optional 4th positional arg (as_of: str | None).
70
+ # Legacy callers with a 3-arg signature are handled by the try/except TypeError
71
+ # fallback in prestage_context() (Decision A2, LLD §Module 8).
72
+ PrestageRecallFn = Callable[..., list[dict]]
68
73
 
69
74
 
70
75
  def _iso_now() -> str:
@@ -100,12 +105,17 @@ def prestage_context(
100
105
  session_id: str = "default",
101
106
  recall_fn: PrestageRecallFn,
102
107
  limiter: _RateLimiter | None = None,
108
+ as_of: str | None = None,
103
109
  ) -> dict:
104
110
  """Proactive-context tool body.
105
111
 
106
112
  Pure function: takes an injected recall_fn + limiter. The MCP server
107
113
  wrapper in the session process wires the real engine and shares a
108
114
  single limiter instance.
115
+
116
+ Phase 4b: optional ``as_of`` (ISO 8601 UTC) for point-in-time context.
117
+ Normalized at entry; invalid input is silently ignored (background tool).
118
+ 3-arg recall_fn callers are supported via TypeError fallback (Decision A2).
109
119
  """
110
120
  _limiter = limiter or _DEFAULT_LIMITER
111
121
  if not _limiter.allow(session_id):
@@ -127,8 +137,29 @@ def prestage_context(
127
137
  }
128
138
  limit = max(1, min(int(limit), 50))
129
139
 
140
+ # Normalize as_of at prestage boundary. Invalid input → silently ignore
141
+ # (prestage is background; user-facing surfaces raise instead).
142
+ _as_of: str | None = None
143
+ if as_of:
144
+ from superlocalmemory.retrieval.temporal_utils import normalize_as_of
145
+ _as_of = normalize_as_of(as_of)
146
+ if _as_of is None:
147
+ logger.warning("prestage_context: invalid as_of %r; ignoring", as_of)
148
+
130
149
  try:
131
- raw = recall_fn(query, limit, profile_id) or []
150
+ # Decision A2 (LLD §Module 8) + audit P2 (CRIT-3): decide arity by
151
+ # INSPECTING the callable, not by catching TypeError. A bare
152
+ # `except TypeError` would silently swallow a genuine TypeError raised
153
+ # inside a 4-arg recall_fn body and drop as_of (silent wrong PIT view).
154
+ import inspect as _inspect
155
+ try:
156
+ _arity = len(_inspect.signature(recall_fn).parameters)
157
+ except (TypeError, ValueError):
158
+ _arity = 4 # unintrospectable (partial/builtin) → assume as_of-capable
159
+ if _arity >= 4:
160
+ raw = recall_fn(query, limit, profile_id, _as_of) or []
161
+ else:
162
+ raw = recall_fn(query, limit, profile_id) or []
132
163
  except Exception as exc:
133
164
  logger.warning("prestage_context recall failed: %s", exc)
134
165
  return {
@@ -172,20 +203,31 @@ def prestage_context(
172
203
  def register_prestage_tool(server, recall_fn: PrestageRecallFn,
173
204
  *, session_id_fn: Callable[[], str] | None = None
174
205
  ) -> None:
175
- """Register the ``prestage_context`` tool on an MCP server."""
206
+ """Register the ``prestage_context`` tool on an MCP server.
207
+
208
+ The tool function is named ``prestage_context`` so profile filters
209
+ (``func.__name__``) and docs agree.
210
+ """
176
211
  limiter = _RateLimiter()
212
+ # Module-level pure function (avoid UnboundLocalError from nested def name).
213
+ _pure_prestage = globals()["prestage_context"]
177
214
 
178
- @server.tool()
179
- async def prestage_context_tool( # pragma: no cover — MCP wiring
215
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
216
+ async def prestage_context( # noqa: F811 — MCP tool surface name
180
217
  query: str,
181
218
  limit: int = 5,
182
219
  profile_id: str = "default",
220
+ as_of: str | None = None,
183
221
  ) -> dict:
184
- """Proactively return top-K memories for a query."""
222
+ """Proactively return top-K memories for a query.
223
+
224
+ Optional ``as_of`` (ISO 8601 UTC) for point-in-time context retrieval.
225
+ """
185
226
  session_id = session_id_fn() if session_id_fn else "default"
186
- return prestage_context(
227
+ return _pure_prestage(
187
228
  query, limit=limit, profile_id=profile_id,
188
- session_id=session_id, recall_fn=recall_fn, limiter=limiter,
229
+ session_id=session_id, recall_fn=recall_fn,
230
+ limiter=limiter, as_of=as_of,
189
231
  )
190
232
 
191
233
 
@@ -19,7 +19,9 @@ from typing import Callable
19
19
 
20
20
  from mcp.types import ToolAnnotations
21
21
 
22
+ from superlocalmemory.core.admission import admits
22
23
  from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
24
+ from superlocalmemory.core.operation_request import OperationKind
23
25
  from superlocalmemory.infra.data_root import state_path
24
26
  from superlocalmemory.mcp._daemon_proxy import daemon_unavailable_error
25
27
  from superlocalmemory.mcp.shared import authorize_mcp_mutation
@@ -63,6 +65,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
63
65
  """Register the 13 core MCP tools on *server*."""
64
66
 
65
67
  @server.tool()
68
+ @admits(OperationKind.REMEMBER)
66
69
  async def remember(
67
70
  content: str, tags: str = "", project: str = "",
68
71
  importance: int = 5, session_id: str = "",
@@ -91,13 +94,25 @@ def register_core_tools(server, get_engine: Callable) -> None:
91
94
  "session_id": session_id,
92
95
  }
93
96
  effective_idempotency_key = idempotency_key
94
- if not effective_idempotency_key and session_id:
95
- material = (
96
- f"{agent_id}\0{session_id}\0{scope or ''}\0{shared_with}\0{content}"
97
- )
98
- effective_idempotency_key = "mcp:" + hashlib.sha256(
99
- material.encode("utf-8")
100
- ).hexdigest()
97
+ if not effective_idempotency_key:
98
+ # Derive a stable key before the first attempt so every retry of
99
+ # the same logical call carries the same key. When a session token
100
+ # is present it is included in the material to keep per-session
101
+ # stores separate. Without a session token the key is derived from
102
+ # the remaining call parameters so repeated observations with the
103
+ # same content, agent, and scope are deduplicated across retries.
104
+ if session_id:
105
+ material = (
106
+ f"{agent_id}\0{session_id}\0{scope or ''}\0{shared_with}\0{content}"
107
+ )
108
+ effective_idempotency_key = "mcp:" + hashlib.sha256(
109
+ material.encode("utf-8")
110
+ ).hexdigest()
111
+ else:
112
+ material = f"{agent_id}\0{scope or ''}\0{shared_with}\0{content}"
113
+ effective_idempotency_key = "mcp:req:" + hashlib.sha256(
114
+ material.encode("utf-8")
115
+ ).hexdigest()
101
116
  # Parse shared_with from comma-separated string
102
117
  _shared_list = [s.strip() for s in shared_with.split(",") if s.strip()] if shared_with else None
103
118
  # v3.5.5 WRITE-THROUGH: route through the daemon's /remember, which does
@@ -109,6 +124,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
109
124
  daemon_owned = False
110
125
  try:
111
126
  import asyncio as _asyncio
127
+
112
128
  from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
113
129
  # is_daemon_running() and daemon_request() both use blocking urllib
114
130
  # against the same uvicorn server — run in threads so the MCP
@@ -173,6 +189,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
173
189
 
174
190
  try:
175
191
  import asyncio as _asyncio
192
+
176
193
  from superlocalmemory.mcp._daemon_proxy import choose_pool
177
194
 
178
195
  worker_meta = {
@@ -247,12 +264,14 @@ def register_core_tools(server, get_engine: Callable) -> None:
247
264
  }
248
265
 
249
266
  @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
267
+ @admits(OperationKind.RECALL)
250
268
  async def recall(
251
269
  query: str, limit: int = CANONICAL_RECALL_LIMIT, agent_id: str = "mcp_client",
252
270
  session_id: str = "", fast: bool | None = None,
253
271
  include_global: bool | None = None,
254
272
  include_shared: bool | None = None,
255
273
  window: str = "",
274
+ as_of: str | None = None,
256
275
  ) -> dict:
257
276
  """Search memories through hybrid retrieval, RRF fusion, and reranking.
258
277
 
@@ -286,6 +305,10 @@ def register_core_tools(server, get_engine: Callable) -> None:
286
305
  range. Accepts a relative span (``"24h"``, ``"7d"``, ``"30d"``,
287
306
  ``"1y"``) or an explicit range (``"2026-07-01..2026-07-31"``). Empty =
288
307
  no time filter.
308
+
309
+ Point-in-time: optional ``as_of`` (ISO-8601 string, e.g.
310
+ ``"2026-01-01T00:00:00+00:00"``) pins recall to a temporal snapshot;
311
+ omit or pass ``None`` for current-state recall.
289
312
  """
290
313
  # v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)
291
314
  if agent_id == "mcp_client":
@@ -342,12 +365,28 @@ def register_core_tools(server, get_engine: Callable) -> None:
342
365
  #
343
366
  # V3.4.26: WorkerPool now concurrent — parallel calls no longer
344
367
  # block behind a single threading.Lock. See worker_pool.py.
368
+ # Phase 4b: normalize as_of at MCP boundary. Invalid → reject.
369
+ # Audit P2: treat empty/whitespace as_of as ABSENT (like HTTP does),
370
+ # not as an invalid value — only a non-blank unparseable string is
371
+ # rejected.
372
+ if as_of is not None and str(as_of).strip():
373
+ from superlocalmemory.retrieval.temporal_utils import normalize_as_of
374
+ as_of = normalize_as_of(as_of)
375
+ if as_of is None:
376
+ return {"success": False, "error": "invalid_as_of"}
377
+ else:
378
+ as_of = None
379
+
380
+ from superlocalmemory.core.admission import enforce_read_scope
381
+ _incl_global, _incl_shared = enforce_read_scope(include_global, include_shared)
382
+
345
383
  def _recall_via_daemon_pool():
346
384
  pool = choose_pool()
347
385
  return pool.recall(
348
386
  query, limit=limit, session_id=effective_sid,
349
- fast=fast, include_global=include_global,
350
- include_shared=include_shared, window=window or None,
387
+ fast=fast, include_global=_incl_global,
388
+ include_shared=_incl_shared, window=window or None,
389
+ as_of=as_of,
351
390
  )
352
391
 
353
392
  result = await asyncio.to_thread(
@@ -376,11 +415,12 @@ def register_core_tools(server, get_engine: Callable) -> None:
376
415
  return {"success": False, "error": str(exc)}
377
416
 
378
417
  @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
379
- async def search(query: str, limit: int = 10, profile_id: str = "") -> dict:
418
+ @admits(OperationKind.RECALL)
419
+ async def search(query: str, limit: int = 10) -> dict:
380
420
  """Full-text search across memories using FTS5 with BM25 ranking."""
381
421
  try:
382
422
  engine = get_engine()
383
- pid = await _runtime_profile(get_engine, profile_id)
423
+ pid = await _runtime_profile(get_engine)
384
424
  facts = engine._db.search_facts_fts(query, pid, limit=limit)
385
425
  items = []
386
426
  for f in facts:
@@ -424,11 +464,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
424
464
  return {"success": False, "error": str(exc)}
425
465
 
426
466
  @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
427
- async def list_recent(limit: int = 20, profile_id: str = "") -> dict:
467
+ async def list_recent(limit: int = 20) -> dict:
428
468
  """List most recently stored memories, newest first."""
429
469
  try:
430
470
  engine = get_engine()
431
- pid = await _runtime_profile(get_engine, profile_id)
471
+ pid = await _runtime_profile(get_engine)
432
472
  # v3.6.12 (search-2): push the limit into the query — was loading the
433
473
  # ENTIRE facts table (deserializing every 768-float embedding) just
434
474
  # to return the top N. get_all_facts preserves created_at DESC order.
@@ -522,11 +562,12 @@ def register_core_tools(server, get_engine: Callable) -> None:
522
562
  return {"success": False, "error": str(exc)}
523
563
 
524
564
  @server.tool()
525
- async def build_graph(profile_id: str = "") -> dict:
565
+ @admits(OperationKind.CORRECT)
566
+ async def build_graph() -> dict:
526
567
  """Rebuild knowledge graph edges for all facts in the active profile."""
527
568
  try:
528
569
  engine = get_engine()
529
- pid = await _runtime_profile(get_engine, profile_id)
570
+ pid = await _runtime_profile(get_engine)
530
571
  authorization = authorize_mcp_mutation(
531
572
  engine,
532
573
  "update",
@@ -550,6 +591,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
550
591
  return {"success": False, "error": str(exc)}
551
592
 
552
593
  @server.tool()
594
+ @admits(OperationKind.PROFILE_SWITCH)
553
595
  async def switch_profile(profile_id: str) -> dict:
554
596
  """Switch the active memory profile. All operations scope to this profile."""
555
597
  try:
@@ -674,19 +716,22 @@ def register_core_tools(server, get_engine: Callable) -> None:
674
716
  logger.exception("switch_profile failed")
675
717
  return {"success": False, "error": str(exc)}
676
718
 
677
- @server.tool()
719
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
678
720
  async def backup_status() -> dict:
679
721
  """Get backup system status, last backup time, and available backup files."""
680
722
  try:
681
723
  engine = get_engine()
682
724
  from superlocalmemory.infra.backup import BackupManager
683
- bm = BackupManager(engine._config.base_dir, engine._db.db_path)
725
+ bm = BackupManager(
726
+ db_path=engine._db.db_path,
727
+ base_dir=engine._config.base_dir,
728
+ )
684
729
  return {"success": True, **bm.get_status()}
685
730
  except Exception as exc:
686
731
  logger.exception("backup_status failed")
687
732
  return {"success": False, "error": str(exc)}
688
733
 
689
- @server.tool()
734
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
690
735
  async def memory_used() -> dict:
691
736
  """Get memory usage breakdown by fact type and lifecycle state."""
692
737
  try:
@@ -711,7 +756,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
711
756
  logger.exception("memory_used failed")
712
757
  return {"success": False, "error": str(exc)}
713
758
 
714
- @server.tool()
759
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
715
760
  async def get_learned_patterns(pattern_type: str = "", limit: int = 20) -> dict:
716
761
  """Get learned behavioral patterns (interests, refinements, archival habits)."""
717
762
  try:
@@ -729,6 +774,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
729
774
  return {"success": False, "error": str(exc)}
730
775
 
731
776
  @server.tool()
777
+ @admits(OperationKind.CORRECT)
732
778
  async def correct_pattern(pattern_id: str, correction: str) -> dict:
733
779
  """Correct or annotate a learned behavioral pattern to improve retrieval."""
734
780
  try:
@@ -757,6 +803,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
757
803
  return {"success": False, "error": str(exc)}
758
804
 
759
805
  @server.tool(annotations=ToolAnnotations(destructiveHint=True))
806
+ @admits(OperationKind.FORGET)
760
807
  async def delete_memory(fact_id: str, agent_id: str = "mcp_client") -> dict:
761
808
  """Delete a specific memory by exact fact ID.
762
809
 
@@ -823,6 +870,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
823
870
  return {"success": False, "error": str(exc)}
824
871
 
825
872
  @server.tool(annotations=ToolAnnotations(idempotentHint=True))
873
+ @admits(OperationKind.CORRECT)
826
874
  async def update_memory(
827
875
  fact_id: str, content: str, agent_id: str = "mcp_client",
828
876
  ) -> dict:
@@ -886,12 +934,12 @@ def register_core_tools(server, get_engine: Callable) -> None:
886
934
  logger.exception("update_memory failed")
887
935
  return {"success": False, "error": str(exc)}
888
936
 
889
- @server.tool()
937
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
890
938
  async def get_attribution() -> dict:
891
939
  """Get system attribution: author, version, license, and provenance metadata."""
892
940
  return {
893
941
  "success": True,
894
- "product": "SuperLocalMemory V3",
942
+ "product": "SuperLocalMemory V4",
895
943
  "author": "Varun Pratap Bhardwaj",
896
944
  "organization": "Qualixar",
897
945
  "license": "AGPL-3.0-or-later",
@@ -19,6 +19,8 @@ import logging
19
19
  from typing import Callable
20
20
 
21
21
  from mcp.types import ToolAnnotations
22
+ from superlocalmemory.core.admission import admits
23
+ from superlocalmemory.core.operation_request import OperationKind
22
24
  from superlocalmemory.infra.data_root import state_path
23
25
  from superlocalmemory.storage.read_connection import ReadConnectionFactory
24
26
 
@@ -28,6 +30,7 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
28
30
  """Register evolution MCP tools for skill evolution intelligence."""
29
31
 
30
32
  @server.tool()
33
+ @admits(OperationKind.EVOLVE_SKILL)
31
34
  async def evolve_skill(
32
35
  skill_name: str,
33
36
  evolution_type: str = "fix",
@@ -270,6 +273,8 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
270
273
  skill_name: Specific skill name (empty = all skills)
271
274
  """
272
275
  try:
276
+ engine = get_engine()
277
+ profile_id = engine.profile_id if engine else "default"
273
278
  db_path = state_path("memory.db")
274
279
  conn = ReadConnectionFactory(db_path).open()
275
280
 
@@ -279,9 +284,9 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
279
284
  "trigger_type, generation, status, mutation_summary, "
280
285
  "blind_verified, created_at, completed_at "
281
286
  "FROM skill_evolution_log "
282
- "WHERE skill_name = ? OR parent_skill_id = ? "
287
+ "WHERE profile_id = ? AND (skill_name = ? OR parent_skill_id = ?) "
283
288
  "ORDER BY created_at ASC",
284
- (skill_name, skill_name),
289
+ (profile_id, skill_name, skill_name),
285
290
  ).fetchall()
286
291
  else:
287
292
  rows = conn.execute(
@@ -289,7 +294,9 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
289
294
  "trigger_type, generation, status, mutation_summary, "
290
295
  "blind_verified, created_at, completed_at "
291
296
  "FROM skill_evolution_log "
297
+ "WHERE profile_id = ? "
292
298
  "ORDER BY created_at DESC LIMIT 100",
299
+ (profile_id,),
293
300
  ).fetchall()
294
301
 
295
302
  conn.close()