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
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
32
32
  os.environ["OMP_NUM_THREADS"] = "2"
33
33
  # ---------------------------------------------------------------------------
34
34
 
35
- __version__ = "3.8.13"
35
+ __version__ = "4.0.0"
36
36
 
37
37
  _REQUIRED_VERSIONS = {
38
38
  "sentence_transformers": "5.3.0",
@@ -270,6 +270,12 @@ def _cmd_loop(args: Namespace) -> None:
270
270
  cmd_loop(args)
271
271
 
272
272
 
273
+ def _cmd_ops(args: Namespace) -> None:
274
+ """Wave-3: operational recovery & admin remediation commands."""
275
+ from superlocalmemory.cli.ops_cmd import cmd_ops
276
+ cmd_ops(args)
277
+
278
+
273
279
  # ---- end SLM v3.6 Optimize dispatch functions ----
274
280
 
275
281
 
@@ -399,6 +405,8 @@ def dispatch(args: Namespace) -> None:
399
405
  "loop": _cmd_loop,
400
406
  # V3.8.2 super-help — grouped overview of every command + topics
401
407
  "help": cmd_help,
408
+ # Wave-3: operational recovery & admin remediation
409
+ "ops": _cmd_ops,
402
410
  }
403
411
  handler = handlers.get(args.command)
404
412
  if handler:
@@ -982,6 +990,10 @@ def cmd_evolve(args: Namespace) -> None:
982
990
  if not session_id:
983
991
  return # Silent exit — nothing to do without a session
984
992
 
993
+ from superlocalmemory.core.admission import gate_cli_mutation
994
+ from superlocalmemory.core.operation_request import OperationKind
995
+ gate_cli_mutation(OperationKind.EVOLVE_SKILL)
996
+
985
997
  # Check if evolution is enabled via config.json
986
998
  config_path = state_path("config.json")
987
999
  try:
@@ -1059,6 +1071,11 @@ def cmd_mode(args: Namespace) -> None:
1059
1071
 
1060
1072
  config = SLMConfig.load()
1061
1073
 
1074
+ if args.value:
1075
+ from superlocalmemory.core.admission import gate_cli_mutation
1076
+ from superlocalmemory.core.operation_request import OperationKind
1077
+ gate_cli_mutation(OperationKind.MODE_CHANGE)
1078
+
1062
1079
  if getattr(args, 'json', False):
1063
1080
  from superlocalmemory.cli.json_output import json_print
1064
1081
  if args.value:
@@ -1431,6 +1448,9 @@ def cmd_recall(args: Namespace) -> None:
1431
1448
  # produces True/False here.
1432
1449
  include_global = getattr(args, 'include_global', None)
1433
1450
  include_shared = getattr(args, 'include_shared', None)
1451
+ # Phase-1/D2: clamp cross-profile scope flags before forwarding to daemon.
1452
+ from superlocalmemory.core.admission import enforce_read_scope
1453
+ include_global, include_shared = enforce_read_scope(include_global, include_shared)
1434
1454
 
1435
1455
  # V3.3.21: Route through daemon for instant response (no cold start).
1436
1456
  # S9-DASH-02: pass a stable session_id derived from the shell's
@@ -1453,10 +1473,23 @@ def cmd_recall(args: Namespace) -> None:
1453
1473
  scope_qs += f"&include_shared={str(include_shared).lower()}"
1454
1474
  _window = getattr(args, "window", "") or ""
1455
1475
  window_qs = f"&window={quote(_window)}" if _window else ""
1476
+ _as_of = getattr(args, "as_of", "") or ""
1477
+ if _as_of:
1478
+ from superlocalmemory.retrieval.temporal_utils import normalize_as_of
1479
+ _as_of_norm = normalize_as_of(_as_of)
1480
+ if _as_of_norm is None:
1481
+ import sys as _sys
1482
+ _sys.stderr.write(
1483
+ f"Error: invalid --as-of value: {_as_of!r}\n"
1484
+ "Expected ISO 8601 UTC datetime, e.g. '2024-01-01T00:00:00Z'.\n"
1485
+ )
1486
+ _sys.exit(1)
1487
+ _as_of = _as_of_norm
1488
+ as_of_qs = f"&as_of={quote(_as_of)}" if _as_of else ""
1456
1489
  result = daemon_request(
1457
1490
  "GET",
1458
1491
  f"/recall?q={quote(args.query)}&limit={args.limit}"
1459
- f"&session_id={quote(session_id)}{fast_qs}{scope_qs}{window_qs}",
1492
+ f"&session_id={quote(session_id)}{fast_qs}{scope_qs}{window_qs}{as_of_qs}",
1460
1493
  )
1461
1494
  if result and "results" in result:
1462
1495
  # Format daemon response same as engine response
@@ -1522,6 +1555,10 @@ def _cli_record_signals(config, query, results):
1522
1555
 
1523
1556
  def cmd_forget(args: Namespace) -> None:
1524
1557
  """Delete daemon-queried memories matching a query."""
1558
+ from superlocalmemory.core.admission import gate_cli_mutation
1559
+ from superlocalmemory.core.operation_request import OperationKind
1560
+ gate_cli_mutation(OperationKind.FORGET)
1561
+
1525
1562
  import urllib.parse
1526
1563
 
1527
1564
  from superlocalmemory.cli.daemon import (
@@ -1644,6 +1681,10 @@ def cmd_forget(args: Namespace) -> None:
1644
1681
 
1645
1682
  def cmd_delete(args: Namespace) -> None:
1646
1683
  """Delete a specific memory by exact fact ID."""
1684
+ from superlocalmemory.core.admission import gate_cli_mutation
1685
+ from superlocalmemory.core.operation_request import OperationKind
1686
+ gate_cli_mutation(OperationKind.FORGET)
1687
+
1647
1688
  import urllib.parse
1648
1689
 
1649
1690
  from superlocalmemory.cli.daemon import (
@@ -1711,6 +1752,10 @@ def cmd_delete(args: Namespace) -> None:
1711
1752
 
1712
1753
  def cmd_update(args: Namespace) -> None:
1713
1754
  """Update the content of a specific memory by exact fact ID."""
1755
+ from superlocalmemory.core.admission import gate_cli_mutation
1756
+ from superlocalmemory.core.operation_request import OperationKind
1757
+ gate_cli_mutation(OperationKind.CORRECT)
1758
+
1714
1759
  import urllib.parse
1715
1760
 
1716
1761
  from superlocalmemory.cli.daemon import (
@@ -1863,7 +1908,7 @@ def cmd_status(args: Namespace) -> None:
1863
1908
  ])
1864
1909
  return
1865
1910
 
1866
- print("SuperLocalMemory V3")
1911
+ print("SuperLocalMemory V4")
1867
1912
  print(f" Mode: {config.mode.value.upper()}")
1868
1913
  print(f" Provider: {config.llm.provider or 'none'}")
1869
1914
  print(
@@ -2023,14 +2068,36 @@ def _gather_optimize_surface_b() -> dict:
2023
2068
  def _readline_with_timeout(
2024
2069
  stream, timeout_sec: float,
2025
2070
  ) -> tuple[str | None, Exception | None]:
2026
- """Read one line from a pipe-like stream without POSIX-only select().
2071
+ """Read one line from a pipe-like stream with a deadline.
2027
2072
 
2028
- Windows select() only accepts sockets, not subprocess pipes. A bounded
2029
- helper thread keeps the embedding-worker probe cross-platform while
2030
- preserving the existing timeout behavior.
2073
+ Prefer a selector poll of the stream's file descriptor on POSIX so a
2074
+ timeout never abandons a thread blocked in readline() (thread + FD leak).
2075
+ Windows select() only accepts sockets, not subprocess pipes — fall back
2076
+ to a bounded helper thread there and for streams without a usable fileno.
2031
2077
  """
2078
+ import selectors
2032
2079
  import threading
2033
2080
 
2081
+ timeout_sec = max(0.0, float(timeout_sec))
2082
+ fd: int | None
2083
+ try:
2084
+ raw_fd = stream.fileno()
2085
+ fd = raw_fd if isinstance(raw_fd, int) else None
2086
+ except (AttributeError, OSError, ValueError, TypeError):
2087
+ fd = None
2088
+
2089
+ if fd is not None and sys.platform != "win32":
2090
+ try:
2091
+ with selectors.DefaultSelector() as sel:
2092
+ sel.register(fd, selectors.EVENT_READ)
2093
+ events = sel.select(timeout=timeout_sec)
2094
+ if not events:
2095
+ return None, None
2096
+ line = stream.readline()
2097
+ return (line if isinstance(line, str) else None), None
2098
+ except (OSError, ValueError) as exc:
2099
+ return None, exc
2100
+
2034
2101
  result: dict[str, object] = {}
2035
2102
 
2036
2103
  def _read() -> None:
@@ -2091,6 +2158,7 @@ _COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
2091
2158
  ("Health & self-healing", [
2092
2159
  ("doctor", "Full pre-flight check (add --fix to auto-repair)"),
2093
2160
  ("health", "Quick math/retrieval layer status"),
2161
+ ("ops", "List/resolve stuck, dead-lettered, or degraded operations (admin)"),
2094
2162
  ("diagnostics", "Export a local diagnostics bundle"),
2095
2163
  ("evidence", "Build/inspect evidence bundles"),
2096
2164
  ("rotate-token", "Rotate the local dashboard install token"),
@@ -2207,7 +2275,7 @@ def cmd_help(args: Namespace) -> None:
2207
2275
  return
2208
2276
 
2209
2277
  from superlocalmemory.cli.json_output import _get_version
2210
- print(f"SuperLocalMemory V3 ({_get_version()}) — command overview")
2278
+ print(f"SuperLocalMemory V4 ({_get_version()}) — command overview")
2211
2279
  print("=" * 58)
2212
2280
  print("Run any command with -h for its options, e.g. slm recall -h\n")
2213
2281
  for title, rows in _COMMAND_GROUPS:
@@ -2254,7 +2322,7 @@ def cmd_doctor(args: Namespace) -> None:
2254
2322
  print(line)
2255
2323
 
2256
2324
  if not use_json:
2257
- print("SuperLocalMemory V3 — Doctor (Pre-flight Check)")
2325
+ print("SuperLocalMemory V4 — Doctor (Pre-flight Check)")
2258
2326
  print("=" * 50)
2259
2327
  print()
2260
2328
 
@@ -2286,6 +2354,32 @@ def cmd_doctor(args: Namespace) -> None:
2286
2354
  if not use_json:
2287
2355
  print(f" auto-repair error: {_fix_exc}\n")
2288
2356
 
2357
+ # H5 — stale-artifact reap (additive; runs after component healer).
2358
+ # Removes provably-dead lock/PID files so the doctor report below
2359
+ # reflects the healed state. Fail-open — never blocks doctor.
2360
+ try:
2361
+ from superlocalmemory.infra.self_heal import reap_stale_artifacts
2362
+ from superlocalmemory.infra.data_root import canonical_data_root
2363
+
2364
+ _sh_report = reap_stale_artifacts(canonical_data_root())
2365
+ if not use_json:
2366
+ if _sh_report["removed"]:
2367
+ print(
2368
+ f" Cleaned {len(_sh_report['removed'])} stale"
2369
+ " lock/pid file(s):"
2370
+ )
2371
+ for _item in _sh_report["removed"]:
2372
+ print(
2373
+ f" - {Path(_item['path']).name}"
2374
+ f" ({_item['reason']})"
2375
+ )
2376
+ else:
2377
+ print(" No stale lock/pid files found.")
2378
+ print()
2379
+ except Exception as _sh_exc:
2380
+ if not use_json:
2381
+ print(f" stale-artifact cleanup skipped: {_sh_exc}\n")
2382
+
2289
2383
  # 1. Python version
2290
2384
  v = sys.version_info
2291
2385
  if v >= (3, 11):
@@ -2884,7 +2978,7 @@ def cmd_warmup(_args: Namespace) -> None:
2884
2978
  """
2885
2979
  import superlocalmemory.core.embeddings as _emb_mod
2886
2980
 
2887
- print("SuperLocalMemory V3 — Embedding Model Warmup")
2981
+ print("SuperLocalMemory V4 — Embedding Model Warmup")
2888
2982
  print("=" * 50)
2889
2983
  print(f" Python: {sys.executable}")
2890
2984
  print(f" Model: nomic-ai/nomic-embed-text-v1.5 (~500MB)")
@@ -2994,10 +3088,10 @@ def cmd_dashboard(args: Namespace) -> None:
2994
3088
 
2995
3089
  port = getattr(args, "port", None) or _get_port()
2996
3090
 
2997
- print(" SuperLocalMemory V3 — Web Dashboard")
3091
+ print(" SuperLocalMemory V4 — Web Dashboard")
2998
3092
  print(f" Starting daemon if needed...")
2999
3093
 
3000
- if not ensure_daemon():
3094
+ if not ensure_daemon(port=port):
3001
3095
  print(" ✗ Could not start daemon. Run `slm doctor` to diagnose.")
3002
3096
  sys.exit(1)
3003
3097
 
@@ -3057,6 +3151,12 @@ def cmd_profile(args: Namespace) -> None:
3057
3151
  Writes to BOTH SQLite and profiles.json so CLI, Dashboard, and
3058
3152
  MCP all see the same profiles.
3059
3153
  """
3154
+ action = getattr(args, "action", "list")
3155
+ if action in ("switch", "create"):
3156
+ from superlocalmemory.core.admission import gate_cli_mutation
3157
+ from superlocalmemory.core.operation_request import OperationKind
3158
+ gate_cli_mutation(OperationKind.PROFILE_SWITCH)
3159
+
3060
3160
  from superlocalmemory.core.config import SLMConfig
3061
3161
  from superlocalmemory.storage.database import DatabaseManager
3062
3162
  from superlocalmemory.storage import schema
@@ -3598,6 +3698,10 @@ def cmd_observe(args: Namespace) -> None:
3598
3698
  print("No content to observe.")
3599
3699
  return
3600
3700
 
3701
+ from superlocalmemory.core.admission import gate_cli_mutation
3702
+ from superlocalmemory.core.operation_request import OperationKind
3703
+ gate_cli_mutation(OperationKind.REMEMBER)
3704
+
3601
3705
  # V3.3.28: Route through daemon (singleton engine, single embedding worker).
3602
3706
  # This is the P0 fix for the memory blast incident of April 7, 2026.
3603
3707
  try:
@@ -3665,6 +3769,9 @@ def cmd_observe(args: Namespace) -> None:
3665
3769
 
3666
3770
  def cmd_decay(args: Namespace) -> None:
3667
3771
  """Run Ebbinghaus forgetting decay cycle."""
3772
+ from superlocalmemory.core.admission import gate_cli_mutation
3773
+ from superlocalmemory.core.operation_request import OperationKind
3774
+ gate_cli_mutation(OperationKind.CONSOLIDATE)
3668
3775
  from superlocalmemory.core.config import SLMConfig
3669
3776
  from superlocalmemory.core.engine import MemoryEngine
3670
3777
 
@@ -3721,6 +3828,9 @@ def cmd_decay(args: Namespace) -> None:
3721
3828
 
3722
3829
  def cmd_quantize(args: Namespace) -> None:
3723
3830
  """Run EAP embedding quantization cycle."""
3831
+ from superlocalmemory.core.admission import gate_cli_mutation
3832
+ from superlocalmemory.core.operation_request import OperationKind
3833
+ gate_cli_mutation(OperationKind.CONSOLIDATE)
3724
3834
  from superlocalmemory.core.config import SLMConfig
3725
3835
  from superlocalmemory.core.engine import MemoryEngine
3726
3836
 
@@ -3779,6 +3889,10 @@ def cmd_quantize(args: Namespace) -> None:
3779
3889
 
3780
3890
  def cmd_consolidate(args: Namespace) -> None:
3781
3891
  """Run cognitive consolidation pipeline."""
3892
+ from superlocalmemory.core.admission import gate_cli_mutation
3893
+ from superlocalmemory.core.operation_request import OperationKind
3894
+ gate_cli_mutation(OperationKind.CONSOLIDATE)
3895
+
3782
3896
  from superlocalmemory.core.config import SLMConfig
3783
3897
  from superlocalmemory.core.engine import MemoryEngine
3784
3898
 
@@ -508,9 +508,13 @@ def _start_daemon_subprocess() -> bool:
508
508
  return _wait_for_daemon(timeout=60)
509
509
 
510
510
 
511
- def ensure_daemon() -> bool:
511
+ def ensure_daemon(*, port: int | None = None) -> bool:
512
512
  """Start daemon if not running. Returns True if daemon is ready.
513
513
 
514
+ ``port`` — when supplied, the daemon is started (or verified) on this port
515
+ instead of the configured default. The dashboard passes its own ``--port``
516
+ here so the bind authority matches the URL shown to the user.
517
+
514
518
  v3.4.4 BULLETPROOF:
515
519
  1. If PID alive → return True immediately (even if warming up)
516
520
  2. File lock prevents two callers from starting concurrent daemons
@@ -179,7 +179,7 @@ def main() -> None:
179
179
 
180
180
  parser = argparse.ArgumentParser(
181
181
  prog="slm",
182
- description=f"SuperLocalMemory V3 ({_ver}) — AI agent memory with mathematical foundations",
182
+ description=f"SuperLocalMemory V4 ({_ver}) — AI agent memory with mathematical foundations",
183
183
  epilog=_HELP_EPILOG,
184
184
  formatter_class=argparse.RawDescriptionHelpFormatter,
185
185
  )
@@ -388,6 +388,12 @@ def main() -> None:
388
388
  "(24h, 7d, 30d, 1y) or an explicit range (2026-07-01..2026-07-31). "
389
389
  "Default: no time filter.",
390
390
  )
391
+ recall_p.add_argument(
392
+ "--as-of", dest="as_of", default="",
393
+ help="Point-in-time recall: an ISO-8601 timestamp "
394
+ "(2026-01-01T00:00:00+00:00) that pins retrieval to a temporal "
395
+ "snapshot. Default: current-state recall.",
396
+ )
391
397
  recall_p.add_argument(
392
398
  "--fast", action="store_true",
393
399
  help="Force-skip the internal agentic verification round (all six retrieval "
@@ -513,7 +519,7 @@ def main() -> None:
513
519
  sub.add_parser("mcp", help="Start MCP server (stdio transport for IDE integration)")
514
520
  sub.add_parser("warmup", help="Pre-download embedding model (~500MB, one-time)")
515
521
 
516
- dashboard_p = sub.add_parser("dashboard", help="Open 17-tab web dashboard")
522
+ dashboard_p = sub.add_parser("dashboard", help="Open web dashboard")
517
523
  dashboard_p.add_argument(
518
524
  "--port", type=int, default=8765, help="Port (default 8765)",
519
525
  )
@@ -903,6 +909,33 @@ def main() -> None:
903
909
  _sp.add_argument("--json", action="store_true",
904
910
  help="Output structured JSON (agent-native)")
905
911
 
912
+ # Wave-3: operational recovery & admin remediation
913
+ ops_p = sub.add_parser(
914
+ "ops",
915
+ help="Operational recovery: list failures, resolve stuck ops, check status",
916
+ )
917
+ ops_sub = ops_p.add_subparsers(dest="ops_command", title="ops subcommands")
918
+ ops_list_p = ops_sub.add_parser(
919
+ "list", help="List all failed, stuck, or degraded operations (admin)")
920
+ ops_list_p.add_argument(
921
+ "--profile", default=None, metavar="PROFILE",
922
+ help="Filter results to a specific profile (default: all)")
923
+ ops_list_p.add_argument(
924
+ "--json", action="store_true", help="Output structured JSON (agent-native)")
925
+ ops_resolve_p = ops_sub.add_parser(
926
+ "resolve", help="Admin action: retry / force-reconcile / cancel a stuck op")
927
+ ops_resolve_p.add_argument("operation_id", help="Operation ID from slm ops list")
928
+ ops_resolve_p.add_argument(
929
+ "--action", required=True,
930
+ choices=["retry", "force_reconcile", "cancel"],
931
+ help="Remediation action to apply")
932
+ ops_resolve_p.add_argument(
933
+ "--json", action="store_true", help="Output structured JSON (agent-native)")
934
+ ops_status_p = ops_sub.add_parser(
935
+ "status", help="Quick failure count + writer stall overview")
936
+ ops_status_p.add_argument(
937
+ "--json", action="store_true", help="Output structured JSON (agent-native)")
938
+
906
939
  args = parser.parse_args()
907
940
 
908
941
  if not args.command:
@@ -0,0 +1,281 @@
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
+ """``slm ops`` — operational recovery & admin remediation.
6
+
7
+ Subcommands:
8
+
9
+ * ``slm ops list [--profile P]``
10
+ Show all failed, stuck, or degraded operations grouped by category.
11
+ Proxies GET /operations/failed on the running daemon.
12
+
13
+ * ``slm ops resolve <id> --action {retry|force_reconcile|cancel}``
14
+ Admin action on a specific operation.
15
+ Proxies POST /operations/<id>/resolve on the running daemon.
16
+
17
+ * ``slm ops status``
18
+ Quick overview: failure counts + writer stall state from /status.
19
+ No authentication required (status is public).
20
+
21
+ RBAC: list / resolve require OWNER or ADMIN role on the daemon.
22
+ Unauthenticated users see a clear permission error.
23
+
24
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json as _json
30
+ import sys
31
+ import urllib.error as _uerr
32
+ import urllib.request as _urq
33
+ from argparse import Namespace
34
+ from typing import Any
35
+
36
+
37
+ _VALID_ACTIONS = ("retry", "force_reconcile", "cancel")
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Internal helpers
42
+ # ---------------------------------------------------------------------------
43
+
44
+ def _get_daemon_port() -> int:
45
+ """Return the active daemon port (default 8765)."""
46
+ try:
47
+ from superlocalmemory.cli.daemon import _get_port
48
+ return _get_port()
49
+ except Exception:
50
+ return 8765
51
+
52
+
53
+ def _daemon_get(path: str, timeout_s: float = 10.0) -> dict | None:
54
+ """HTTP GET to the daemon; return parsed JSON or None on failure."""
55
+ port = _get_daemon_port()
56
+ url = f"http://127.0.0.1:{port}{path}"
57
+ try:
58
+ with _urq.urlopen(url, timeout=timeout_s) as resp: # noqa: S310
59
+ raw = resp.read().decode()
60
+ return _json.loads(raw)
61
+ except _uerr.HTTPError as exc:
62
+ if exc.code == 403:
63
+ _die(
64
+ "Permission denied: list/resolve requires OWNER or ADMIN role.\n"
65
+ "Check your SLM credentials or ask your administrator."
66
+ )
67
+ body = exc.read().decode(errors="replace") if hasattr(exc, "read") else str(exc)
68
+ _die(f"Daemon returned HTTP {exc.code}: {body}")
69
+ except _uerr.URLError as exc:
70
+ _die(
71
+ f"Could not reach SLM daemon at {url}: {exc.reason}\n"
72
+ "Make sure the daemon is running: slm serve"
73
+ )
74
+ return None # unreachable; _die exits
75
+
76
+
77
+ def _daemon_post(path: str, body: dict, timeout_s: float = 10.0) -> dict | None:
78
+ """HTTP POST to the daemon; return parsed JSON or None on failure."""
79
+ port = _get_daemon_port()
80
+ url = f"http://127.0.0.1:{port}{path}"
81
+ try:
82
+ req = _urq.Request(
83
+ url,
84
+ data=_json.dumps(body).encode(),
85
+ headers={"Content-Type": "application/json"},
86
+ method="POST",
87
+ )
88
+ with _urq.urlopen(req, timeout=timeout_s) as resp: # noqa: S310
89
+ raw = resp.read().decode()
90
+ return _json.loads(raw)
91
+ except _uerr.HTTPError as exc:
92
+ if exc.code == 403:
93
+ _die(
94
+ "Permission denied: resolve requires OWNER or ADMIN role.\n"
95
+ "Check your SLM credentials or ask your administrator."
96
+ )
97
+ if exc.code == 400:
98
+ body_txt = exc.read().decode(errors="replace") if hasattr(exc, "read") else str(exc)
99
+ _die(f"Bad request: {body_txt}")
100
+ body_txt = exc.read().decode(errors="replace") if hasattr(exc, "read") else str(exc)
101
+ _die(f"Daemon returned HTTP {exc.code}: {body_txt}")
102
+ except _uerr.URLError as exc:
103
+ _die(
104
+ f"Could not reach SLM daemon at {url}: {exc.reason}\n"
105
+ "Make sure the daemon is running: slm serve"
106
+ )
107
+ return None # unreachable; _die exits
108
+
109
+
110
+ def _die(message: str) -> None:
111
+ print(f"error: {message}", file=sys.stderr)
112
+ sys.exit(1)
113
+
114
+
115
+ def _print_json(data: Any) -> None:
116
+ print(_json.dumps(data, indent=2, default=str))
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Subcommand handlers
121
+ # ---------------------------------------------------------------------------
122
+
123
+ def _cmd_ops_list(args: Namespace) -> None:
124
+ """List all failed, stuck, or degraded operations."""
125
+ profile = getattr(args, "profile", None)
126
+ path = "/operations/failed"
127
+ if profile:
128
+ path = f"{path}?profile={profile}"
129
+
130
+ data = _daemon_get(path)
131
+ if data is None:
132
+ return
133
+
134
+ if getattr(args, "json", False):
135
+ _print_json(data)
136
+ return
137
+
138
+ total: int = data.get("total", 0)
139
+ if total == 0:
140
+ print("All operations healthy. No failures detected.")
141
+ return
142
+
143
+ print(f"Failed operations: {total} total\n")
144
+
145
+ dead_letter = data.get("dead_letter", [])
146
+ if dead_letter:
147
+ print(f"--- Dead-letter (ingestion exhausted, {len(dead_letter)}) ---")
148
+ for entry in dead_letter:
149
+ print(
150
+ f" [{entry.get('operation_id', '?')}] "
151
+ f"type={entry.get('operation_type', '?')} "
152
+ f"attempts={entry.get('attempts', '?')} "
153
+ f"profile={entry.get('profile_id', '?')}"
154
+ )
155
+ if entry.get("error"):
156
+ print(f" error: {entry['error']}")
157
+ print()
158
+
159
+ degraded = data.get("degraded_manifests", [])
160
+ if degraded:
161
+ print(f"--- Degraded manifests ({len(degraded)}) ---")
162
+ for entry in degraded:
163
+ print(
164
+ f" [{entry.get('operation_id', '?')}] "
165
+ f"state={entry.get('state', '?')} "
166
+ f"profile={entry.get('profile_id', '?')}"
167
+ )
168
+ print()
169
+
170
+ exhausted = data.get("exhausted_obligations", [])
171
+ if exhausted:
172
+ print(f"--- Exhausted projection obligations ({len(exhausted)}) ---")
173
+ for entry in exhausted:
174
+ print(
175
+ f" [{entry.get('operation_id', '?')}] "
176
+ f"kind={entry.get('kind', '?')} "
177
+ f"attempts={entry.get('attempts', '?')} "
178
+ f"profile={entry.get('profile_id', '?')}"
179
+ )
180
+ print()
181
+
182
+ print("Use `slm ops resolve <id> --action cancel|retry|force_reconcile` to remediate.")
183
+
184
+
185
+ def _cmd_ops_resolve(args: Namespace) -> None:
186
+ """Admin action on a specific operation."""
187
+ operation_id: str = args.operation_id
188
+ action: str = args.action
189
+
190
+ if action not in _VALID_ACTIONS:
191
+ _die(f"--action must be one of: {', '.join(_VALID_ACTIONS)}")
192
+
193
+ result = _daemon_post(
194
+ f"/operations/{operation_id}/resolve",
195
+ {"action": action},
196
+ )
197
+ if result is None:
198
+ return
199
+
200
+ if getattr(args, "json", False):
201
+ _print_json(result)
202
+ return
203
+
204
+ success = result.get("success", False)
205
+ if success:
206
+ print(
207
+ f"OK: operation {operation_id!r} resolved with action={action!r}. "
208
+ f"{result.get('message', '')}"
209
+ )
210
+ else:
211
+ reason = result.get("reason") or result.get("error") or "unknown reason"
212
+ print(f"Resolve failed: {reason}", file=sys.stderr)
213
+ sys.exit(1)
214
+
215
+
216
+ def _cmd_ops_status(args: Namespace) -> None:
217
+ """Quick ops-focused health status from the daemon."""
218
+ data = _daemon_get("/status")
219
+ if data is None:
220
+ return
221
+
222
+ fields = {
223
+ "dead_letter_count": data.get("dead_letter_count", 0),
224
+ "degraded_operations": data.get("degraded_operations", 0),
225
+ "exhausted_obligations": data.get("exhausted_obligations", 0),
226
+ "writer_stalled": data.get("writer_stalled", False),
227
+ "writer_stalled_op_id": data.get("writer_stalled_op_id"),
228
+ "writer_stalled_age_s": data.get("writer_stalled_age_s"),
229
+ }
230
+
231
+ if getattr(args, "json", False):
232
+ _print_json(fields)
233
+ return
234
+
235
+ total_issues = (
236
+ fields["dead_letter_count"]
237
+ + fields["degraded_operations"]
238
+ + fields["exhausted_obligations"]
239
+ )
240
+ stalled = fields["writer_stalled"]
241
+
242
+ if total_issues == 0 and not stalled:
243
+ print("Operations status: HEALTHY — no failures detected.")
244
+ return
245
+
246
+ print("Operations status: DEGRADED\n")
247
+ if fields["dead_letter_count"]:
248
+ print(f" dead-letter entries : {fields['dead_letter_count']}")
249
+ if fields["degraded_operations"]:
250
+ print(f" degraded manifests : {fields['degraded_operations']}")
251
+ if fields["exhausted_obligations"]:
252
+ print(f" exhausted obligations : {fields['exhausted_obligations']}")
253
+ if stalled:
254
+ op_id = fields["writer_stalled_op_id"] or "?"
255
+ age = fields["writer_stalled_age_s"]
256
+ age_str = f" (age {age:.1f}s)" if age is not None else ""
257
+ print(f" writer STALLED : op={op_id}{age_str}")
258
+ print("\nRun `slm ops list` to see details, or check the dashboard.")
259
+
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # Public entry point
263
+ # ---------------------------------------------------------------------------
264
+
265
+ def cmd_ops(args: Namespace) -> None:
266
+ """Dispatch ``slm ops`` subcommands."""
267
+ sub = getattr(args, "ops_command", None)
268
+ handlers = {
269
+ "list": _cmd_ops_list,
270
+ "resolve": _cmd_ops_resolve,
271
+ "status": _cmd_ops_status,
272
+ }
273
+ handler = handlers.get(sub)
274
+ if handler:
275
+ handler(args)
276
+ else:
277
+ print("Usage: slm ops <list|resolve|status> [options]")
278
+ print(" slm ops list [--profile P] [--json]")
279
+ print(" slm ops resolve <id> --action {retry|force_reconcile|cancel} [--json]")
280
+ print(" slm ops status [--json]")
281
+ sys.exit(1)
@@ -410,7 +410,7 @@ def run_wizard(auto: bool = False) -> None:
410
410
 
411
411
  print()
412
412
  print("╔══════════════════════════════════════════════════════════╗")
413
- print("║ SuperLocalMemory V3 — The Unified Brain ║")
413
+ print("║ SuperLocalMemory V4 — The Unified Brain ║")
414
414
  print("║ by Varun Pratap Bhardwaj / Qualixar ║")
415
415
  print("╚══════════════════════════════════════════════════════════╝")
416
416
  print()