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.
- package/ATTRIBUTION.md +24 -0
- package/CHANGELOG.md +35 -0
- package/README.md +142 -35
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/cache_cmd.py +198 -0
- package/src/superlocalmemory/cli/commands.py +80 -2
- package/src/superlocalmemory/cli/compress_cmd.py +179 -0
- package/src/superlocalmemory/cli/help_cmd.py +197 -0
- package/src/superlocalmemory/cli/main.py +122 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +175 -0
- package/src/superlocalmemory/cli/optimize_constants.py +31 -0
- package/src/superlocalmemory/cli/proxy_cmd.py +95 -0
- package/src/superlocalmemory/core/config.py +5 -0
- package/src/superlocalmemory/core/engine.py +23 -0
- package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
- package/src/superlocalmemory/llm/backbone.py +10 -4
- package/src/superlocalmemory/mcp/server.py +34 -0
- package/src/superlocalmemory/mcp/tools_v3.py +6 -2
- package/src/superlocalmemory/optimize/NOTICE +11 -0
- package/src/superlocalmemory/optimize/__init__.py +0 -0
- package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
- package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
- package/src/superlocalmemory/optimize/adapters/wrap.py +188 -0
- package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
- package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
- package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
- package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
- package/src/superlocalmemory/optimize/cache/exact.py +85 -0
- package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
- package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
- package/src/superlocalmemory/optimize/cache/manager.py +452 -0
- package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
- package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
- package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
- package/src/superlocalmemory/optimize/compress/align.py +153 -0
- package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
- package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
- package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
- package/src/superlocalmemory/optimize/compress/router.py +548 -0
- package/src/superlocalmemory/optimize/config/__init__.py +35 -0
- package/src/superlocalmemory/optimize/config/defaults.py +48 -0
- package/src/superlocalmemory/optimize/config/schema.py +255 -0
- package/src/superlocalmemory/optimize/config/store.py +209 -0
- package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
- package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
- package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
- package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
- package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
- package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
- package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
- package/src/superlocalmemory/optimize/proxy/server.py +151 -0
- package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
- package/src/superlocalmemory/optimize/storage/db.py +1016 -0
- package/src/superlocalmemory/optimize/storage/schema.py +184 -0
- package/src/superlocalmemory/server/routes/optimize.py +166 -0
- package/src/superlocalmemory/server/routes/v3_api.py +63 -1
- package/src/superlocalmemory/server/unified_daemon.py +105 -0
- package/src/superlocalmemory/ui/index.html +98 -0
- package/src/superlocalmemory/ui/js/ng-shell.js +2 -1
- package/src/superlocalmemory/ui/js/optimize.js +173 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +2 -1
- package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
- 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()
|