superlocalmemory 3.6.8 → 3.6.10
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/CHANGELOG.md +83 -0
- package/README.md +8 -4
- package/package.json +1 -1
- package/pyproject.toml +6 -1
- package/src/superlocalmemory/__init__.py +6 -2
- package/src/superlocalmemory/cli/compress_cmd.py +32 -70
- package/src/superlocalmemory/cli/daemon.py +25 -3
- package/src/superlocalmemory/cli/optimize_cmd.py +1 -3
- package/src/superlocalmemory/cli/setup_wizard.py +49 -0
- package/src/superlocalmemory/core/config.py +28 -0
- package/src/superlocalmemory/core/engine.py +15 -4
- package/src/superlocalmemory/core/health_monitor.py +32 -9
- package/src/superlocalmemory/mcp/agent_context.py +111 -0
- package/src/superlocalmemory/mcp/tools_active.py +47 -12
- package/src/superlocalmemory/mcp/tools_core.py +22 -2
- package/src/superlocalmemory/mcp/tools_mesh.py +37 -38
- package/src/superlocalmemory/optimize/cache/boundary_store.py +23 -9
- package/src/superlocalmemory/optimize/cache/exact.py +7 -4
- package/src/superlocalmemory/optimize/cache/key_builder.py +13 -0
- package/src/superlocalmemory/optimize/cache/manager.py +70 -8
- package/src/superlocalmemory/optimize/cache/semantic.py +10 -5
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +1 -7
- package/src/superlocalmemory/optimize/compress/router.py +82 -87
- package/src/superlocalmemory/optimize/config/__init__.py +16 -0
- package/src/superlocalmemory/optimize/config/defaults.py +1 -6
- package/src/superlocalmemory/optimize/config/schema.py +2 -19
- package/src/superlocalmemory/optimize/config/store.py +15 -1
- package/src/superlocalmemory/optimize/metrics/counters.py +15 -7
- package/src/superlocalmemory/optimize/proxy/_helpers.py +100 -2
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +12 -0
- package/src/superlocalmemory/optimize/proxy/capture.py +243 -0
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +31 -0
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +12 -0
- package/src/superlocalmemory/optimize/proxy/server.py +29 -0
- package/src/superlocalmemory/optimize/storage/db.py +78 -11
- package/src/superlocalmemory/optimize/storage/schema.py +11 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +8 -3
- package/src/superlocalmemory/server/routes/optimize.py +6 -8
- package/src/superlocalmemory/server/unified_daemon.py +68 -11
- package/src/superlocalmemory/ui/index.html +18 -14
- package/src/superlocalmemory/ui/js/auto-settings.js +3 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +3 -0
- package/src/superlocalmemory/ui/js/optimize.js +9 -9
- package/src/superlocalmemory.egg-info/PKG-INFO +10 -5
- package/src/superlocalmemory.egg-info/SOURCES.txt +2 -2
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/src/superlocalmemory/optimize/compress/extractive_code.py +0 -311
- package/src/superlocalmemory/optimize/compress/extractive_json.py +0 -72
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib as _hashlib
|
|
6
|
+
import json as _json_mod
|
|
5
7
|
import logging
|
|
6
8
|
import threading
|
|
7
9
|
import time
|
|
8
10
|
from abc import ABC, abstractmethod
|
|
9
11
|
from typing import Any, Callable
|
|
10
12
|
|
|
13
|
+
# C-08: precomputed hash for the common "default" tenant — avoids sha256 on every request
|
|
14
|
+
_DEFAULT_TENANT_HASH: str = _hashlib.sha256(b"default").hexdigest()
|
|
15
|
+
|
|
11
16
|
from superlocalmemory.optimize.cache.exact import ExactCache
|
|
12
17
|
from superlocalmemory.optimize.cache.invalidation import InvalidationEngine
|
|
13
18
|
from superlocalmemory.optimize.cache.key_builder import CacheConfig, KeyBuilder
|
|
@@ -230,6 +235,26 @@ class CacheManager:
|
|
|
230
235
|
ttl_seconds=0,
|
|
231
236
|
)
|
|
232
237
|
self._metrics.exact_misses += 1
|
|
238
|
+
|
|
239
|
+
# C-01: semantic fallback on exact miss (only when explicitly enabled)
|
|
240
|
+
if self._semantic.is_enabled():
|
|
241
|
+
try:
|
|
242
|
+
sem_result = self._semantic.lookup(req, tenant_id, None)
|
|
243
|
+
if sem_result is not None:
|
|
244
|
+
self._metrics.semantic_hits += 1
|
|
245
|
+
if isinstance(sem_result, CachedResponse):
|
|
246
|
+
return sem_result
|
|
247
|
+
# VCacheSemantic may return a response dict — wrap it
|
|
248
|
+
return CachedResponse(
|
|
249
|
+
hit=True,
|
|
250
|
+
data=_json_mod.dumps(sem_result, ensure_ascii=False).encode(),
|
|
251
|
+
cache_key=key,
|
|
252
|
+
ttl_seconds=0,
|
|
253
|
+
)
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
logger.warning("SemanticTier.lookup raised (fail-open): %s", exc)
|
|
256
|
+
self._metrics.semantic_misses += 1
|
|
257
|
+
|
|
233
258
|
# Return miss WITH the key so callers can use it for cache storage.
|
|
234
259
|
return CachedResponse(hit=False, data=None, cache_key=key, ttl_seconds=0)
|
|
235
260
|
|
|
@@ -258,6 +283,13 @@ class CacheManager:
|
|
|
258
283
|
self._invalidation.register(key, tenant_id, tags)
|
|
259
284
|
self._metrics.sets += 1
|
|
260
285
|
|
|
286
|
+
# C-02: index in semantic tier after exact write (fail-open)
|
|
287
|
+
if self._semantic.is_enabled():
|
|
288
|
+
try:
|
|
289
|
+
self._semantic.index_entry(req, tenant_id, None, response_dict)
|
|
290
|
+
except Exception as exc:
|
|
291
|
+
logger.warning("SemanticTier.index_entry raised (fail-open): %s", exc)
|
|
292
|
+
|
|
261
293
|
# ---- CacheHook protocol implementation (INTERFACE-CONTRACT §3) ----
|
|
262
294
|
|
|
263
295
|
def check(self, req: ProxyRequest) -> "CachedResponse | None":
|
|
@@ -269,7 +301,7 @@ class CacheManager:
|
|
|
269
301
|
cache-miss result.
|
|
270
302
|
"""
|
|
271
303
|
try:
|
|
272
|
-
result = self.get(req, tenant_id=
|
|
304
|
+
result = self.get(req, tenant_id=_DEFAULT_TENANT_HASH)
|
|
273
305
|
if result is not None and not result.hit:
|
|
274
306
|
MetricsCollector.get_instance().on_miss()
|
|
275
307
|
return result
|
|
@@ -280,21 +312,51 @@ class CacheManager:
|
|
|
280
312
|
def store(self, req: ProxyRequest, resp: ProviderResponse) -> None:
|
|
281
313
|
"""CacheHook.store() — persist response; fail-open on error."""
|
|
282
314
|
try:
|
|
283
|
-
self.set(req, resp, tenant_id=
|
|
315
|
+
self.set(req, resp, tenant_id=_DEFAULT_TENANT_HASH)
|
|
284
316
|
except Exception as exc:
|
|
285
317
|
logger.warning("CacheManager.store raised (fail-open): %s", exc)
|
|
286
318
|
|
|
287
319
|
def on_hit(self, req: ProxyRequest, resp: bytes, tokens_saved: int) -> None:
|
|
288
320
|
"""CacheHook.on_hit() — forward token savings to MetricsCollector.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
321
|
+
|
|
322
|
+
M-01: compute input tokens from request body when caller passes 0.
|
|
323
|
+
M-02: parse real output tokens from cached response usage field.
|
|
324
|
+
All counts are estimates for display — not billing-accurate.
|
|
293
325
|
"""
|
|
326
|
+
import json as _json
|
|
327
|
+
|
|
328
|
+
# M-01: estimate input tokens from message content
|
|
329
|
+
if tokens_saved == 0 and isinstance(req, ProxyRequest):
|
|
330
|
+
try:
|
|
331
|
+
body = req.body or {}
|
|
332
|
+
total_chars = 0
|
|
333
|
+
for m in (body.get("messages") or []):
|
|
334
|
+
c = m.get("content", "")
|
|
335
|
+
if isinstance(c, str):
|
|
336
|
+
total_chars += len(c)
|
|
337
|
+
elif isinstance(c, list):
|
|
338
|
+
for blk in c:
|
|
339
|
+
if isinstance(blk, dict):
|
|
340
|
+
total_chars += len(blk.get("text", "") or "")
|
|
341
|
+
total_chars += len(body.get("system", "") or "")
|
|
342
|
+
tokens_saved = max(0, total_chars // 4)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# M-02: parse real output tokens from stored response
|
|
294
347
|
output_tokens = 0
|
|
295
348
|
if resp:
|
|
296
|
-
|
|
297
|
-
|
|
349
|
+
try:
|
|
350
|
+
data = _json.loads(resp)
|
|
351
|
+
usage = data.get("usage") or {}
|
|
352
|
+
output_tokens = (
|
|
353
|
+
usage.get("output_tokens")
|
|
354
|
+
or usage.get("completion_tokens")
|
|
355
|
+
or 0
|
|
356
|
+
)
|
|
357
|
+
except Exception:
|
|
358
|
+
output_tokens = len(resp) // 4 # fallback byte-estimate
|
|
359
|
+
|
|
298
360
|
MetricsCollector.get_instance().on_hit(
|
|
299
361
|
tokens_saved_input=tokens_saved,
|
|
300
362
|
tokens_saved_output=output_tokens,
|
|
@@ -294,7 +294,8 @@ class VCacheSemantic(SemanticTier):
|
|
|
294
294
|
# Step 5: vCache exploit/explore (MLE τ̂ via record_outcome updates)
|
|
295
295
|
record = self._boundary_store.get(best_entry_id)
|
|
296
296
|
delta = float(getattr(cfg, "semantic_error_target", _DEFAULT_ERROR_TARGET))
|
|
297
|
-
|
|
297
|
+
sem_return_threshold = float(getattr(cfg, "semantic_return_threshold", _DEFAULT_RETURN_THRESHOLD))
|
|
298
|
+
if record.should_explore(best_score, delta=delta, return_threshold=sem_return_threshold):
|
|
298
299
|
logger.debug(
|
|
299
300
|
"VCacheSemantic: explore (score=%.4f entry=%s t_hat=%.4f)",
|
|
300
301
|
best_score, best_entry_id, record.t_hat,
|
|
@@ -363,11 +364,11 @@ class VCacheSemantic(SemanticTier):
|
|
|
363
364
|
self._index[tenant_id] = []
|
|
364
365
|
return
|
|
365
366
|
entries: list[tuple[str, str, np.ndarray]] = []
|
|
366
|
-
for entry_id, blob in rows:
|
|
367
|
+
for entry_id, blob, ctx_fp in rows: # C-10: unpack persisted context_fp
|
|
367
368
|
try:
|
|
368
369
|
v = np.frombuffer(blob, dtype=np.float32).copy()
|
|
369
370
|
if v.shape[0] == _EMBED_DIM:
|
|
370
|
-
entries.append((entry_id,
|
|
371
|
+
entries.append((entry_id, ctx_fp, v))
|
|
371
372
|
except Exception:
|
|
372
373
|
continue
|
|
373
374
|
with self._index_lock:
|
|
@@ -422,13 +423,17 @@ class VCacheSemantic(SemanticTier):
|
|
|
422
423
|
)
|
|
423
424
|
return
|
|
424
425
|
|
|
425
|
-
# Store vector in DB
|
|
426
|
+
# Store vector in DB — C-10: persist context_fp alongside the vector
|
|
426
427
|
vec_bytes = vec.tobytes()
|
|
427
428
|
self._db.vec_add(
|
|
428
429
|
entry_id=entry_id,
|
|
429
430
|
tenant_id=tenant_id,
|
|
430
431
|
vector=vec_bytes,
|
|
431
|
-
meta={
|
|
432
|
+
meta={
|
|
433
|
+
"model": "nomic-ai/nomic-embed-text-v1.5",
|
|
434
|
+
"dim": _EMBED_DIM,
|
|
435
|
+
"context_fp": context_fp,
|
|
436
|
+
},
|
|
432
437
|
)
|
|
433
438
|
|
|
434
439
|
# Initialize boundary record if new
|
|
@@ -32,16 +32,10 @@ class LLMLinguaCompressor:
|
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
34
34
|
self,
|
|
35
|
-
model_name: str =
|
|
35
|
+
model_name: str = _MODEL_XLM,
|
|
36
36
|
device_map: str = "cpu",
|
|
37
37
|
rate: float = _DEFAULT_RATE,
|
|
38
38
|
) -> None:
|
|
39
|
-
import os
|
|
40
|
-
if os.environ.get("SLM_DISABLE_HF_DOWNLOAD", "0") == "1":
|
|
41
|
-
raise ImportError(
|
|
42
|
-
"LLMLingua-2 model download blocked: SLM_DISABLE_HF_DOWNLOAD=1. "
|
|
43
|
-
"Set compress_llmlingua_allow_download=true in optimize.json."
|
|
44
|
-
)
|
|
45
39
|
try:
|
|
46
40
|
from llmlingua import PromptCompressor # type: ignore[import]
|
|
47
41
|
except ImportError as e:
|
|
@@ -26,7 +26,6 @@ from superlocalmemory.optimize.config.store import ConfigStore
|
|
|
26
26
|
logger = logging.getLogger("slm.optimize.compress.router")
|
|
27
27
|
|
|
28
28
|
_MIN_CHARS_FOR_COMPRESSION: int = 500
|
|
29
|
-
_MIN_RATIO_STRUCTURED: float = 0.60
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
class CompressRouter:
|
|
@@ -50,8 +49,6 @@ class CompressRouter:
|
|
|
50
49
|
return cls._instance
|
|
51
50
|
|
|
52
51
|
def __init__(self) -> None:
|
|
53
|
-
self._json_compressor: "JSONCompressor | None" = None
|
|
54
|
-
self._code_compressor: "CodeCompressor | None" = None
|
|
55
52
|
self._llmlingua_compressor: "LLMLinguaCompressor | None" = None
|
|
56
53
|
self._ccr_store: "CCRStore | None" = None
|
|
57
54
|
self._aligner: "CacheAligner | None" = None
|
|
@@ -103,12 +100,17 @@ class CompressRouter:
|
|
|
103
100
|
tenant_id="default",
|
|
104
101
|
)
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
# S-01/Stage-9 fix: build new_bytes BEFORE the improvement guard.
|
|
104
|
+
# Layer 1 normalization saves characters (not word-count tokens), so
|
|
105
|
+
# tokens_after == tokens_before for "normalize" strategy. Checking byte
|
|
106
|
+
# length of the serialized body correctly detects Layer 1 savings.
|
|
109
107
|
body["messages"] = new_messages
|
|
110
108
|
new_bytes = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode()
|
|
111
109
|
|
|
110
|
+
bytes_saved = len(req.body_bytes) - len(new_bytes)
|
|
111
|
+
if bytes_saved <= 0 and tokens_after >= tokens_before:
|
|
112
|
+
return req # neither bytes nor tokens improved
|
|
113
|
+
|
|
112
114
|
# CONTRACT §3: fire on_compress metrics callback
|
|
113
115
|
lossy = strategy == "llmlingua2_prose"
|
|
114
116
|
self.on_compress(tokens_before, tokens_after, lossy)
|
|
@@ -160,7 +162,12 @@ class CompressRouter:
|
|
|
160
162
|
primary_strategy = "none"
|
|
161
163
|
new_messages: list[dict[str, Any]] = []
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
# K-05: protect last N *user* turns, not last N messages of any role
|
|
166
|
+
user_indices = [i for i, m in enumerate(messages) if m.get("role") == "user"]
|
|
167
|
+
protect_indices: set[int] = set(user_indices[-protect_recent:]) if protect_recent > 0 else set()
|
|
168
|
+
# Always protect the very last message (current turn, any role)
|
|
169
|
+
if messages:
|
|
170
|
+
protect_indices.add(len(messages) - 1)
|
|
164
171
|
|
|
165
172
|
for idx, msg in enumerate(messages):
|
|
166
173
|
role = msg.get("role", "")
|
|
@@ -248,96 +255,77 @@ class CompressRouter:
|
|
|
248
255
|
) -> tuple[str, int, int, str]:
|
|
249
256
|
tokens_before = _token_estimate(text)
|
|
250
257
|
|
|
251
|
-
|
|
258
|
+
if len(text) < _MIN_CHARS_FOR_COMPRESSION:
|
|
259
|
+
return text, tokens_before, tokens_before, "none"
|
|
260
|
+
|
|
261
|
+
# K-01/K-02/K-03: NEVER compress structured content (JSON or code)
|
|
252
262
|
stripped = text.strip()
|
|
253
263
|
if stripped.startswith(("{", "[")):
|
|
264
|
+
# PERF-02: for large content, structural bracket-match avoids O(n) json.loads().
|
|
265
|
+
# Conservative: matching outer brackets → treat as JSON and skip compression.
|
|
266
|
+
# K-01 mandate is safety-first: false-positive (non-JSON treated as JSON) is
|
|
267
|
+
# safe; false-negative (JSON compressed) would be a correctness violation.
|
|
268
|
+
_last = stripped[-1] if stripped else ""
|
|
269
|
+
if len(stripped) > 8192 and (
|
|
270
|
+
(stripped[0] == "{" and _last == "}") or (stripped[0] == "[" and _last == "]")
|
|
271
|
+
):
|
|
272
|
+
return text, tokens_before, tokens_before, "none" # large JSON → passthrough
|
|
254
273
|
try:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
obj["__slm_ccr__"] = ccr_id
|
|
269
|
-
compressed = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
270
|
-
elif isinstance(obj, list):
|
|
271
|
-
# RB-02: list-root embedding
|
|
272
|
-
wrapper = {"__slm_ccr__": ccr_id, "__slm_data__": obj}
|
|
273
|
-
compressed = json.dumps(wrapper, ensure_ascii=False, separators=(",", ":"))
|
|
274
|
-
except Exception:
|
|
275
|
-
pass
|
|
276
|
-
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
277
|
-
logger.debug("[%s] JSON compressed %.2f ratio ccr_id=%s", request_id, ratio, ccr_id)
|
|
278
|
-
return compressed, tokens_before_adj, tokens_after, "extractive_json"
|
|
279
|
-
return text, tokens_before, tokens_before, "none"
|
|
280
|
-
except (json.JSONDecodeError, Exception):
|
|
281
|
-
pass
|
|
282
|
-
|
|
283
|
-
# Code detection
|
|
284
|
-
lang = _detect_language(text)
|
|
285
|
-
if lang is not None:
|
|
286
|
-
compressor = self._get_code_compressor()
|
|
287
|
-
# RB-03: compress first, compute ratio, then store CCR only if beneficial
|
|
288
|
-
compressed_probe = compressor.compress(text, language=lang, ccr_id="")
|
|
289
|
-
tokens_after = _token_estimate(compressed_probe)
|
|
290
|
-
ratio = tokens_after / tokens_before if tokens_before else 1.0
|
|
291
|
-
if ratio < _MIN_RATIO_STRUCTURED:
|
|
292
|
-
# B-03: store-before-compress
|
|
293
|
-
ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
|
|
294
|
-
compressed = compressor.compress(text, language=lang, ccr_id=ccr_id)
|
|
295
|
-
if ccr_id:
|
|
296
|
-
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
297
|
-
logger.debug("[%s] Code compressed lang=%s ratio=%.2f ccr_id=%s",
|
|
298
|
-
request_id, lang, ratio, ccr_id)
|
|
299
|
-
return compressed, tokens_before, tokens_after, "extractive_code"
|
|
300
|
-
return text, tokens_before, tokens_before, "none"
|
|
274
|
+
json.loads(stripped)
|
|
275
|
+
return text, tokens_before, tokens_before, "none" # valid JSON → passthrough
|
|
276
|
+
except json.JSONDecodeError:
|
|
277
|
+
pass # not valid JSON — treat as prose
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
logger.warning("compress: unexpected error probing JSON content: %s", exc)
|
|
280
|
+
|
|
281
|
+
if _detect_language(text) is not None:
|
|
282
|
+
return text, tokens_before, tokens_before, "none" # code → passthrough
|
|
283
|
+
|
|
284
|
+
# Layer 1 — lossless whitespace normalization (always-on, safe)
|
|
285
|
+
normalized = self._normalize_whitespace(text)
|
|
286
|
+
tokens_after_l1 = _token_estimate(normalized)
|
|
301
287
|
|
|
302
|
-
#
|
|
303
|
-
# (Phase 3 — opt-in prose tier; off by default; gated by config
|
|
304
|
-
# field compress_prose added in LLD-04 INTERFACE-CONTRACT v2.)
|
|
288
|
+
# Layer 2 — LLMLingua-2 prose compression (aggressive + opt-in only)
|
|
305
289
|
cfg = self._get_config()
|
|
306
290
|
prose_enabled = bool(getattr(cfg, "compress_prose", False))
|
|
307
|
-
if aggressive and prose_enabled:
|
|
291
|
+
if aggressive and prose_enabled: # pragma: no cover — LLMLingua optional dep
|
|
308
292
|
compressor = self._get_llmlingua_compressor()
|
|
309
293
|
if compressor is not None:
|
|
310
|
-
# B-03: store
|
|
294
|
+
# B-03: store original BEFORE lossy compression
|
|
311
295
|
ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
|
|
312
|
-
compressed = compressor.compress(
|
|
296
|
+
compressed = compressor.compress(normalized)
|
|
313
297
|
if ccr_id:
|
|
314
298
|
self._ccr_update_compressed(ccr_id, compressed.encode())
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
299
|
+
tokens_after_l2 = _token_estimate(compressed)
|
|
300
|
+
if tokens_after_l2 < tokens_before:
|
|
301
|
+
logger.info(
|
|
302
|
+
"[%s] LLMLingua-2 prose compressed rate=%.2f ccr_id=%s (LOSSY)",
|
|
303
|
+
request_id,
|
|
304
|
+
tokens_after_l2 / tokens_before if tokens_before else 1.0,
|
|
305
|
+
ccr_id,
|
|
306
|
+
)
|
|
307
|
+
return compressed, tokens_before, tokens_after_l2, "llmlingua2_prose"
|
|
323
308
|
|
|
324
|
-
|
|
309
|
+
# S-01 fix: compare character length, not word count.
|
|
310
|
+
# _token_estimate() is word-count — whitespace normalization saves characters/bytes
|
|
311
|
+
# but never removes words, so token counts are identical before and after Layer 1.
|
|
312
|
+
# Character comparison correctly detects when normalization reduced the body size.
|
|
313
|
+
if len(normalized) < len(text):
|
|
314
|
+
return normalized, tokens_before, tokens_after_l1, "normalize"
|
|
325
315
|
|
|
326
|
-
|
|
316
|
+
return text, tokens_before, tokens_before, "none"
|
|
327
317
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _normalize_whitespace(text: str) -> str:
|
|
320
|
+
"""Layer 1 lossless: collapse excess blank lines, strip trailing spaces per line."""
|
|
321
|
+
import re
|
|
322
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
323
|
+
lines = [line.rstrip() for line in text.split("\n")]
|
|
324
|
+
return "\n".join(lines)
|
|
333
325
|
|
|
334
|
-
|
|
335
|
-
if self._code_compressor is None:
|
|
336
|
-
from superlocalmemory.optimize.compress.extractive_code import CodeCompressor
|
|
337
|
-
self._code_compressor = CodeCompressor()
|
|
338
|
-
return self._code_compressor
|
|
326
|
+
# ── Lazy loaders ─────────────────────────────────────────────────────
|
|
339
327
|
|
|
340
|
-
def _get_llmlingua_compressor(self) -> "LLMLinguaCompressor | None":
|
|
328
|
+
def _get_llmlingua_compressor(self) -> "LLMLinguaCompressor | None": # pragma: no cover
|
|
341
329
|
if self._llmlingua_compressor is None:
|
|
342
330
|
try:
|
|
343
331
|
from superlocalmemory.optimize.compress.prose_llmlingua import LLMLinguaCompressor
|
|
@@ -396,21 +384,32 @@ class CompressRouter:
|
|
|
396
384
|
return CompressTextResult(
|
|
397
385
|
compressed_text=compressed, strategy=strat,
|
|
398
386
|
tokens_before=tb, tokens_after=ta,
|
|
387
|
+
lossy=strat == "llmlingua2_prose",
|
|
399
388
|
)
|
|
400
389
|
except Exception as exc:
|
|
401
390
|
logger.debug("compress_text failed (non-fatal): %s", exc)
|
|
391
|
+
t = _token_estimate(text)
|
|
402
392
|
return CompressTextResult(
|
|
403
393
|
compressed_text=text, strategy="none",
|
|
404
|
-
tokens_before=
|
|
394
|
+
tokens_before=t, tokens_after=t,
|
|
395
|
+
lossy=False,
|
|
405
396
|
)
|
|
406
397
|
|
|
407
398
|
|
|
408
399
|
@dataclass
|
|
409
400
|
class CompressTextResult:
|
|
401
|
+
"""Result of a compress_text() call.
|
|
402
|
+
|
|
403
|
+
UX-02 note: lossy=True only when strategy="llmlingua2_prose" (Layer 2).
|
|
404
|
+
In the default install (LLMLingua optional dep not installed), lossy is
|
|
405
|
+
always False — install `llmlingua>=0.2.0` and set compress_prose=True +
|
|
406
|
+
compress_mode="aggressive" to activate lossy compression.
|
|
407
|
+
"""
|
|
410
408
|
compressed_text: str
|
|
411
|
-
strategy: str # "
|
|
409
|
+
strategy: str # "normalize" | "llmlingua2_prose" | "none"
|
|
412
410
|
tokens_before: int
|
|
413
411
|
tokens_after: int
|
|
412
|
+
lossy: bool = False # K-10: True only for llmlingua2_prose (Layer 2)
|
|
414
413
|
|
|
415
414
|
|
|
416
415
|
# ── Module-level helpers ──────────────────────────────────────────────────────
|
|
@@ -419,10 +418,6 @@ def _token_estimate(text: str) -> int:
|
|
|
419
418
|
return len(text.split()) if text else 0
|
|
420
419
|
|
|
421
420
|
|
|
422
|
-
def _token_estimate_structured(text: str) -> int:
|
|
423
|
-
return max(1, len(text) // 4) if text else 0
|
|
424
|
-
|
|
425
|
-
|
|
426
421
|
def _msg_has_tool_result(msg: dict) -> bool:
|
|
427
422
|
"""B-09: Detect historical tool_result blocks in messages."""
|
|
428
423
|
content = msg.get("content", "")
|
|
@@ -24,6 +24,22 @@ def load_optimize_config() -> OptimizeConfig:
|
|
|
24
24
|
return get_optimize_config()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def get_shared_store() -> "ConfigStore":
|
|
28
|
+
"""Return the process-wide ConfigStore singleton, creating it on first use.
|
|
29
|
+
|
|
30
|
+
This is the SINGLE instance the daemon, the /api/optimize routes, the proxy
|
|
31
|
+
hook-reload callback, and the hot-reload watchdog must all share so that a UI
|
|
32
|
+
config change reaches the live proxy (fixes W-02 hot-reload + W-05 fresh-store
|
|
33
|
+
-per-request). The daemon calls this at startup, registers a change callback,
|
|
34
|
+
and starts the watchdog.
|
|
35
|
+
"""
|
|
36
|
+
global _store
|
|
37
|
+
if _store is None:
|
|
38
|
+
from superlocalmemory.optimize.config.store import ConfigStore
|
|
39
|
+
_store = ConfigStore()
|
|
40
|
+
return _store
|
|
41
|
+
|
|
42
|
+
|
|
27
43
|
def _set_config_store(store: "ConfigStore") -> None:
|
|
28
44
|
global _store
|
|
29
45
|
_store = store
|
|
@@ -23,15 +23,10 @@ DEFAULT_OPTIMIZE_CONFIG = OptimizeConfig(
|
|
|
23
23
|
semantic_boundary_floor=0.85,
|
|
24
24
|
semantic_pad_latency_ms=0.0,
|
|
25
25
|
semantic_centroid_min_similarity=0.85,
|
|
26
|
-
compress_enabled=
|
|
26
|
+
compress_enabled=False,
|
|
27
27
|
compress_mode="safe",
|
|
28
|
-
compress_code=True,
|
|
29
|
-
compress_json=True,
|
|
30
28
|
compress_prose=False,
|
|
31
|
-
compress_ccr=True,
|
|
32
|
-
compress_align=True,
|
|
33
29
|
compress_protect_recent=4,
|
|
34
|
-
compress_llmlingua_allow_download=False,
|
|
35
30
|
ttl_seconds=86400,
|
|
36
31
|
ttl=TTLConfig(),
|
|
37
32
|
providers={
|
|
@@ -89,15 +89,10 @@ class OptimizeConfig:
|
|
|
89
89
|
semantic_centroid_min_similarity: float = 0.85
|
|
90
90
|
|
|
91
91
|
# Compress
|
|
92
|
-
compress_enabled: bool =
|
|
92
|
+
compress_enabled: bool = False
|
|
93
93
|
compress_mode: str = "safe"
|
|
94
|
-
compress_code: bool = True
|
|
95
|
-
compress_json: bool = True
|
|
96
94
|
compress_prose: bool = False
|
|
97
|
-
compress_ccr: bool = True
|
|
98
|
-
compress_align: bool = True
|
|
99
95
|
compress_protect_recent: int = 4
|
|
100
|
-
compress_llmlingua_allow_download: bool = False
|
|
101
96
|
|
|
102
97
|
# TTL + providers + pricing
|
|
103
98
|
ttl_seconds: int = 86400
|
|
@@ -182,17 +177,10 @@ class OptimizeConfig:
|
|
|
182
177
|
semantic_centroid_min_similarity=float(
|
|
183
178
|
d.get("semantic_centroid_min_similarity", 0.85)
|
|
184
179
|
),
|
|
185
|
-
compress_enabled=bool(d.get("compress_enabled",
|
|
180
|
+
compress_enabled=bool(d.get("compress_enabled", False)),
|
|
186
181
|
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
182
|
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
183
|
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
184
|
ttl_seconds=int(d.get("ttl_seconds", 86400)),
|
|
197
185
|
providers=providers,
|
|
198
186
|
pricing=dict(d.get("pricing", {})),
|
|
@@ -230,13 +218,8 @@ class OptimizeConfig:
|
|
|
230
218
|
"semantic_centroid_min_similarity": self.semantic_centroid_min_similarity,
|
|
231
219
|
"compress_enabled": self.compress_enabled,
|
|
232
220
|
"compress_mode": self.compress_mode,
|
|
233
|
-
"compress_code": self.compress_code,
|
|
234
|
-
"compress_json": self.compress_json,
|
|
235
221
|
"compress_prose": self.compress_prose,
|
|
236
|
-
"compress_ccr": self.compress_ccr,
|
|
237
|
-
"compress_align": self.compress_align,
|
|
238
222
|
"compress_protect_recent": self.compress_protect_recent,
|
|
239
|
-
"compress_llmlingua_allow_download": self.compress_llmlingua_allow_download,
|
|
240
223
|
"ttl_seconds": self.ttl_seconds,
|
|
241
224
|
"providers": {k: v.as_dict() for k, v in self.providers.items()},
|
|
242
225
|
"pricing": self.pricing,
|
|
@@ -74,7 +74,13 @@ class ConfigStore:
|
|
|
74
74
|
return self._current_config
|
|
75
75
|
|
|
76
76
|
def save(self, config: OptimizeConfig) -> None:
|
|
77
|
-
"""Write config to optimize.json with version bump.
|
|
77
|
+
"""Write config to optimize.json with version bump.
|
|
78
|
+
|
|
79
|
+
Fires registered change callbacks immediately after a successful write
|
|
80
|
+
(outside the lock) so a UI/CLI save reaches the live proxy without
|
|
81
|
+
waiting for the 2s watchdog poll. The watchdog skips this write via
|
|
82
|
+
``_saved_by_self`` so callbacks fire exactly once.
|
|
83
|
+
"""
|
|
78
84
|
config.validate()
|
|
79
85
|
with self._lock:
|
|
80
86
|
new_version = self._version + 1
|
|
@@ -93,6 +99,14 @@ class ConfigStore:
|
|
|
93
99
|
self._version = new_version
|
|
94
100
|
finally:
|
|
95
101
|
self._saved_by_self = False
|
|
102
|
+
callbacks = list(self._change_callbacks)
|
|
103
|
+
# Fire callbacks OUTSIDE the lock — a callback may rebuild proxy hooks
|
|
104
|
+
# or call back into get(); keeping them off the lock avoids contention.
|
|
105
|
+
for cb in callbacks:
|
|
106
|
+
try:
|
|
107
|
+
cb(new_cfg)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.warning("ConfigStore save callback error: %s", exc)
|
|
96
110
|
|
|
97
111
|
def start_watchdog(self) -> None:
|
|
98
112
|
"""Start the background hot-reload watchdog thread.
|
|
@@ -24,7 +24,7 @@ class MetricsCollector:
|
|
|
24
24
|
Hook binding (from INTERFACE-CONTRACT §3):
|
|
25
25
|
on_hit(tokens_saved_input, tokens_saved_output): cache hit
|
|
26
26
|
on_miss(): cache miss
|
|
27
|
-
on_compress(
|
|
27
|
+
on_compress(tokens_before, tokens_after): compress ran (word-count proxy)
|
|
28
28
|
on_eviction(): entry expired/evicted
|
|
29
29
|
"""
|
|
30
30
|
|
|
@@ -69,20 +69,28 @@ class MetricsCollector:
|
|
|
69
69
|
with self._data_lock:
|
|
70
70
|
self._misses += 1
|
|
71
71
|
|
|
72
|
-
def on_compress(self,
|
|
73
|
-
"""Record a compression run
|
|
72
|
+
def on_compress(self, tokens_before: int, tokens_after: int) -> None:
|
|
73
|
+
"""Record a compression run. Arguments are word-count proxy estimates from _token_estimate().
|
|
74
|
+
|
|
75
|
+
M-03: consistent naming — these are token estimates, not byte counts.
|
|
76
|
+
Stored in compress_bytes_original/after fields for DB schema compat; unit is word-count.
|
|
77
|
+
"""
|
|
74
78
|
with self._data_lock:
|
|
75
79
|
self._compress_runs += 1
|
|
76
|
-
self._compress_bytes_original += max(0,
|
|
77
|
-
self._compress_bytes_after += max(0,
|
|
78
|
-
|
|
79
|
-
self._tokens_saved_compress += saved_tokens
|
|
80
|
+
self._compress_bytes_original += max(0, tokens_before)
|
|
81
|
+
self._compress_bytes_after += max(0, tokens_after)
|
|
82
|
+
self._tokens_saved_compress += max(0, tokens_before - tokens_after)
|
|
80
83
|
|
|
81
84
|
def on_eviction(self) -> None:
|
|
82
85
|
"""Record an eviction."""
|
|
83
86
|
with self._data_lock:
|
|
84
87
|
self._evictions += 1
|
|
85
88
|
|
|
89
|
+
def increment_skipped_temperature(self) -> None:
|
|
90
|
+
"""C-04: record a cache skip due to non-zero temperature."""
|
|
91
|
+
with self._data_lock:
|
|
92
|
+
self._calls_skipped += 1
|
|
93
|
+
|
|
86
94
|
def record_latency(self, ms: float) -> None:
|
|
87
95
|
"""Record latency overhead in milliseconds."""
|
|
88
96
|
if ms < 0:
|