superlocalmemory 3.5.8 → 3.6.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 (72) hide show
  1. package/ATTRIBUTION.md +24 -0
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +142 -35
  4. package/package.json +1 -1
  5. package/pyproject.toml +2 -1
  6. package/src/superlocalmemory/__init__.py +1 -1
  7. package/src/superlocalmemory/cli/cache_cmd.py +198 -0
  8. package/src/superlocalmemory/cli/commands.py +80 -2
  9. package/src/superlocalmemory/cli/compress_cmd.py +179 -0
  10. package/src/superlocalmemory/cli/help_cmd.py +197 -0
  11. package/src/superlocalmemory/cli/main.py +122 -0
  12. package/src/superlocalmemory/cli/optimize_cmd.py +175 -0
  13. package/src/superlocalmemory/cli/optimize_constants.py +31 -0
  14. package/src/superlocalmemory/cli/proxy_cmd.py +95 -0
  15. package/src/superlocalmemory/core/config.py +5 -0
  16. package/src/superlocalmemory/core/engine.py +23 -0
  17. package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
  18. package/src/superlocalmemory/llm/backbone.py +10 -4
  19. package/src/superlocalmemory/mcp/server.py +34 -0
  20. package/src/superlocalmemory/mcp/tools_v3.py +6 -2
  21. package/src/superlocalmemory/optimize/NOTICE +11 -0
  22. package/src/superlocalmemory/optimize/__init__.py +0 -0
  23. package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
  24. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
  25. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
  26. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
  27. package/src/superlocalmemory/optimize/adapters/wrap.py +188 -0
  28. package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
  29. package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
  30. package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
  31. package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
  32. package/src/superlocalmemory/optimize/cache/exact.py +85 -0
  33. package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
  34. package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
  35. package/src/superlocalmemory/optimize/cache/manager.py +452 -0
  36. package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
  37. package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
  38. package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
  39. package/src/superlocalmemory/optimize/compress/align.py +153 -0
  40. package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
  41. package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
  42. package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
  43. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
  44. package/src/superlocalmemory/optimize/compress/router.py +548 -0
  45. package/src/superlocalmemory/optimize/config/__init__.py +35 -0
  46. package/src/superlocalmemory/optimize/config/defaults.py +48 -0
  47. package/src/superlocalmemory/optimize/config/schema.py +255 -0
  48. package/src/superlocalmemory/optimize/config/store.py +209 -0
  49. package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
  50. package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
  51. package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
  52. package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
  53. package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
  54. package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
  55. package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
  56. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
  57. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
  58. package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
  59. package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
  60. package/src/superlocalmemory/optimize/proxy/server.py +151 -0
  61. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  62. package/src/superlocalmemory/optimize/storage/db.py +1016 -0
  63. package/src/superlocalmemory/optimize/storage/schema.py +184 -0
  64. package/src/superlocalmemory/server/routes/optimize.py +166 -0
  65. package/src/superlocalmemory/server/routes/v3_api.py +63 -1
  66. package/src/superlocalmemory/server/unified_daemon.py +105 -0
  67. package/src/superlocalmemory/ui/index.html +98 -0
  68. package/src/superlocalmemory/ui/js/ng-shell.js +2 -1
  69. package/src/superlocalmemory/ui/js/optimize.js +173 -0
  70. package/src/superlocalmemory.egg-info/PKG-INFO +2 -1
  71. package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
  72. package/src/superlocalmemory.egg-info/requires.txt +1 -0
@@ -0,0 +1,48 @@
1
+ """Default OptimizeConfig singleton — import-safe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from superlocalmemory.optimize.config.schema import (
6
+ OptimizeConfig, ProviderConfig, TTLConfig,
7
+ )
8
+
9
+ DEFAULT_OPTIMIZE_CONFIG = OptimizeConfig(
10
+ enabled=False,
11
+ proxy_enabled=False,
12
+ cache_enabled=True,
13
+ semantic_enabled=False,
14
+ semantic_return_threshold=0.98,
15
+ semantic_verify_lo=0.90,
16
+ semantic_error_target=0.02,
17
+ semantic_explore_rate=0.10,
18
+ semantic_constant_time=False,
19
+ semantic_centroid_defense=True,
20
+ semantic_multiturn_guard=True,
21
+ semantic_ann_top_k=5,
22
+ semantic_boundary_init=0.95,
23
+ semantic_boundary_floor=0.85,
24
+ semantic_pad_latency_ms=0.0,
25
+ semantic_centroid_min_similarity=0.85,
26
+ compress_enabled=True,
27
+ compress_mode="safe",
28
+ compress_code=True,
29
+ compress_json=True,
30
+ compress_prose=False,
31
+ compress_ccr=True,
32
+ compress_align=True,
33
+ compress_protect_recent=4,
34
+ compress_llmlingua_allow_download=False,
35
+ ttl_seconds=86400,
36
+ ttl=TTLConfig(),
37
+ providers={
38
+ "anthropic": ProviderConfig(enabled=True, base_url=""),
39
+ "openai": ProviderConfig(enabled=True, base_url=""),
40
+ "gemini": ProviderConfig(enabled=True, base_url=""),
41
+ },
42
+ pricing={},
43
+ active_model="claude-sonnet-4-6",
44
+ usd_to_inr_rate=83.5,
45
+ pricing_overrides={},
46
+ prometheus_port=9091,
47
+ config_version=1,
48
+ )
@@ -0,0 +1,255 @@
1
+ """Typed schema for optimize.json.
2
+
3
+ All fields have a default — partial JSON is always valid (additive migration).
4
+ No imports from optimize.storage (no circular dependency).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class TTLConfig:
18
+ """TTL settings for each cache tier, in seconds."""
19
+
20
+ exact_seconds: int = 86400
21
+ semantic_seconds: int = 3600
22
+ ccr_seconds: int = 604800
23
+ sweep_interval_seconds: int = 3600
24
+
25
+ def validate(self) -> None:
26
+ for fname in ("exact_seconds", "semantic_seconds", "ccr_seconds",
27
+ "sweep_interval_seconds"):
28
+ v = getattr(self, fname)
29
+ if not isinstance(v, int) or v <= 0:
30
+ raise ValueError(
31
+ f"TTLConfig.{fname} must be a positive integer, got {v!r}"
32
+ )
33
+
34
+ @classmethod
35
+ def from_dict(cls, d: dict[str, Any]) -> "TTLConfig":
36
+ known = {f for f in cls.__dataclass_fields__}
37
+ return cls(**{k: v for k, v in d.items() if k in known})
38
+
39
+
40
+ @dataclass
41
+ class ProviderConfig:
42
+ """Per-provider settings."""
43
+
44
+ enabled: bool = True
45
+ base_url: str = ""
46
+
47
+ @classmethod
48
+ def from_dict(cls, d: dict[str, Any]) -> "ProviderConfig":
49
+ return cls(
50
+ enabled=bool(d.get("enabled", True)),
51
+ base_url=str(d.get("base_url", "")),
52
+ )
53
+
54
+ def as_dict(self) -> dict[str, Any]:
55
+ return {"enabled": self.enabled, "base_url": self.base_url}
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class OptimizeConfig:
60
+ """Master typed schema for ~/.superlocalmemory/optimize.json.
61
+
62
+ INTERFACE-CONTRACT §2 CONFORMANCE — all fields frozen, no aliases.
63
+ """
64
+
65
+ # Master kill-switch
66
+ enabled: bool = False
67
+ proxy_enabled: bool = False
68
+
69
+ # Cache
70
+ cache_enabled: bool = True
71
+ semantic_enabled: bool = False
72
+ semantic_return_threshold: float = 0.98
73
+ semantic_verify_lo: float = 0.90
74
+ semantic_error_target: float = 0.02
75
+ semantic_explore_rate: float = 0.10
76
+ semantic_constant_time: bool = False
77
+ semantic_centroid_defense: bool = True
78
+ semantic_multiturn_guard: bool = True
79
+ semantic_ann_top_k: int = 5
80
+ semantic_boundary_init: float = 0.95
81
+ semantic_boundary_floor: float = 0.85
82
+ semantic_boundary_ceiling: float = 0.995
83
+ semantic_boundary_step: float = 0.01
84
+ semantic_max_turns_for_semantic: int = 6
85
+ semantic_context_window_turns: int = 3
86
+ semantic_centroid_distance_floor: float = 0.15
87
+ semantic_verifier_model: str = ""
88
+ semantic_pad_latency_ms: float = 0.0
89
+ semantic_centroid_min_similarity: float = 0.85
90
+
91
+ # Compress
92
+ compress_enabled: bool = True
93
+ compress_mode: str = "safe"
94
+ compress_code: bool = True
95
+ compress_json: bool = True
96
+ compress_prose: bool = False
97
+ compress_ccr: bool = True
98
+ compress_align: bool = True
99
+ compress_protect_recent: int = 4
100
+ compress_llmlingua_allow_download: bool = False
101
+
102
+ # TTL + providers + pricing
103
+ ttl_seconds: int = 86400
104
+ providers: dict = field(default_factory=dict)
105
+ pricing: dict = field(default_factory=dict)
106
+
107
+ # Metrics/savings
108
+ active_model: str | None = None
109
+ usd_to_inr_rate: float = 83.5
110
+ pricing_overrides: dict = field(default_factory=dict)
111
+
112
+ # Observability
113
+ prometheus_port: int = 9091
114
+
115
+ # TTL sub-config
116
+ ttl: "TTLConfig" = field(default_factory=lambda: TTLConfig())
117
+
118
+ # Config version
119
+ config_version: int = 1
120
+
121
+ def validate(self) -> None:
122
+ if self.compress_mode not in ("safe", "aggressive"):
123
+ raise ValueError(
124
+ f"compress_mode must be 'safe' or 'aggressive', "
125
+ f"got {self.compress_mode!r}"
126
+ )
127
+ if not (0.0 < self.semantic_return_threshold <= 1.0):
128
+ raise ValueError(
129
+ f"semantic_return_threshold must be in (0.0, 1.0], "
130
+ f"got {self.semantic_return_threshold!r}"
131
+ )
132
+ if not (0.0 < self.semantic_error_target < 1.0):
133
+ raise ValueError(
134
+ f"semantic_error_target must be in (0.0, 1.0), "
135
+ f"got {self.semantic_error_target!r}"
136
+ )
137
+ if self.semantic_pad_latency_ms > 100:
138
+ logger.warning(
139
+ "semantic_pad_latency_ms=%s may negate cache latency benefit",
140
+ self.semantic_pad_latency_ms,
141
+ )
142
+ if self.prometheus_port < 1024 or self.prometheus_port > 65535:
143
+ raise ValueError(
144
+ f"prometheus_port must be 1024-65535, got {self.prometheus_port!r}"
145
+ )
146
+ self.ttl.validate()
147
+
148
+ @classmethod
149
+ def from_dict(cls, d: dict[str, Any]) -> "OptimizeConfig":
150
+ ttl_raw = d.get("ttl", {})
151
+ ttl = TTLConfig.from_dict(ttl_raw) if isinstance(ttl_raw, dict) else TTLConfig()
152
+
153
+ providers_raw = d.get("providers", {})
154
+ providers: dict[str, ProviderConfig] = {}
155
+ if isinstance(providers_raw, dict):
156
+ for pname, pdata in providers_raw.items():
157
+ if isinstance(pdata, dict):
158
+ providers[pname] = ProviderConfig.from_dict(pdata)
159
+
160
+ return cls(
161
+ enabled=bool(d.get("enabled", False)),
162
+ proxy_enabled=bool(d.get("proxy_enabled", False)),
163
+ cache_enabled=bool(d.get("cache_enabled", True)),
164
+ semantic_enabled=bool(d.get("semantic_enabled", False)),
165
+ semantic_return_threshold=float(d.get("semantic_return_threshold", 0.98)),
166
+ semantic_verify_lo=float(d.get("semantic_verify_lo", 0.90)),
167
+ semantic_error_target=float(d.get("semantic_error_target", 0.02)),
168
+ semantic_explore_rate=float(d.get("semantic_explore_rate", 0.10)),
169
+ semantic_constant_time=bool(d.get("semantic_constant_time", False)),
170
+ semantic_centroid_defense=bool(d.get("semantic_centroid_defense", True)),
171
+ semantic_multiturn_guard=bool(d.get("semantic_multiturn_guard", True)),
172
+ semantic_ann_top_k=int(d.get("semantic_ann_top_k", 5)),
173
+ semantic_boundary_init=float(d.get("semantic_boundary_init", 0.95)),
174
+ semantic_boundary_floor=float(d.get("semantic_boundary_floor", 0.85)),
175
+ semantic_boundary_ceiling=float(d.get("semantic_boundary_ceiling", 0.995)),
176
+ semantic_boundary_step=float(d.get("semantic_boundary_step", 0.01)),
177
+ semantic_max_turns_for_semantic=int(d.get("semantic_max_turns_for_semantic", 6)),
178
+ semantic_context_window_turns=int(d.get("semantic_context_window_turns", 3)),
179
+ semantic_centroid_distance_floor=float(d.get("semantic_centroid_distance_floor", 0.15)),
180
+ semantic_verifier_model=str(d.get("semantic_verifier_model", "")),
181
+ semantic_pad_latency_ms=float(d.get("semantic_pad_latency_ms", 0.0)),
182
+ semantic_centroid_min_similarity=float(
183
+ d.get("semantic_centroid_min_similarity", 0.85)
184
+ ),
185
+ compress_enabled=bool(d.get("compress_enabled", True)),
186
+ compress_mode=str(d.get("compress_mode", "safe")),
187
+ compress_code=bool(d.get("compress_code", True)),
188
+ compress_json=bool(d.get("compress_json", True)),
189
+ compress_prose=bool(d.get("compress_prose", False)),
190
+ compress_ccr=bool(d.get("compress_ccr", True)),
191
+ compress_align=bool(d.get("compress_align", True)),
192
+ compress_protect_recent=int(d.get("compress_protect_recent", 4)),
193
+ compress_llmlingua_allow_download=bool(
194
+ d.get("compress_llmlingua_allow_download", False)
195
+ ),
196
+ ttl_seconds=int(d.get("ttl_seconds", 86400)),
197
+ providers=providers,
198
+ pricing=dict(d.get("pricing", {})),
199
+ active_model=str(d.get("active_model", None)) if d.get("active_model") else None,
200
+ usd_to_inr_rate=float(d.get("usd_to_inr_rate", 83.5)),
201
+ pricing_overrides=dict(d.get("pricing_overrides", {})),
202
+ prometheus_port=int(d.get("prometheus_port", 9091)),
203
+ ttl=ttl,
204
+ config_version=int(d.get("config_version", 1)),
205
+ )
206
+
207
+ def as_dict(self) -> dict[str, Any]:
208
+ return {
209
+ "enabled": self.enabled,
210
+ "proxy_enabled": self.proxy_enabled,
211
+ "cache_enabled": self.cache_enabled,
212
+ "semantic_enabled": self.semantic_enabled,
213
+ "semantic_return_threshold": self.semantic_return_threshold,
214
+ "semantic_verify_lo": self.semantic_verify_lo,
215
+ "semantic_error_target": self.semantic_error_target,
216
+ "semantic_explore_rate": self.semantic_explore_rate,
217
+ "semantic_constant_time": self.semantic_constant_time,
218
+ "semantic_centroid_defense": self.semantic_centroid_defense,
219
+ "semantic_multiturn_guard": self.semantic_multiturn_guard,
220
+ "semantic_ann_top_k": self.semantic_ann_top_k,
221
+ "semantic_boundary_init": self.semantic_boundary_init,
222
+ "semantic_boundary_floor": self.semantic_boundary_floor,
223
+ "semantic_boundary_ceiling": self.semantic_boundary_ceiling,
224
+ "semantic_boundary_step": self.semantic_boundary_step,
225
+ "semantic_max_turns_for_semantic": self.semantic_max_turns_for_semantic,
226
+ "semantic_context_window_turns": self.semantic_context_window_turns,
227
+ "semantic_centroid_distance_floor": self.semantic_centroid_distance_floor,
228
+ "semantic_verifier_model": self.semantic_verifier_model,
229
+ "semantic_pad_latency_ms": self.semantic_pad_latency_ms,
230
+ "semantic_centroid_min_similarity": self.semantic_centroid_min_similarity,
231
+ "compress_enabled": self.compress_enabled,
232
+ "compress_mode": self.compress_mode,
233
+ "compress_code": self.compress_code,
234
+ "compress_json": self.compress_json,
235
+ "compress_prose": self.compress_prose,
236
+ "compress_ccr": self.compress_ccr,
237
+ "compress_align": self.compress_align,
238
+ "compress_protect_recent": self.compress_protect_recent,
239
+ "compress_llmlingua_allow_download": self.compress_llmlingua_allow_download,
240
+ "ttl_seconds": self.ttl_seconds,
241
+ "providers": {k: v.as_dict() for k, v in self.providers.items()},
242
+ "pricing": self.pricing,
243
+ "active_model": self.active_model,
244
+ "usd_to_inr_rate": self.usd_to_inr_rate,
245
+ "pricing_overrides": self.pricing_overrides,
246
+ "prometheus_port": self.prometheus_port,
247
+ "ttl": {
248
+ "exact_seconds": self.ttl.exact_seconds,
249
+ "semantic_seconds": self.ttl.semantic_seconds,
250
+ "ccr_seconds": self.ttl.ccr_seconds,
251
+ "sweep_interval_seconds": self.ttl.sweep_interval_seconds,
252
+ },
253
+ "config_version": self.config_version,
254
+ }
255
+
@@ -0,0 +1,209 @@
1
+ """ConfigStore — reads, writes, and hot-reloads optimize.json.
2
+
3
+ SINGLE SOURCE OF TRUTH for the daemon, UI, CLI, and MCP tools.
4
+ UI and CLI write via save(); daemon reads via get() which is always current.
5
+
6
+ Hot-reload: a background watchdog thread polls optimize.json every 2 seconds.
7
+ On mtime change, it parses and validates; if valid, atomically swaps the
8
+ active config. If invalid, keeps the old config (fail-open).
9
+
10
+ Thread safety: self._lock (threading.RLock) protects self._current_config
11
+ and self._version. get() acquires a read-lock; save() acquires a write-lock.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import os
19
+ import threading
20
+ import time
21
+ from pathlib import Path
22
+ from typing import Any, Callable
23
+
24
+ from superlocalmemory.optimize.config.schema import OptimizeConfig
25
+ from superlocalmemory.optimize.config.defaults import DEFAULT_OPTIMIZE_CONFIG
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ _DEFAULT_CONFIG_PATH: Path = Path.home() / ".superlocalmemory" / "optimize.json"
30
+ _POLL_INTERVAL_SECONDS: float = 2.0
31
+
32
+
33
+ class ConfigStore:
34
+ """Manages optimize.json with hot-reload capability."""
35
+
36
+ def __init__(
37
+ self,
38
+ config_path: Path | None = None,
39
+ poll_interval: float = _POLL_INTERVAL_SECONDS,
40
+ ) -> None:
41
+ self._config_path = Path(config_path) if config_path else _DEFAULT_CONFIG_PATH
42
+ self._poll_interval_seconds = float(poll_interval)
43
+ self._lock = threading.RLock()
44
+ self._change_callbacks: list[Callable[[OptimizeConfig], None]] = []
45
+ self._stop_event = threading.Event()
46
+ self._watchdog_thread: threading.Thread | None = None
47
+
48
+ self._saved_by_self: bool = False
49
+ self._current_config: OptimizeConfig = DEFAULT_OPTIMIZE_CONFIG
50
+ self._last_mtime: float = 0.0
51
+ self._version: int = DEFAULT_OPTIMIZE_CONFIG.config_version
52
+
53
+ # Load initial state from disk if available.
54
+ try:
55
+ if self._config_path.exists():
56
+ self._current_config = self._load_from_disk()
57
+ try:
58
+ self._last_mtime = self._config_path.stat().st_mtime
59
+ except OSError:
60
+ self._last_mtime = 0.0
61
+ self._version = self._current_config.config_version
62
+ except (json.JSONDecodeError, ValueError, TypeError, OSError) as exc:
63
+ logger.warning(
64
+ "ConfigStore: failed to parse %s — using defaults: %s",
65
+ self._config_path, exc,
66
+ )
67
+
68
+ def get(self) -> OptimizeConfig:
69
+ """Return the current active config (thread-safe, no I/O).
70
+
71
+ INTERFACE-CONTRACT v2.2 §2 — canonical accessor.
72
+ """
73
+ with self._lock:
74
+ return self._current_config
75
+
76
+ def save(self, config: OptimizeConfig) -> None:
77
+ """Write config to optimize.json with version bump."""
78
+ config.validate()
79
+ with self._lock:
80
+ new_version = self._version + 1
81
+ new_cfg = OptimizeConfig.from_dict(
82
+ {**config.as_dict(), "config_version": new_version}
83
+ )
84
+ new_cfg.validate()
85
+ self._saved_by_self = True
86
+ try:
87
+ self._atomic_write(new_cfg.as_dict())
88
+ try:
89
+ self._last_mtime = self._config_path.stat().st_mtime
90
+ except OSError:
91
+ self._last_mtime = 0.0
92
+ self._current_config = new_cfg
93
+ self._version = new_version
94
+ finally:
95
+ self._saved_by_self = False
96
+
97
+ def start_watchdog(self) -> None:
98
+ """Start the background hot-reload watchdog thread.
99
+
100
+ Idempotent — safe to call multiple times.
101
+ """
102
+ with self._lock:
103
+ if self._watchdog_thread is not None and self._watchdog_thread.is_alive():
104
+ return
105
+ self._stop_event.clear()
106
+ self._watchdog_thread = threading.Thread(
107
+ target=self._watchdog_loop,
108
+ name="slm-optimize-config-watchdog",
109
+ daemon=True,
110
+ )
111
+ self._watchdog_thread.start()
112
+
113
+ def stop_watchdog(self) -> None:
114
+ """Signal the watchdog thread to stop and join it (timeout=5s)."""
115
+ self._stop_event.set()
116
+ t = self._watchdog_thread
117
+ if t is not None:
118
+ t.join(timeout=5.0)
119
+ with self._lock:
120
+ self._watchdog_thread = None
121
+
122
+ def version(self) -> int:
123
+ with self._lock:
124
+ return self._version
125
+
126
+ def register_change_callback(
127
+ self, callback: Callable[[OptimizeConfig], None]
128
+ ) -> None:
129
+ with self._lock:
130
+ self._change_callbacks.append(callback)
131
+
132
+ # ---- private ----
133
+
134
+ def _load_from_disk(self) -> OptimizeConfig:
135
+ raw = self._config_path.read_text(encoding="utf-8")
136
+ data = json.loads(raw)
137
+ if not isinstance(data, dict):
138
+ raise ValueError("optimize.json root must be an object")
139
+ return OptimizeConfig.from_dict(data)
140
+
141
+ def _atomic_write(self, data: dict[str, Any]) -> None:
142
+ self._config_path.parent.mkdir(parents=True, exist_ok=True)
143
+ tmp = self._config_path.with_suffix(self._config_path.suffix + ".tmp")
144
+ with open(tmp, "w", encoding="utf-8") as f:
145
+ json.dump(data, f, indent=2, sort_keys=True)
146
+ f.flush()
147
+ os.fsync(f.fileno())
148
+ os.replace(tmp, self._config_path)
149
+ try:
150
+ os.chmod(self._config_path, 0o600)
151
+ except OSError:
152
+ pass
153
+
154
+ def _watchdog_loop(self) -> None:
155
+ while not self._stop_event.is_set():
156
+ # Sleep with cancellation
157
+ if self._stop_event.wait(timeout=self._poll_interval_seconds):
158
+ return
159
+ try:
160
+ try:
161
+ mtime = self._config_path.stat().st_mtime
162
+ except FileNotFoundError:
163
+ continue
164
+ except OSError as exc:
165
+ logger.warning("ConfigStore watchdog stat error: %s", exc)
166
+ continue
167
+
168
+ with self._lock:
169
+ if mtime == self._last_mtime:
170
+ continue
171
+ if self._saved_by_self:
172
+ self._last_mtime = mtime
173
+ self._saved_by_self = False
174
+ continue
175
+
176
+ # Parse + validate OUTSIDE the lock to avoid contention.
177
+ try:
178
+ new_config = self._load_from_disk()
179
+ except (json.JSONDecodeError, ValueError, TypeError, OSError) as exc:
180
+ logger.warning(
181
+ "ConfigStore hot-reload PARSE ERROR — keeping previous config: %s",
182
+ exc,
183
+ )
184
+ continue
185
+
186
+ with self._lock:
187
+ # Re-check after lock: another save may have completed.
188
+ if mtime == self._last_mtime:
189
+ continue
190
+ self._current_config = new_config
191
+ self._last_mtime = mtime
192
+ self._version += 1
193
+ new_version = self._version
194
+
195
+ # Snapshot callbacks under lock, iterate outside.
196
+ with self._lock:
197
+ callbacks = list(self._change_callbacks)
198
+ for cb in callbacks:
199
+ try:
200
+ cb(new_config)
201
+ except Exception as exc:
202
+ logger.warning("ConfigStore callback error: %s", exc)
203
+
204
+ logger.info(
205
+ "ConfigStore hot-reloaded optimize.json (version %d)",
206
+ new_version,
207
+ )
208
+ except Exception as exc:
209
+ logger.critical("ConfigStore watchdog thread crashed: %s", exc, exc_info=True)
@@ -0,0 +1,8 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ """Metrics layer for SLM v3.6 Optimize module."""
4
+
5
+ from superlocalmemory.optimize.metrics.counters import MetricsCollector, get_metrics
6
+ from superlocalmemory.optimize.metrics.estimator import SavingsEstimator
7
+
8
+ __all__ = ["MetricsCollector", "SavingsEstimator", "get_metrics"]
@@ -0,0 +1,138 @@
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
+ #
5
+ # ATTRIBUTION: CacheMetrics design pattern adapted from:
6
+ # omnicache_ai/core/metrics.py (MIT License)
7
+
8
+ """Thread-safe atomic counters for all SLM Optimize operations."""
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ from typing import ClassVar
14
+
15
+ from superlocalmemory.optimize.storage.db import MetricsSnapshot
16
+
17
+
18
+ class MetricsCollector:
19
+ """Thread-safe atomic in-memory counters. Daemon-scoped singleton.
20
+
21
+ All increment methods acquire a threading.Lock. Each increment is
22
+ O(1) and must add < 1 µs of latency on the hot path.
23
+
24
+ Hook binding (from INTERFACE-CONTRACT §3):
25
+ on_hit(tokens_saved_input, tokens_saved_output): cache hit
26
+ on_miss(): cache miss
27
+ on_compress(bytes_original, bytes_after): compress ran
28
+ on_eviction(): entry expired/evicted
29
+ """
30
+
31
+ _instance: ClassVar[MetricsCollector | None] = None
32
+ _lock: ClassVar[threading.Lock] = threading.Lock()
33
+
34
+ def __init__(self) -> None:
35
+ self._data_lock = threading.Lock()
36
+ self._hits: int = 0
37
+ self._misses: int = 0
38
+ self._calls_skipped: int = 0
39
+ self._tokens_saved_input: int = 0
40
+ self._tokens_saved_output: int = 0
41
+ self._tokens_saved_compress: int = 0
42
+ self._evictions: int = 0
43
+ self._latency_overhead_ms_sum: float = 0.0
44
+ self._latency_samples: int = 0
45
+ self._compress_runs: int = 0
46
+ self._compress_bytes_original: int = 0
47
+ self._compress_bytes_after: int = 0
48
+ self._cache_size_bytes: int = 0
49
+ self._cache_entry_count: int = 0
50
+
51
+ @classmethod
52
+ def get_instance(cls) -> MetricsCollector:
53
+ """Return the process-global singleton. Thread-safe."""
54
+ if cls._instance is None:
55
+ with cls._lock:
56
+ if cls._instance is None:
57
+ cls._instance = cls()
58
+ return cls._instance
59
+
60
+ def on_hit(self, tokens_saved_input: int = 0, tokens_saved_output: int = 0) -> None:
61
+ """Record a cache hit with tokens saved."""
62
+ with self._data_lock:
63
+ self._hits += 1
64
+ self._tokens_saved_input += max(0, tokens_saved_input)
65
+ self._tokens_saved_output += max(0, tokens_saved_output)
66
+
67
+ def on_miss(self) -> None:
68
+ """Record a cache miss."""
69
+ with self._data_lock:
70
+ self._misses += 1
71
+
72
+ def on_compress(self, bytes_original: int, bytes_after: int) -> None:
73
+ """Record a compression run with byte counts."""
74
+ with self._data_lock:
75
+ self._compress_runs += 1
76
+ self._compress_bytes_original += max(0, bytes_original)
77
+ self._compress_bytes_after += max(0, bytes_after)
78
+ saved_tokens = max(0, bytes_original - bytes_after)
79
+ self._tokens_saved_compress += saved_tokens
80
+
81
+ def on_eviction(self) -> None:
82
+ """Record an eviction."""
83
+ with self._data_lock:
84
+ self._evictions += 1
85
+
86
+ def record_latency(self, ms: float) -> None:
87
+ """Record latency overhead in milliseconds."""
88
+ if ms < 0:
89
+ return
90
+ with self._data_lock:
91
+ self._latency_overhead_ms_sum += ms
92
+ self._latency_samples += 1
93
+
94
+ def snapshot(self, cache_size_bytes: int | None = None, cache_entry_count: int | None = None) -> MetricsSnapshot:
95
+ """Return a consistent read of all counters as a MetricsSnapshot."""
96
+ with self._data_lock:
97
+ import time
98
+ return MetricsSnapshot(
99
+ id=1,
100
+ hits=self._hits,
101
+ misses=self._misses,
102
+ calls_skipped=self._calls_skipped,
103
+ tokens_saved_input=self._tokens_saved_input,
104
+ tokens_saved_output=self._tokens_saved_output,
105
+ tokens_saved_compress=self._tokens_saved_compress,
106
+ evictions=self._evictions,
107
+ latency_overhead_ms_sum=self._latency_overhead_ms_sum,
108
+ latency_samples=self._latency_samples,
109
+ compress_runs=self._compress_runs,
110
+ compress_bytes_original=self._compress_bytes_original,
111
+ compress_bytes_after=self._compress_bytes_after,
112
+ cache_size_bytes=cache_size_bytes if cache_size_bytes is not None else self._cache_size_bytes,
113
+ cache_entry_count=cache_entry_count if cache_entry_count is not None else self._cache_entry_count,
114
+ updated_at=time.time(),
115
+ )
116
+
117
+ def reset(self) -> None:
118
+ """Reset all counters. FOR TESTS ONLY."""
119
+ with self._data_lock:
120
+ self._hits = 0
121
+ self._misses = 0
122
+ self._calls_skipped = 0
123
+ self._tokens_saved_input = 0
124
+ self._tokens_saved_output = 0
125
+ self._tokens_saved_compress = 0
126
+ self._evictions = 0
127
+ self._latency_overhead_ms_sum = 0.0
128
+ self._latency_samples = 0
129
+ self._compress_runs = 0
130
+ self._compress_bytes_original = 0
131
+ self._compress_bytes_after = 0
132
+ self._cache_size_bytes = 0
133
+ self._cache_entry_count = 0
134
+
135
+
136
+ def get_metrics() -> MetricsCollector:
137
+ """Return the process-global MetricsCollector singleton."""
138
+ return MetricsCollector.get_instance()