superlocalmemory 3.6.9 → 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 (42) hide show
  1. package/CHANGELOG.md +78 -10
  2. package/README.md +8 -4
  3. package/package.json +1 -1
  4. package/pyproject.toml +6 -1
  5. package/src/superlocalmemory/__init__.py +1 -1
  6. package/src/superlocalmemory/cli/compress_cmd.py +32 -70
  7. package/src/superlocalmemory/cli/optimize_cmd.py +1 -3
  8. package/src/superlocalmemory/cli/setup_wizard.py +49 -0
  9. package/src/superlocalmemory/mcp/agent_context.py +111 -0
  10. package/src/superlocalmemory/mcp/tools_active.py +7 -8
  11. package/src/superlocalmemory/mcp/tools_core.py +16 -0
  12. package/src/superlocalmemory/optimize/cache/boundary_store.py +23 -9
  13. package/src/superlocalmemory/optimize/cache/exact.py +7 -4
  14. package/src/superlocalmemory/optimize/cache/key_builder.py +13 -0
  15. package/src/superlocalmemory/optimize/cache/manager.py +70 -8
  16. package/src/superlocalmemory/optimize/cache/semantic.py +10 -5
  17. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +1 -7
  18. package/src/superlocalmemory/optimize/compress/router.py +82 -87
  19. package/src/superlocalmemory/optimize/config/__init__.py +16 -0
  20. package/src/superlocalmemory/optimize/config/defaults.py +1 -6
  21. package/src/superlocalmemory/optimize/config/schema.py +2 -19
  22. package/src/superlocalmemory/optimize/config/store.py +15 -1
  23. package/src/superlocalmemory/optimize/metrics/counters.py +15 -7
  24. package/src/superlocalmemory/optimize/proxy/_helpers.py +100 -2
  25. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +12 -0
  26. package/src/superlocalmemory/optimize/proxy/capture.py +243 -0
  27. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +31 -0
  28. package/src/superlocalmemory/optimize/proxy/openai_surface.py +12 -0
  29. package/src/superlocalmemory/optimize/proxy/server.py +29 -0
  30. package/src/superlocalmemory/optimize/storage/db.py +78 -11
  31. package/src/superlocalmemory/optimize/storage/schema.py +11 -0
  32. package/src/superlocalmemory/server/routes/optimize.py +6 -8
  33. package/src/superlocalmemory/server/unified_daemon.py +26 -5
  34. package/src/superlocalmemory/ui/index.html +18 -14
  35. package/src/superlocalmemory/ui/js/auto-settings.js +3 -1
  36. package/src/superlocalmemory/ui/js/ng-shell.js +3 -0
  37. package/src/superlocalmemory/ui/js/optimize.js +9 -9
  38. package/src/superlocalmemory.egg-info/PKG-INFO +10 -5
  39. package/src/superlocalmemory.egg-info/SOURCES.txt +2 -2
  40. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  41. package/src/superlocalmemory/optimize/compress/extractive_code.py +0 -311
  42. package/src/superlocalmemory/optimize/compress/extractive_json.py +0 -72
@@ -93,19 +93,23 @@ class PerItemBoundaryRecord:
93
93
  query_sim: float,
94
94
  delta: float = 0.05,
95
95
  epsilon_grid: tuple[float, ...] = (0.01, 0.02, 0.05, 0.10),
96
+ return_threshold: float = 1.0,
96
97
  ) -> float:
97
98
  """Compute τ̂ — the vCache exploration probability (Eq. 11).
98
99
 
99
100
  Args:
100
- query_sim: Cosine similarity s(x) ∈ [0, 1] for the incoming query.
101
- delta: δ — user-defined maximum error rate.
102
- Theorem 4.1 guarantee: Pr(correct) ≥ 1 - δ.
103
- epsilon_grid: ε values for the Eq. 11 min sweep.
104
- Distinct from δ; controls CI conservativeness.
101
+ query_sim: Cosine similarity s(x) ∈ [0, 1] for the incoming query.
102
+ delta: δ — user-defined maximum error rate.
103
+ Theorem 4.1 guarantee: Pr(correct) ≥ 1 - δ.
104
+ epsilon_grid: ε values for the Eq. 11 min sweep.
105
+ Distinct from δ; controls CI conservativeness.
106
+ return_threshold: Semantic return threshold from config (semantic_return_threshold).
107
+ C-03 fix: during cold start, exploit directly when
108
+ query_sim >= return_threshold instead of always exploring.
105
109
 
106
110
  Returns:
107
111
  τ̂ ∈ [0.0, 1.0]. Lower = more exploitation.
108
- Cold start (n < 3): returns 1.0 (always explore).
112
+ Cold start (n < 3): 0.0 if query_sim >= return_threshold, else 1.0.
109
113
 
110
114
  Eq. 11 derivation (from the paper):
111
115
  1. I_tt = Σ γ̂² · p_i(1 - p_i) [Fisher info diagonal]
@@ -117,7 +121,12 @@ class PerItemBoundaryRecord:
117
121
  """
118
122
  n = len(self.samples)
119
123
  if n < 3:
120
- return 1.0 # cold startalways explore
124
+ # ARCH-02 note: this function serves dual purpose (a) warm-phase vCache
125
+ # Eq. 11 tau computation and (b) cold-start similarity gate. The cold-start
126
+ # branch (n < 3) is intentionally simple: if the query is already above the
127
+ # return threshold, serve it (tau=0.0 → exploit); otherwise explore (tau=1.0).
128
+ # C-03: honor return_threshold — avoids 100% miss until 3 samples are accumulated.
129
+ return 0.0 if query_sim >= return_threshold else 1.0
121
130
 
122
131
  # Step 1: Fisher-information SE
123
132
  i_tt = 0.0
@@ -144,12 +153,17 @@ class PerItemBoundaryRecord:
144
153
 
145
154
  return best_tau
146
155
 
147
- def should_explore(self, query_sim: float, delta: float = 0.05) -> bool:
156
+ def should_explore(
157
+ self,
158
+ query_sim: float,
159
+ delta: float = 0.05,
160
+ return_threshold: float = 1.0,
161
+ ) -> bool:
148
162
  """Return True (explore = LLM call) or False (exploit = serve cache).
149
163
 
150
164
  Source: vCache Algorithm 2: draw u ~ Uniform(0, 1); explore iff u ≤ τ̂.
151
165
  """
152
- tau = self.compute_tau(query_sim, delta=delta)
166
+ tau = self.compute_tau(query_sim, delta=delta, return_threshold=return_threshold)
153
167
  return _RNG.random() <= tau
154
168
 
155
169
  def add_sample(
@@ -3,11 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import logging
6
7
  import time
7
8
  from typing import Any
8
9
 
9
10
  from superlocalmemory.optimize.cache.key_builder import CacheConfig
10
11
 
12
+ logger = logging.getLogger(__name__)
11
13
 
12
14
  _NON_CACHEABLE_FINISH_REASONS: frozenset[str] = frozenset({
13
15
  "tool_use",
@@ -20,17 +22,21 @@ _NON_CACHEABLE_FINISH_REASONS: frozenset[str] = frozenset({
20
22
  def _is_cacheable_response(response: dict) -> bool:
21
23
  finish = response.get("stop_reason") or response.get("finish_reason") or ""
22
24
  if finish in _NON_CACHEABLE_FINISH_REASONS:
25
+ logger.debug("exact: skip cache (finish_reason=%r)", finish)
23
26
  return False
24
27
  choices = response.get("choices") or []
25
28
  for choice in choices:
26
29
  fr = (choice.get("finish_reason") or "")
27
30
  if fr in _NON_CACHEABLE_FINISH_REASONS:
31
+ logger.debug("exact: skip cache (choice.finish_reason=%r)", fr)
28
32
  return False
29
33
  msg = choice.get("message") or {}
30
34
  if msg.get("tool_calls"):
35
+ logger.debug("exact: skip cache (choice.message.tool_calls present)")
31
36
  return False
32
37
  for block in response.get("content") or []:
33
38
  if isinstance(block, dict) and block.get("type") == "tool_use":
39
+ logger.debug("exact: skip cache (tool_use content block)")
34
40
  return False
35
41
  return True
36
42
 
@@ -46,9 +52,6 @@ class ExactCache:
46
52
  row = self._db.get(key, tenant_id)
47
53
  if row is None:
48
54
  return None
49
- if row.ttl_expires is not None and row.ttl_expires < time.time():
50
- self._db.delete(key, tenant_id)
51
- return None
52
55
  return json.loads(row.value.decode("utf-8"))
53
56
 
54
57
  def set(
@@ -74,7 +77,7 @@ class ExactCache:
74
77
  value=encoded,
75
78
  model=model,
76
79
  ttl_expires=expires_at,
77
- tags=[],
80
+ tags=tags,
78
81
  )
79
82
  return True
80
83
 
@@ -4,10 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import json
7
+ import logging
7
8
  import re
8
9
  from dataclasses import dataclass, field
9
10
  from typing import Any
10
11
 
12
+ logger = logging.getLogger(__name__)
13
+
11
14
  DETERMINISTIC_PARAMS: frozenset[str] = frozenset({
12
15
  "max_tokens", "stop", "stop_sequences", "top_p", "top_k",
13
16
  "response_format", "tools", "tool_choice",
@@ -62,6 +65,16 @@ class KeyBuilder:
62
65
  temperature = 0.0
63
66
 
64
67
  if temperature != 0 and not self._config.allow_nonzero_temperature_cache:
68
+ logger.debug(
69
+ "cache skip: temperature=%.2f allow_nonzero=%s",
70
+ temperature,
71
+ self._config.allow_nonzero_temperature_cache,
72
+ )
73
+ try:
74
+ from superlocalmemory.optimize.metrics.counters import MetricsCollector
75
+ MetricsCollector.get_instance().increment_skipped_temperature()
76
+ except Exception:
77
+ pass
65
78
  return None
66
79
 
67
80
  deterministic_params: dict[str, Any] = {}
@@ -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={