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.
Files changed (48) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +8 -4
  3. package/package.json +1 -1
  4. package/pyproject.toml +6 -1
  5. package/src/superlocalmemory/__init__.py +6 -2
  6. package/src/superlocalmemory/cli/compress_cmd.py +32 -70
  7. package/src/superlocalmemory/cli/daemon.py +25 -3
  8. package/src/superlocalmemory/cli/optimize_cmd.py +1 -3
  9. package/src/superlocalmemory/cli/setup_wizard.py +49 -0
  10. package/src/superlocalmemory/core/config.py +28 -0
  11. package/src/superlocalmemory/core/engine.py +15 -4
  12. package/src/superlocalmemory/core/health_monitor.py +32 -9
  13. package/src/superlocalmemory/mcp/agent_context.py +111 -0
  14. package/src/superlocalmemory/mcp/tools_active.py +47 -12
  15. package/src/superlocalmemory/mcp/tools_core.py +22 -2
  16. package/src/superlocalmemory/mcp/tools_mesh.py +37 -38
  17. package/src/superlocalmemory/optimize/cache/boundary_store.py +23 -9
  18. package/src/superlocalmemory/optimize/cache/exact.py +7 -4
  19. package/src/superlocalmemory/optimize/cache/key_builder.py +13 -0
  20. package/src/superlocalmemory/optimize/cache/manager.py +70 -8
  21. package/src/superlocalmemory/optimize/cache/semantic.py +10 -5
  22. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +1 -7
  23. package/src/superlocalmemory/optimize/compress/router.py +82 -87
  24. package/src/superlocalmemory/optimize/config/__init__.py +16 -0
  25. package/src/superlocalmemory/optimize/config/defaults.py +1 -6
  26. package/src/superlocalmemory/optimize/config/schema.py +2 -19
  27. package/src/superlocalmemory/optimize/config/store.py +15 -1
  28. package/src/superlocalmemory/optimize/metrics/counters.py +15 -7
  29. package/src/superlocalmemory/optimize/proxy/_helpers.py +100 -2
  30. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +12 -0
  31. package/src/superlocalmemory/optimize/proxy/capture.py +243 -0
  32. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +31 -0
  33. package/src/superlocalmemory/optimize/proxy/openai_surface.py +12 -0
  34. package/src/superlocalmemory/optimize/proxy/server.py +29 -0
  35. package/src/superlocalmemory/optimize/storage/db.py +78 -11
  36. package/src/superlocalmemory/optimize/storage/schema.py +11 -0
  37. package/src/superlocalmemory/retrieval/spreading_activation.py +8 -3
  38. package/src/superlocalmemory/server/routes/optimize.py +6 -8
  39. package/src/superlocalmemory/server/unified_daemon.py +68 -11
  40. package/src/superlocalmemory/ui/index.html +18 -14
  41. package/src/superlocalmemory/ui/js/auto-settings.js +3 -1
  42. package/src/superlocalmemory/ui/js/ng-shell.js +3 -0
  43. package/src/superlocalmemory/ui/js/optimize.js +9 -9
  44. package/src/superlocalmemory.egg-info/PKG-INFO +10 -5
  45. package/src/superlocalmemory.egg-info/SOURCES.txt +2 -2
  46. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  47. package/src/superlocalmemory/optimize/compress/extractive_code.py +0 -311
  48. 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="default")
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="default")
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
- Contract §7: cache skip saves BOTH input+output tokens (whole call avoided).
291
- tokens_saved = input tokens saved (from request).
292
- Output tokens estimated from response body size (~4 bytes per token).
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
- # Rough estimate: 4 bytes per token for English text
297
- output_tokens = len(resp) // 4
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
- if record.should_explore(best_score, delta=delta):
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, "", v))
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={"model": "nomic-ai/nomic-embed-text-v1.5", "dim": _EMBED_DIM},
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 = _MODEL_BERT,
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
- if tokens_after >= tokens_before:
107
- return req # no improvement
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
- protect_indices = set(range(max(0, len(messages) - protect_recent), len(messages)))
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
- # JSON detection
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
- parsed = json.loads(stripped)
256
- compressor = self._get_json_compressor()
257
- compressed = compressor.compress(parsed)
258
- tokens_after = _token_estimate_structured(compressed)
259
- tokens_before_adj = _token_estimate_structured(text)
260
- ratio = tokens_after / tokens_before_adj if tokens_before_adj else 1.0
261
- if ratio < _MIN_RATIO_STRUCTURED:
262
- # B-03: store-before-compress
263
- ccr_id = self._ccr_store_original(text.encode(), model, tenant_id)
264
- if ccr_id:
265
- try:
266
- obj = json.loads(compressed)
267
- if isinstance(obj, dict):
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
- # Prose: only if aggressive mode AND compress_prose is enabled
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-before-compress
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(text)
296
+ compressed = compressor.compress(normalized)
313
297
  if ccr_id:
314
298
  self._ccr_update_compressed(ccr_id, compressed.encode())
315
- tokens_after = _token_estimate(compressed)
316
- logger.info(
317
- "[%s] LLMLingua-2 prose compressed rate=%.2f ccr_id=%s (LOSSY)",
318
- request_id,
319
- tokens_after / tokens_before if tokens_before else 1.0,
320
- ccr_id,
321
- )
322
- return compressed, tokens_before, tokens_after, "llmlingua2_prose"
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
- return text, tokens_before, tokens_before, "none"
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
- # ── Lazy loaders ─────────────────────────────────────────────────────
316
+ return text, tokens_before, tokens_before, "none"
327
317
 
328
- def _get_json_compressor(self) -> "JSONCompressor":
329
- if self._json_compressor is None:
330
- from superlocalmemory.optimize.compress.extractive_json import JSONCompressor
331
- self._json_compressor = JSONCompressor()
332
- return self._json_compressor
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
- def _get_code_compressor(self) -> "CodeCompressor":
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=len(text.split()), tokens_after=len(text.split()),
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 # "extractive_json" | "extractive_code" | "llmlingua2_prose" | "none"
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=True,
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 = True
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", True)),
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(bytes_original, bytes_after): compress ran
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, bytes_original: int, bytes_after: int) -> None:
73
- """Record a compression run with byte counts."""
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, 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
+ 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: