superlocalmemory 3.8.13 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -121
  3. package/README.md +65 -63
  4. package/docs/pi-dev-integration.md +1 -1
  5. package/package.json +6 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +1 -1
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +1 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +1 -1
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  29. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  31. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  32. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +11 -4
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/cli/commands.py +125 -11
  41. package/src/superlocalmemory/cli/daemon.py +5 -1
  42. package/src/superlocalmemory/cli/main.py +35 -2
  43. package/src/superlocalmemory/cli/ops_cmd.py +281 -0
  44. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  45. package/src/superlocalmemory/compliance/audit.py +65 -0
  46. package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
  47. package/src/superlocalmemory/compliance/gdpr.py +416 -20
  48. package/src/superlocalmemory/compliance/retention.py +74 -22
  49. package/src/superlocalmemory/compliance/scheduler.py +78 -9
  50. package/src/superlocalmemory/core/actor_context.py +166 -0
  51. package/src/superlocalmemory/core/admission.py +549 -0
  52. package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
  53. package/src/superlocalmemory/core/config.py +202 -24
  54. package/src/superlocalmemory/core/consolidation_engine.py +13 -13
  55. package/src/superlocalmemory/core/context_cache.py +28 -0
  56. package/src/superlocalmemory/core/embeddings.py +64 -2
  57. package/src/superlocalmemory/core/engine.py +7 -2
  58. package/src/superlocalmemory/core/engine_ingestion.py +65 -3
  59. package/src/superlocalmemory/core/engine_wiring.py +36 -9
  60. package/src/superlocalmemory/core/ingest_policy.py +38 -0
  61. package/src/superlocalmemory/core/maintenance.py +255 -0
  62. package/src/superlocalmemory/core/modes.py +40 -13
  63. package/src/superlocalmemory/core/mutations.py +437 -44
  64. package/src/superlocalmemory/core/operation_policy.py +92 -0
  65. package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
  66. package/src/superlocalmemory/core/operation_request.py +127 -0
  67. package/src/superlocalmemory/core/ops_remediation.py +542 -0
  68. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  69. package/src/superlocalmemory/core/remember_runtime.py +202 -4
  70. package/src/superlocalmemory/core/remote_mode.py +20 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +150 -0
  72. package/src/superlocalmemory/core/topic_signature.py +19 -4
  73. package/src/superlocalmemory/core/transactions/__init__.py +78 -0
  74. package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
  75. package/src/superlocalmemory/core/transactions/erasure.py +825 -0
  76. package/src/superlocalmemory/core/transactions/manifest.py +255 -0
  77. package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
  78. package/src/superlocalmemory/core/transactions/obligations.py +272 -0
  79. package/src/superlocalmemory/core/transactions/owners.py +114 -0
  80. package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
  81. package/src/superlocalmemory/core/transactions/service.py +330 -0
  82. package/src/superlocalmemory/core/worker_pool.py +33 -5
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
  84. package/src/superlocalmemory/encoding/emotional.py +75 -14
  85. package/src/superlocalmemory/encoding/scene_builder.py +115 -13
  86. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  87. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  88. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  89. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  90. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  91. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  92. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  93. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  94. package/src/superlocalmemory/evolution/types.py +30 -8
  95. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  96. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  97. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  98. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  99. package/src/superlocalmemory/infra/backup.py +434 -7
  100. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  101. package/src/superlocalmemory/infra/self_heal.py +401 -0
  102. package/src/superlocalmemory/learning/feedback.py +52 -9
  103. package/src/superlocalmemory/loops/engine.py +10 -0
  104. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  105. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  106. package/src/superlocalmemory/mcp/profiles.py +5 -0
  107. package/src/superlocalmemory/mcp/resources.py +8 -0
  108. package/src/superlocalmemory/mcp/server.py +51 -4
  109. package/src/superlocalmemory/mcp/shared.py +19 -0
  110. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  111. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  112. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  113. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  114. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  115. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  116. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  117. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  118. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  119. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  120. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  121. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  122. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  123. package/src/superlocalmemory/mesh/broker.py +124 -46
  124. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  125. package/src/superlocalmemory/mesh/discovery.py +365 -0
  126. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  127. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  128. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  129. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  130. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  131. package/src/superlocalmemory/optimize/config/store.py +45 -0
  132. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  133. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  134. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  135. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  136. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  137. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  138. package/src/superlocalmemory/retrieval/engine.py +19 -4
  139. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  140. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  141. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  142. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  143. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  144. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  145. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  146. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  147. package/src/superlocalmemory/server/api.py +5 -5
  148. package/src/superlocalmemory/server/egress_policy.py +258 -0
  149. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  150. package/src/superlocalmemory/server/route_mutations.py +20 -0
  151. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  152. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  153. package/src/superlocalmemory/server/routes/events.py +15 -0
  154. package/src/superlocalmemory/server/routes/memories.py +56 -3
  155. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  156. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  157. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  158. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  159. package/src/superlocalmemory/server/routes/ws.py +86 -0
  160. package/src/superlocalmemory/server/ui.py +6 -6
  161. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  162. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  163. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  164. package/src/superlocalmemory/storage/database.py +329 -24
  165. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  166. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  167. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  168. package/src/superlocalmemory/storage/migration_runner.py +140 -417
  169. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  170. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  171. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  172. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  173. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  174. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  175. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  176. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
  177. package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
  178. package/src/superlocalmemory/storage/schema.py +67 -0
  179. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  180. package/src/superlocalmemory/trust/scorer.py +28 -4
  181. package/src/superlocalmemory/ui/index.html +14 -3
  182. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  183. package/src/superlocalmemory/ui/js/brain.js +6 -4
  184. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  185. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  186. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  187. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  188. package/src/superlocalmemory/ui/js/modal.js +272 -5
  189. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  190. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  191. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  192. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  193. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  194. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  195. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  196. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  197. package/src/superlocalmemory/ui/js/settings.js +7 -1
  198. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  199. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  200. package/src/superlocalmemory/cli/post_install.py +0 -114
  201. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  202. package/src/superlocalmemory/core/db_pool.py +0 -80
  203. package/src/superlocalmemory/core/error_catalog.py +0 -113
  204. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  205. package/src/superlocalmemory/core/priority_queue.py +0 -61
  206. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  207. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  208. package/src/superlocalmemory/core/slmignore.py +0 -125
  209. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  210. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  211. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  212. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -1,247 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
- """WebhookDispatcher -- background HTTP POST delivery for memory events.
5
-
6
- Runs on a daemon thread so webhook delivery never blocks the main event
7
- flow. Failed deliveries are retried with exponential back-off (up to
8
- ``MAX_RETRIES`` attempts).
9
-
10
- Security:
11
- * Only ``http://`` and ``https://`` URLs are accepted.
12
- * Private / loopback IPs are rejected.
13
- * 10-second timeout per outgoing request.
14
- """
15
-
16
- import ipaddress
17
- import json
18
- import logging
19
- import socket
20
- import threading
21
- import time
22
- import urllib.parse
23
- from datetime import datetime, timezone
24
- from queue import Empty, Queue
25
- from typing import Dict, Optional
26
-
27
- logger = logging.getLogger("superlocalmemory.webhooks")
28
-
29
- # ---------------------------------------------------------------------------
30
- # Configuration constants
31
- # ---------------------------------------------------------------------------
32
- MAX_RETRIES = 3
33
- RETRY_BACKOFF_BASE = 2 # seconds: 2, 4, 8
34
- REQUEST_TIMEOUT = 10 # seconds
35
- MAX_QUEUE_SIZE = 1000
36
- def _get_version() -> str:
37
- try:
38
- from importlib.metadata import version
39
- return version("superlocalmemory")
40
- except Exception:
41
- return "3.0.0"
42
-
43
-
44
- VERSION = _get_version()
45
-
46
- # stdlib HTTP -- always available
47
- from urllib.request import Request, urlopen # noqa: E402
48
- from urllib.error import HTTPError, URLError # noqa: E402
49
-
50
-
51
- def _is_private_ip(hostname: str) -> bool:
52
- """Return ``True`` if *hostname* resolves to a private / loopback IP."""
53
- try:
54
- ip_str = socket.gethostbyname(hostname)
55
- ip = ipaddress.ip_address(ip_str)
56
- return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved
57
- except (socket.gaierror, ValueError):
58
- return False
59
-
60
-
61
- class WebhookDispatcher:
62
- """Background webhook delivery with retry logic.
63
-
64
- Thread-safe. Enqueues deliveries and processes them on a dedicated
65
- daemon thread.
66
- """
67
-
68
- _instances: Dict[str, "WebhookDispatcher"] = {}
69
- _instances_lock = threading.Lock()
70
-
71
- @classmethod
72
- def get_instance(cls, name: str = "default") -> "WebhookDispatcher":
73
- """Get or create a named singleton."""
74
- with cls._instances_lock:
75
- if name not in cls._instances:
76
- cls._instances[name] = cls()
77
- return cls._instances[name]
78
-
79
- @classmethod
80
- def reset_instance(cls, name: Optional[str] = None) -> None:
81
- """Remove singleton(s). Primarily for testing."""
82
- with cls._instances_lock:
83
- if name is None:
84
- for inst in cls._instances.values():
85
- inst.close()
86
- cls._instances.clear()
87
- elif name in cls._instances:
88
- cls._instances[name].close()
89
- del cls._instances[name]
90
-
91
- def __init__(self) -> None:
92
- self._queue: Queue = Queue(maxsize=MAX_QUEUE_SIZE)
93
- self._closed = False
94
- self._stats = {
95
- "dispatched": 0,
96
- "succeeded": 0,
97
- "failed": 0,
98
- "retries": 0,
99
- }
100
- self._stats_lock = threading.Lock()
101
-
102
- self._worker = threading.Thread(
103
- target=self._worker_loop,
104
- name="slm-webhook-worker",
105
- daemon=True,
106
- )
107
- self._worker.start()
108
- logger.info("WebhookDispatcher started")
109
-
110
- # ----- public API -----
111
-
112
- def dispatch(self, event: dict, webhook_url: str) -> None:
113
- """Enqueue a webhook delivery.
114
-
115
- Raises:
116
- ValueError: If *webhook_url* is invalid or private.
117
- RuntimeError: If the dispatcher is closed.
118
- """
119
- if self._closed:
120
- raise RuntimeError("WebhookDispatcher is closed")
121
-
122
- if not webhook_url or not (
123
- webhook_url.startswith("http://") or webhook_url.startswith("https://")
124
- ):
125
- raise ValueError(f"Invalid webhook URL: {webhook_url}")
126
-
127
- parsed = urllib.parse.urlparse(webhook_url)
128
- if parsed.hostname and _is_private_ip(parsed.hostname):
129
- raise ValueError(
130
- f"Webhook URL points to private/internal network: {webhook_url}"
131
- )
132
-
133
- try:
134
- self._queue.put_nowait(
135
- {
136
- "event": event,
137
- "url": webhook_url,
138
- "attempt": 0,
139
- "enqueued_at": datetime.now(timezone.utc).isoformat(),
140
- }
141
- )
142
- with self._stats_lock:
143
- self._stats["dispatched"] += 1
144
- except Exception:
145
- logger.warning("Webhook queue full, dropping event for %s", webhook_url)
146
-
147
- def get_stats(self) -> dict:
148
- """Return delivery statistics snapshot."""
149
- with self._stats_lock:
150
- return dict(self._stats)
151
-
152
- def close(self) -> None:
153
- """Shut down the dispatcher, draining remaining items."""
154
- if self._closed:
155
- return
156
- self._closed = True
157
- self._queue.put(None) # sentinel
158
- if self._worker.is_alive():
159
- self._worker.join(timeout=5)
160
- logger.info("WebhookDispatcher closed: stats=%s", self._stats)
161
-
162
- @property
163
- def is_closed(self) -> bool:
164
- return self._closed
165
-
166
- @property
167
- def queue_size(self) -> int:
168
- return self._queue.qsize()
169
-
170
- # ----- internal -----
171
-
172
- def _worker_loop(self) -> None:
173
- """Background loop: dequeue and deliver."""
174
- while not self._closed:
175
- try:
176
- item = self._queue.get(timeout=1.0)
177
- except Empty:
178
- continue
179
-
180
- if item is None: # shutdown sentinel
181
- self._queue.task_done()
182
- break
183
-
184
- self._deliver(item)
185
- self._queue.task_done()
186
-
187
- def _deliver(self, item: dict) -> None:
188
- """Attempt delivery with exponential-backoff retry."""
189
- event = item["event"]
190
- url = item["url"]
191
- attempt = item["attempt"]
192
-
193
- try:
194
- payload = json.dumps(
195
- {
196
- "event": event,
197
- "delivered_at": datetime.now(timezone.utc).isoformat(),
198
- "attempt": attempt + 1,
199
- "source": "superlocalmemory",
200
- "version": VERSION,
201
- }
202
- ).encode("utf-8")
203
-
204
- req = Request(
205
- url,
206
- data=payload,
207
- headers={
208
- "Content-Type": "application/json",
209
- "User-Agent": f"SuperLocalMemory/{VERSION}",
210
- "X-SLM-Event-Type": event.get("event_type", "unknown"),
211
- },
212
- method="POST",
213
- )
214
-
215
- with urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
216
- status = resp.status
217
- if 200 <= status < 300:
218
- with self._stats_lock:
219
- self._stats["succeeded"] += 1
220
- logger.debug("Webhook delivered: url=%s status=%d", url, status)
221
- return
222
- raise HTTPError(url, status, f"HTTP {status}", {}, None)
223
-
224
- except Exception as exc:
225
- logger.warning(
226
- "Webhook delivery failed (attempt %d/%d): url=%s error=%s",
227
- attempt + 1,
228
- MAX_RETRIES,
229
- url,
230
- exc,
231
- )
232
-
233
- if attempt + 1 < MAX_RETRIES:
234
- backoff = RETRY_BACKOFF_BASE ** (attempt + 1)
235
- time.sleep(backoff)
236
- with self._stats_lock:
237
- self._stats["retries"] += 1
238
- item["attempt"] = attempt + 1
239
- self._deliver(item)
240
- else:
241
- with self._stats_lock:
242
- self._stats["failed"] += 1
243
- logger.error(
244
- "Webhook permanently failed after %d attempts: url=%s",
245
- MAX_RETRIES,
246
- url,
247
- )
@@ -1,320 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3
4
-
5
- """Quantization Scheduler -- combined SAGQ + EAP precision management.
6
-
7
- Background quantization worker that periodically reviews all memories and
8
- applies both EAP (forgetting) and SAGQ (network centrality) signals together.
9
-
10
- Conflict resolution: max(EAP_precision, SAGQ_precision) -- safety first.
11
- Core Memory blocks are immune to quantization (HR-01).
12
- Every precision change is logged to fact_access_log for audit trail (HR-09).
13
-
14
- HR-01: Core Memory immune.
15
- HR-07: No-op when config.enabled=False.
16
- HR-08: Synchronous execution (no threading).
17
- HR-09: Audit trail for every change.
18
-
19
- Part of Qualixar | Author: Varun Pratap Bhardwaj
20
- License: AGPL-3.0-or-later
21
- """
22
-
23
- from __future__ import annotations
24
-
25
- import logging
26
- import time
27
- from dataclasses import dataclass
28
- from datetime import UTC, datetime
29
- from typing import Any, Callable, TYPE_CHECKING
30
-
31
- from superlocalmemory.storage.models import _new_id
32
-
33
- if TYPE_CHECKING:
34
- from superlocalmemory.storage.database import DatabaseManager
35
- from superlocalmemory.dynamics.activation_guided_quantization import (
36
- ActivationGuidedQuantizer,
37
- SAGQPrecision,
38
- )
39
- from superlocalmemory.core.config import SAGQConfig
40
-
41
- logger = logging.getLogger(__name__)
42
-
43
-
44
- # ---------------------------------------------------------------------------
45
- # Data classes (frozen -- all immutable, Rule 10)
46
- # ---------------------------------------------------------------------------
47
-
48
-
49
- @dataclass(frozen=True)
50
- class PrecisionChange:
51
- """Record of a single precision change for audit trail."""
52
-
53
- fact_id: str
54
- old_bit_width: int
55
- new_bit_width: int
56
- action: str # "upgrade" | "downgrade"
57
- centrality: float
58
- sagq_signal: int
59
- eap_signal: int
60
- timestamp: str # ISO 8601 datetime string
61
-
62
-
63
- @dataclass(frozen=True)
64
- class SchedulerRunResult:
65
- """Result of a single scheduler run."""
66
-
67
- total_facts: int
68
- upgrades: int
69
- downgrades: int
70
- skipped: int
71
- errors: int
72
- changes: tuple[PrecisionChange, ...]
73
- duration_ms: float
74
-
75
-
76
- # ---------------------------------------------------------------------------
77
- # Bit-width -> quantization level mapping
78
- # ---------------------------------------------------------------------------
79
-
80
- _BW_TO_LEVEL: dict[int, str] = {
81
- 32: "float32",
82
- 8: "int8",
83
- 4: "polar4",
84
- 2: "polar2",
85
- 0: "deleted",
86
- }
87
-
88
-
89
- # ---------------------------------------------------------------------------
90
- # QuantizationScheduler
91
- # ---------------------------------------------------------------------------
92
-
93
-
94
- class QuantizationScheduler:
95
- """Combined SAGQ + EAP quantization scheduler.
96
-
97
- Runs synchronously (HR-08). Called by the consolidation engine
98
- or CLI command on schedule.
99
- """
100
-
101
- def __init__(
102
- self,
103
- db: Any,
104
- sagq: Any,
105
- eap_mapper: Callable[[str], int],
106
- quantized_store: Any,
107
- vector_store: Any,
108
- config: Any,
109
- ) -> None:
110
- """Initialize scheduler. No side effects."""
111
- self._db = db
112
- self._sagq = sagq
113
- self._eap_mapper = eap_mapper
114
- self._quantized_store = quantized_store
115
- self._vector_store = vector_store
116
- self._config = config
117
-
118
- def run(self, profile_id: str) -> SchedulerRunResult:
119
- """Execute one combined SAGQ + EAP quantization pass.
120
-
121
- Algorithm:
122
- 1. Compute SAGQ precision recommendations (centrality + EAP + max())
123
- 2. Exclude core memory facts (HR-01)
124
- 3. Execute upgrades/downgrades with error isolation
125
- 4. Log audit trail for each change (HR-09)
126
- 5. Return summary
127
-
128
- Returns SchedulerRunResult with totals and change records.
129
- """
130
- # HR-07: No-op when disabled
131
- if not self._config.enabled:
132
- return SchedulerRunResult(
133
- total_facts=0, upgrades=0, downgrades=0,
134
- skipped=0, errors=0, changes=(), duration_ms=0.0,
135
- )
136
-
137
- start_time = time.monotonic()
138
-
139
- # Step 2: Get SAGQ precision recommendations
140
- recommendations = self._sagq.compute_sagq_precision_batch(
141
- profile_id, self._eap_mapper,
142
- )
143
-
144
- if not recommendations:
145
- duration_ms = (time.monotonic() - start_time) * 1000
146
- return SchedulerRunResult(
147
- total_facts=0, upgrades=0, downgrades=0,
148
- skipped=0, errors=0, changes=(), duration_ms=duration_ms,
149
- )
150
-
151
- # Step 5a: Get core memory fact IDs (HR-01)
152
- core_fact_ids = self._get_core_fact_ids(profile_id)
153
-
154
- # Step 6: Process each recommendation
155
- upgrades = 0
156
- downgrades = 0
157
- skipped = 0
158
- errors = 0
159
- changes: list[PrecisionChange] = []
160
-
161
- for prec in recommendations:
162
- # HR-01: Core Memory immune
163
- if prec.fact_id in core_fact_ids:
164
- skipped += 1
165
- continue
166
-
167
- if prec.action == "skip":
168
- skipped += 1
169
- continue
170
-
171
- change = self._process_precision_change(prec, profile_id)
172
-
173
- if change is None:
174
- # Error or unable to process
175
- if prec.action in ("downgrade", "upgrade"):
176
- errors += 1
177
- else:
178
- skipped += 1
179
- continue
180
-
181
- if change.action == "downgrade":
182
- downgrades += 1
183
- elif change.action == "upgrade":
184
- upgrades += 1
185
-
186
- changes.append(change)
187
-
188
- duration_ms = (time.monotonic() - start_time) * 1000
189
-
190
- logger.info(
191
- "SAGQ scheduler: %d upgrades, %d downgrades, %d skipped, "
192
- "%d errors in %.1fms",
193
- upgrades, downgrades, skipped, errors, duration_ms,
194
- )
195
-
196
- return SchedulerRunResult(
197
- total_facts=len(recommendations),
198
- upgrades=upgrades,
199
- downgrades=downgrades,
200
- skipped=skipped,
201
- errors=errors,
202
- changes=tuple(changes),
203
- duration_ms=duration_ms,
204
- )
205
-
206
- def _process_precision_change(
207
- self, prec: Any, profile_id: str,
208
- ) -> PrecisionChange | None:
209
- """Process a single precision change with error isolation.
210
-
211
- Each fact is independent -- one failure does not block others.
212
- Returns PrecisionChange on success, None on failure.
213
- """
214
- try:
215
- if prec.action == "downgrade":
216
- # Fetch float32 embedding
217
- emb = self._vector_store.get_embedding(prec.fact_id, profile_id)
218
- if emb is None:
219
- logger.warning(
220
- "SAGQ: No float32 for %s, skip downgrade", prec.fact_id,
221
- )
222
- return None
223
- # Compress to target bit-width
224
- self._quantized_store.compress_fact(
225
- prec.fact_id, profile_id, emb, prec.final_bit_width,
226
- )
227
-
228
- elif prec.action == "upgrade":
229
- # Upgrade = re-compress at higher bit_width from float32 backup
230
- emb = self._vector_store.get_embedding(prec.fact_id, profile_id)
231
- if emb is None:
232
- logger.warning(
233
- "SAGQ: No float32 for %s, skip upgrade", prec.fact_id,
234
- )
235
- return None
236
- self._quantized_store.compress_fact(
237
- prec.fact_id, profile_id, emb, prec.final_bit_width,
238
- )
239
-
240
- else:
241
- return None # skip
242
-
243
- # Update embedding_metadata (Q7)
244
- level = self._bit_width_to_quantization_level(prec.final_bit_width)
245
- self._db.execute(
246
- "UPDATE embedding_metadata "
247
- "SET bit_width = ?, quantization_level = ? "
248
- "WHERE fact_id = ? AND profile_id = ?",
249
- (prec.final_bit_width, level, prec.fact_id, profile_id),
250
- )
251
-
252
- # Audit trail (Q10 -- HR-09)
253
- self._db.execute(
254
- "INSERT INTO fact_access_log "
255
- "(log_id, fact_id, profile_id, accessed_at, access_type, session_id) "
256
- "VALUES (?, ?, ?, datetime('now'), 'consolidation', 'sagq_scheduler')",
257
- (_new_id(), prec.fact_id, profile_id),
258
- )
259
-
260
- now_iso = datetime.now(UTC).isoformat()
261
- return PrecisionChange(
262
- fact_id=prec.fact_id,
263
- old_bit_width=prec.current_bit_width,
264
- new_bit_width=prec.final_bit_width,
265
- action=prec.action,
266
- centrality=prec.centrality,
267
- sagq_signal=prec.sagq_bit_width,
268
- eap_signal=prec.eap_bit_width,
269
- timestamp=now_iso,
270
- )
271
-
272
- except Exception as exc:
273
- logger.error(
274
- "SAGQ: precision change failed for %s: %s", prec.fact_id, exc,
275
- )
276
- return None
277
-
278
- def _get_core_fact_ids(self, profile_id: str) -> set[str]:
279
- """Get fact IDs referenced by core_memory_blocks (immune to quantization).
280
-
281
- Uses json_each() to extract from source_fact_ids JSON array (Q8).
282
- """
283
- try:
284
- rows = self._db.execute(
285
- "SELECT json_each.value as fact_id "
286
- "FROM core_memory_blocks, json_each(core_memory_blocks.source_fact_ids) "
287
- "WHERE core_memory_blocks.profile_id = ?",
288
- (profile_id,),
289
- )
290
- return {dict(r)["fact_id"] for r in rows}
291
- except Exception as exc:
292
- logger.debug("SAGQ: core_memory_blocks query failed: %s", exc)
293
- return set()
294
-
295
- def _bit_width_to_quantization_level(self, bit_width: int) -> str:
296
- """Map bit-width integer to quantization level string."""
297
- return _BW_TO_LEVEL.get(bit_width, "float32")
298
-
299
- def should_run(self, last_run_at: str | None) -> bool:
300
- """Check if enough time has passed since the last run.
301
-
302
- Args:
303
- last_run_at: ISO 8601 datetime of last run, or None if never run.
304
-
305
- Returns True if the scheduler should run now.
306
- """
307
- if last_run_at is None:
308
- return True
309
-
310
- try:
311
- last_run = datetime.fromisoformat(last_run_at)
312
- now = datetime.now(UTC)
313
- # Ensure both are timezone-aware for subtraction
314
- if last_run.tzinfo is None:
315
- last_run = last_run.replace(tzinfo=UTC)
316
- hours_since = (now - last_run).total_seconds() / 3600
317
- return hours_since >= self._config.scheduler_interval_hours
318
- except (ValueError, TypeError) as exc:
319
- logger.warning("SAGQ: Could not parse last_run_at '%s': %s", last_run_at, exc)
320
- return True