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.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- 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
|