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
@@ -33,6 +33,7 @@ import dataclasses
33
33
  import json
34
34
  import logging
35
35
  import os
36
+ import re
36
37
  import sqlite3
37
38
  import struct
38
39
  import time
@@ -52,6 +53,25 @@ from superlocalmemory.optimize.storage import schema as _schema
52
53
  logger = logging.getLogger(__name__)
53
54
 
54
55
  _DEFAULT_TENANT: str = "default"
56
+ _TENANT_HEX64 = re.compile(r"[0-9a-f]{64}")
57
+
58
+
59
+ def _normalize_tenant_id(tenant_id: str) -> str:
60
+ """Match CacheManager.build_key tenant normalization (v3.6.10 fix).
61
+
62
+ The proxy stores entries under SHA-256(tenant) when the tenant is not
63
+ already a 64-char hex digest. Public tenant-scoped helpers (entry_count,
64
+ clear_tenant, entry_exists) MUST apply the same hashing or they query the
65
+ wrong tenant — the cause of the dashboard reporting "0 entries" while the
66
+ cache is in fact populated.
67
+ """
68
+ tid = tenant_id or _DEFAULT_TENANT
69
+ if _TENANT_HEX64.fullmatch(tid):
70
+ return tid
71
+ import hashlib as _hashlib
72
+ return _hashlib.sha256(tid.encode()).hexdigest()
73
+
74
+
55
75
  _ZLIB_LEVEL: int = 6
56
76
  _AES_NONCE_BYTES: int = 12
57
77
  _PBKDF2_ITERATIONS: int = 100_000
@@ -61,6 +81,9 @@ LLMCACHE_DBNAME: str = "llmcache.db"
61
81
  MID_FILENAME: str = ".llmcache_key"
62
82
  SALT_PREFIX: str = "salt:"
63
83
 
84
+ # C-06: persisted AES key — survives machine-id changes after first run
85
+ _KEY_FILE: Path = Path.home() / LLMCACHE_DIRNAME / "opt-key.bin"
86
+
64
87
  _FORBIDDEN_MEMORY_TABLES: frozenset[str] = frozenset({
65
88
  "memories", "atomic_facts", "profiles", "canonical_entities",
66
89
  "entity_aliases", "consolidation_log", "trust_scores", "bm25_tokens",
@@ -74,7 +97,13 @@ _FORBIDDEN_MEMORY_TABLES: frozenset[str] = frozenset({
74
97
 
75
98
  @dataclass
76
99
  class MetricsSnapshot:
77
- """Mirror of llmcache_metrics columns — names MUST match exactly."""
100
+ """Mirror of llmcache_metrics columns — names MUST match exactly.
101
+
102
+ S-03 / M-03 note: compress_bytes_original and compress_bytes_after store
103
+ WORD-COUNT proxy values (len(text.split())), NOT byte counts. The column
104
+ names use "bytes" for DB schema backward compatibility. Treat these fields
105
+ as token-count proxies, not literal byte measurements.
106
+ """
78
107
  id: int = 1
79
108
  hits: int = 0
80
109
  misses: int = 0
@@ -86,8 +115,8 @@ class MetricsSnapshot:
86
115
  latency_overhead_ms_sum: float = 0.0
87
116
  latency_samples: int = 0
88
117
  compress_runs: int = 0
89
- compress_bytes_original: int = 0
90
- compress_bytes_after: int = 0
118
+ compress_bytes_original: int = 0 # unit: word-count proxy (see S-03 note above)
119
+ compress_bytes_after: int = 0 # unit: word-count proxy (see S-03 note above)
91
120
  cache_size_bytes: int = 0
92
121
  cache_entry_count: int = 0
93
122
  updated_at: float = 0.0
@@ -209,8 +238,7 @@ class CacheDB:
209
238
  except OSError as exc:
210
239
  logger.warning("CacheDB: could not chmod 600 on %s: %s", self._db_path, exc)
211
240
  self._salt = self._load_or_create_salt()
212
- machine_id = self._get_machine_id()
213
- self._aes_key = self._derive_aes_key(machine_id, self._salt)
241
+ self._aes_key = self._get_or_persist_aes_key(self._salt)
214
242
  self.assert_no_memory_db_tables()
215
243
 
216
244
  # ---- context manager ----
@@ -292,6 +320,33 @@ class CacheDB:
292
320
  logger.warning("CacheDB: could not persist machine id: %s", exc)
293
321
  return mid
294
322
 
323
+ def _get_or_persist_aes_key(self, salt: bytes) -> bytes:
324
+ """C-06: Load persisted key from disk, or derive + persist on first run.
325
+
326
+ Surviving a machine-id change: after first derivation the key is saved to
327
+ opt-key.bin (0600). On subsequent starts the file is read directly,
328
+ so changing the underlying machine-id string cannot invalidate existing
329
+ cache entries.
330
+ """
331
+ try:
332
+ if _KEY_FILE.exists():
333
+ key = _KEY_FILE.read_bytes()
334
+ if len(key) == 32:
335
+ return key
336
+ except Exception as exc:
337
+ logger.warning("CacheDB: could not read persisted AES key: %s", exc)
338
+
339
+ # First run (or corrupted file): derive from machine-id and persist.
340
+ machine_id = self._get_machine_id()
341
+ key = self._derive_aes_key(machine_id, salt)
342
+ try:
343
+ _KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
344
+ _KEY_FILE.write_bytes(key)
345
+ os.chmod(_KEY_FILE, 0o600)
346
+ except Exception as exc:
347
+ logger.warning("CacheDB: could not persist AES key (fail-open): %s", exc)
348
+ return key
349
+
295
350
  def _derive_aes_key(self, machine_id: str, salt: bytes) -> bytes:
296
351
  kdf = PBKDF2HMAC(
297
352
  algorithm=hashes.SHA256(),
@@ -626,11 +681,12 @@ class CacheDB:
626
681
  try:
627
682
  dim = int(meta.get("dim", len(vector) // 4))
628
683
  model_name = str(meta.get("model", "nomic-ai/nomic-embed-text-v1.5"))
684
+ context_fp = str(meta.get("context_fp", "")) # C-10: persist context fingerprint
629
685
  self._db.execute(
630
686
  "INSERT OR REPLACE INTO llmcache_semantic_vectors "
631
- "(entry_id, tenant_id, vector_blob, vector_dim, model_name) "
632
- "VALUES (?, ?, ?, ?, ?)",
633
- (entry_id, tenant_id, vector, dim, model_name),
687
+ "(entry_id, tenant_id, vector_blob, vector_dim, model_name, context_fp) "
688
+ "VALUES (?, ?, ?, ?, ?, ?)",
689
+ (entry_id, tenant_id, vector, dim, model_name, context_fp),
634
690
  )
635
691
  except sqlite3.Error as exc:
636
692
  logger.warning("CacheDB.vec_add failed: %s", exc)
@@ -855,14 +911,22 @@ class CacheDB:
855
911
 
856
912
  # ---- v2 additions ----
857
913
 
858
- def get_all_vectors(self, tenant_id: str) -> list[tuple[str, bytes]]:
914
+ def get_all_vectors(self, tenant_id: str) -> list[tuple[str, bytes, str]]:
915
+ """Return (entry_id, vector_blob, context_fp) for all vectors in a tenant.
916
+
917
+ C-10: context_fp is included so _lazy_warm_tenant() can restore it without
918
+ recomputing embeddings from messages that may no longer be in scope.
919
+ """
859
920
  try:
860
921
  rows = self._db.execute(
861
- "SELECT entry_id, vector_blob FROM llmcache_semantic_vectors "
922
+ "SELECT entry_id, vector_blob, context_fp FROM llmcache_semantic_vectors "
862
923
  "WHERE tenant_id = ?",
863
924
  (tenant_id,),
864
925
  )
865
- return [(dict(r)["entry_id"], dict(r)["vector_blob"]) for r in rows]
926
+ return [
927
+ (dict(r)["entry_id"], dict(r)["vector_blob"], dict(r).get("context_fp", ""))
928
+ for r in rows
929
+ ]
866
930
  except sqlite3.Error as exc:
867
931
  logger.warning("CacheDB.get_all_vectors failed: %s", exc)
868
932
  return []
@@ -919,6 +983,7 @@ class CacheDB:
919
983
  # ---- convenience / non-contract helpers ----
920
984
 
921
985
  def entry_exists(self, cache_key: str, tenant_id: str = _DEFAULT_TENANT) -> bool:
986
+ tenant_id = _normalize_tenant_id(tenant_id)
922
987
  try:
923
988
  rows = self._db.execute(
924
989
  "SELECT 1 FROM llmcache_entries WHERE cache_key = ? AND tenant_id = ? LIMIT 1",
@@ -929,6 +994,7 @@ class CacheDB:
929
994
  return False
930
995
 
931
996
  def clear_tenant(self, tenant_id: str) -> int:
997
+ tenant_id = _normalize_tenant_id(tenant_id)
932
998
  try:
933
999
  with self._db.transaction():
934
1000
  rows = self._db.execute(
@@ -955,6 +1021,7 @@ class CacheDB:
955
1021
  return 0
956
1022
 
957
1023
  def entry_count(self, tenant_id: str = _DEFAULT_TENANT) -> int:
1024
+ tenant_id = _normalize_tenant_id(tenant_id)
958
1025
  try:
959
1026
  rows = self._db.execute(
960
1027
  "SELECT COUNT(*) AS n FROM llmcache_entries "
@@ -53,6 +53,7 @@ _DDL_STATEMENTS: tuple[str, ...] = (
53
53
  vector_blob BLOB NOT NULL,
54
54
  vector_dim INTEGER NOT NULL DEFAULT 768,
55
55
  model_name TEXT NOT NULL DEFAULT 'nomic-ai/nomic-embed-text-v1.5',
56
+ context_fp TEXT NOT NULL DEFAULT '',
56
57
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
57
58
  )
58
59
  """,
@@ -127,10 +128,20 @@ def create_all_tables(conn: sqlite3.Connection) -> None:
127
128
 
128
129
  Safe to call repeatedly — all DDL uses IF NOT EXISTS.
129
130
  Also seeds the single llmcache_metrics row (id=1) via INSERT OR IGNORE.
131
+ C-10: adds context_fp column to existing DBs via ALTER TABLE migration.
130
132
  """
131
133
  for stmt in _DDL_STATEMENTS:
132
134
  conn.execute(stmt)
133
135
  conn.execute("INSERT OR IGNORE INTO llmcache_metrics(id) VALUES (1)")
136
+ # C-10 migration: add context_fp column if missing (existing installs pre-v3.6.10)
137
+ existing_cols = {
138
+ row[1]
139
+ for row in conn.execute("PRAGMA table_info(llmcache_semantic_vectors)")
140
+ }
141
+ if "context_fp" not in existing_cols:
142
+ conn.execute(
143
+ "ALTER TABLE llmcache_semantic_vectors ADD COLUMN context_fp TEXT NOT NULL DEFAULT ''"
144
+ )
134
145
  row = conn.execute(
135
146
  "SELECT 1 FROM llmcache_schema_version WHERE version = ?",
136
147
  (CACHE_SCHEMA_VERSION,),
@@ -32,17 +32,15 @@ class ConfigUpdateRequest(BaseModel):
32
32
  semantic_enabled: bool | None = None
33
33
  compress_enabled: bool | None = None
34
34
  compress_mode: Literal['safe', 'aggressive'] | None = None
35
- compress_code: bool | None = None
36
35
  compress_prose: bool | None = None
37
- compress_ccr: bool | None = None
38
36
 
39
37
 
40
38
  @router.get("/config")
41
39
  async def get_config() -> dict[str, Any]:
42
40
  """Return current optimize config as JSON."""
43
41
  try:
44
- from superlocalmemory.optimize.config.store import ConfigStore
45
- cfg = ConfigStore().get()
42
+ from superlocalmemory.optimize.config import get_shared_store
43
+ cfg = get_shared_store().get()
46
44
  return cfg.as_dict()
47
45
  except Exception as exc:
48
46
  logger.warning("GET /api/optimize/config failed: %s", exc)
@@ -54,8 +52,8 @@ async def put_config(body: ConfigUpdateRequest) -> dict[str, Any]:
54
52
  """Update optimize config (partial). Daemon hot-reloads within 2s."""
55
53
  try:
56
54
  import dataclasses
57
- from superlocalmemory.optimize.config.store import ConfigStore
58
- store = ConfigStore()
55
+ from superlocalmemory.optimize.config import get_shared_store
56
+ store = get_shared_store()
59
57
  cfg = store.get()
60
58
  updates: dict[str, Any] = {}
61
59
  for field_name in ConfigUpdateRequest.model_fields:
@@ -80,11 +78,11 @@ async def get_savings() -> dict[str, Any]:
80
78
  Field names match INTERFACE-CONTRACT §5 exactly.
81
79
  """
82
80
  try:
83
- from superlocalmemory.optimize.config.store import ConfigStore
81
+ from superlocalmemory.optimize.config import get_shared_store
84
82
  from superlocalmemory.optimize.metrics.counters import get_metrics
85
83
  from superlocalmemory.optimize.storage.db import CacheDB
86
84
 
87
- cfg = ConfigStore().get()
85
+ cfg = get_shared_store().get()
88
86
  collector = get_metrics()
89
87
  db = CacheDB()
90
88
  snap = collector.snapshot(
@@ -1183,18 +1183,31 @@ def create_app() -> FastAPI:
1183
1183
  # API keys (x-api-key, Authorization), NOT the SLM API key. Auth-exempt
1184
1184
  # path prefixes are configured below in the auth_middleware block.
1185
1185
  try:
1186
- from superlocalmemory.optimize.config.store import ConfigStore
1186
+ from superlocalmemory.optimize.config import _set_config_store, get_shared_store
1187
1187
  from superlocalmemory.optimize.proxy.server import ProxyApp, build_proxy_router
1188
1188
 
1189
- _opt_cfg = ConfigStore().get()
1189
+ # ONE shared ConfigStore for daemon + routes + watchdog + proxy reload
1190
+ # (fixes W-05 fresh-store-per-request; powers runtime hot-reload).
1191
+ _opt_store = get_shared_store()
1192
+ _set_config_store(_opt_store)
1193
+ _opt_cfg = _opt_store.get()
1194
+ # W-03 fix: the proxy path is gated by proxy_enabled ALONE. The master
1195
+ # `enabled` gates only the SDK adapter, never the proxy mount.
1190
1196
  if _opt_cfg.proxy_enabled:
1191
1197
  _proxy = ProxyApp(config=_opt_cfg)
1192
1198
  application.state.optimize_proxy = _proxy
1193
1199
  _proxy_router = build_proxy_router(_proxy)
1194
1200
  # prefix="" — proxy claims /v1/*, /v1beta/* directly.
1195
1201
  application.include_router(_proxy_router, prefix="")
1202
+ # v3.6.10: runtime hot-reload — rebuild the proxy HookChain whenever
1203
+ # optimize.json changes so cache_enabled / compress_enabled can be
1204
+ # toggled INDEPENDENTLY from the UI with no restart. UI save fires the
1205
+ # callback immediately; external edits are caught by the 2s watchdog.
1206
+ _opt_store.register_change_callback(_proxy.reload_from_config)
1207
+ _opt_store.start_watchdog()
1196
1208
  logger.info(
1197
- "optimize.proxy mounted on /v1/*, /v1beta/* port=8765"
1209
+ "optimize.proxy mounted on /v1/*, /v1beta/* port=8765 "
1210
+ "(runtime cache/compress hot-reload enabled)"
1198
1211
  )
1199
1212
  else:
1200
1213
  application.state.optimize_proxy = None
@@ -1246,8 +1259,16 @@ def create_app() -> FastAPI:
1246
1259
  logger.info("MCP transport security: allowed_hosts=%r", _mcp_allowed)
1247
1260
  global _mcp_app
1248
1261
  _mcp_app = _mcp_fastmcp.streamable_http_app()
1249
- application.mount("/mcp", _mcp_app)
1250
- logger.info("MCP HTTP transport mounted at /mcp (Streamable HTTP, port 8765)")
1262
+
1263
+ # v3.6.10: per-agent-ID routing /mcp/{agent_id} extracts the agent
1264
+ # identity from the URL path and places it in a ContextVar so all MCP
1265
+ # tools (remember, recall, etc.) automatically use the correct namespace.
1266
+ # AgentIDExtractorASGI lives in mcp/agent_context so it is unit-testable
1267
+ # (tests/test_mcp/test_agent_context.py) rather than buried inline here.
1268
+ from superlocalmemory.mcp.agent_context import AgentIDExtractorASGI
1269
+
1270
+ application.mount("/mcp", AgentIDExtractorASGI(_mcp_app))
1271
+ logger.info("MCP HTTP transport mounted at /mcp (Streamable HTTP, port 8765; per-agent routing enabled)")
1251
1272
  except Exception as _mcp_exc: # pragma: no cover — defensive
1252
1273
  logger.warning("MCP HTTP mount failed (non-fatal, stdio still works): %s", _mcp_exc)
1253
1274
 
@@ -1248,6 +1248,16 @@
1248
1248
  <input class="form-check-input" type="checkbox" id="opt-enabled">
1249
1249
  <label class="form-check-label" for="opt-enabled">Optimize Enabled</label>
1250
1250
  </div>
1251
+ <div class="form-check form-switch mb-2">
1252
+ <input class="form-check-input" type="checkbox" id="opt-proxy-enabled">
1253
+ <label class="form-check-label" for="opt-proxy-enabled">
1254
+ Proxy Enabled
1255
+ <span class="text-muted small ms-1">(restart required)</span>
1256
+ </label>
1257
+ </div>
1258
+ <div id="opt-restart-notice" class="alert alert-warning py-1 px-2 small mb-2 d-none">
1259
+ Proxy setting changed. Run <code>slm restart</code> to apply.
1260
+ </div>
1251
1261
  <div class="form-check form-switch mb-2">
1252
1262
  <input class="form-check-input" type="checkbox" id="opt-cache-enabled">
1253
1263
  <label class="form-check-label" for="opt-cache-enabled">Cache Enabled</label>
@@ -1263,26 +1273,14 @@
1263
1273
  <div class="mb-2">
1264
1274
  <label for="opt-compress-mode" class="form-label">Compression Mode</label>
1265
1275
  <select class="form-select form-select-sm" id="opt-compress-mode">
1266
- <option value="safe">Safe</option>
1267
- <option value="aggressive">Aggressive</option>
1276
+ <option value="safe">Safe (lossless)</option>
1277
+ <option value="aggressive">Aggressive (LLMLingua-2, prose only)</option>
1268
1278
  </select>
1269
1279
  </div>
1270
- <div class="form-check form-switch mb-2">
1271
- <input class="form-check-input" type="checkbox" id="opt-compress-code">
1272
- <label class="form-check-label" for="opt-compress-code">Code Compression</label>
1273
- </div>
1274
1280
  <div class="form-check form-switch mb-2">
1275
1281
  <input class="form-check-input" type="checkbox" id="opt-compress-prose">
1276
1282
  <label class="form-check-label" for="opt-compress-prose">Prose Compression</label>
1277
1283
  </div>
1278
- <div class="form-check form-switch mb-2">
1279
- <input class="form-check-input" type="checkbox" id="opt-compress-ccr">
1280
- <label class="form-check-label" for="opt-compress-ccr">CCR</label>
1281
- </div>
1282
- <div class="form-check form-switch mb-2">
1283
- <input class="form-check-input" type="checkbox" id="opt-compress-align">
1284
- <label class="form-check-label" for="opt-compress-align">Alignment Compression</label>
1285
- </div>
1286
1284
  </div>
1287
1285
  <div class="col-md-6">
1288
1286
  <h6>Savings</h6>
@@ -1299,6 +1297,12 @@
1299
1297
  <div class="small text-muted">Config version: <span id="opt-config-version">-</span></div>
1300
1298
  <div id="opt-stale-warning" class="text-warning small mt-1"></div>
1301
1299
  <button class="btn btn-sm btn-outline-primary mt-2" id="opt-copy-url">Copy URL</button>
1300
+ <div class="mt-2 small text-muted">
1301
+ <strong>Point agents at the proxy:</strong><br>
1302
+ <code>slm wrap claude</code> — Claude Code<br>
1303
+ <code>--openai-api-base http://localhost:8765/v1</code> — Aider<br>
1304
+ <code>OPENAI_BASE_URL=http://localhost:8765/v1</code> — OpenAI SDK
1305
+ </div>
1302
1306
  </div>
1303
1307
  </div>
1304
1308
  </div>
@@ -324,10 +324,12 @@ async function testConnection() {
324
324
  if (resultEl) { resultEl.textContent = 'Testing...'; resultEl.className = 'ms-2 small text-muted'; }
325
325
 
326
326
  try {
327
+ var testBody = {provider: provider, model: model};
328
+ if (apiKey) testBody.api_key = apiKey;
327
329
  var resp = await fetch('/api/v3/provider/test', {
328
330
  method: 'POST',
329
331
  headers: {'Content-Type': 'application/json'},
330
- body: JSON.stringify({provider: provider, model: model, api_key: apiKey})
332
+ body: JSON.stringify(testBody)
331
333
  });
332
334
  var data = await resp.json();
333
335
  if (data.success) {
@@ -338,6 +338,9 @@
338
338
  // ── Lazy Load Tab Data ─────────────────────────────────────
339
339
  function triggerTabLoad(tabId) {
340
340
  switch(tabId) {
341
+ case 'brain-pane':
342
+ if (typeof loadBrain === 'function') loadBrain();
343
+ break;
341
344
  case 'graph-pane':
342
345
  if (typeof loadGraph === 'function') loadGraph();
343
346
  // v3.4.4: Initialize chat panel if not already present
@@ -23,14 +23,12 @@
23
23
  if (!resp.ok) return;
24
24
  var cfg = await resp.json();
25
25
  _setToggle('opt-enabled', cfg.enabled);
26
+ _setToggle('opt-proxy-enabled', cfg.proxy_enabled);
26
27
  _setToggle('opt-cache-enabled', cfg.cache_enabled);
27
28
  _setToggle('opt-semantic-enabled', cfg.semantic_enabled);
28
29
  _setToggle('opt-compress-enabled', cfg.compress_enabled);
29
30
  _setSelect('opt-compress-mode', cfg.compress_mode);
30
- _setToggle('opt-compress-code', cfg.compress_code);
31
31
  _setToggle('opt-compress-prose', cfg.compress_prose);
32
- _setToggle('opt-compress-ccr', cfg.compress_ccr);
33
- _setToggle('opt-compress-align', cfg.compress_align);
34
32
  var verEl = document.getElementById('opt-config-version');
35
33
  if (verEl) verEl.textContent = cfg.config_version || '-';
36
34
  } catch (e) {
@@ -70,14 +68,12 @@
70
68
  var val = e.target.checked;
71
69
 
72
70
  var fieldMap = {
73
- 'opt-enabled': 'enabled',
74
- 'opt-cache-enabled': 'cache_enabled',
71
+ 'opt-enabled': 'enabled',
72
+ 'opt-proxy-enabled': 'proxy_enabled',
73
+ 'opt-cache-enabled': 'cache_enabled',
75
74
  'opt-semantic-enabled': 'semantic_enabled',
76
75
  'opt-compress-enabled': 'compress_enabled',
77
- 'opt-compress-code': 'compress_code',
78
- 'opt-compress-prose': 'compress_prose',
79
- 'opt-compress-ccr': 'compress_ccr',
80
- 'opt-compress-align': 'compress_align'
76
+ 'opt-compress-prose': 'compress_prose'
81
77
  };
82
78
 
83
79
  if (id === 'opt-compress-mode') {
@@ -109,6 +105,10 @@
109
105
  var body = {};
110
106
  body[field] = val;
111
107
  _putConfig(body);
108
+ if (id === 'opt-proxy-enabled' || id === 'opt-enabled') {
109
+ var notice = document.getElementById('opt-restart-notice');
110
+ if (notice) notice.classList.remove('d-none');
111
+ }
112
112
  }
113
113
  });
114
114
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.6.9
3
+ Version: 3.6.10
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: AGPL-3.0-or-later
@@ -58,6 +58,7 @@ Requires-Dist: huggingface_hub==0.36.2
58
58
  Requires-Dist: torch==2.11.0
59
59
  Requires-Dist: scikit-learn==1.8.0
60
60
  Requires-Dist: sqlite-vec==0.1.9
61
+ Requires-Dist: llmlingua==0.2.2
61
62
  Provides-Extra: search
62
63
  Requires-Dist: sentence-transformers==5.3.0; extra == "search"
63
64
  Requires-Dist: optimum==2.1.0; extra == "search"
@@ -125,18 +126,22 @@ Dynamic: license-file
125
126
  <details>
126
127
  <summary><strong>What's New in V3.6 — Optimize: SKIP, SHRINK, DISCOUNT, REMEMBER</strong> (click to expand)</summary>
127
128
 
128
- > V3.6 is the only local-first layer that SKIPS repeat LLM calls (cache: 100% saved), SHRINKS prompts 60-95% (compress: extractive + LLMLingua-2), and DISCOUNTS prefix costs (align: native KV-cache) — and remembers everything — in one install. **Your first cache hit pays for the install time. Hours of coding on repeat, minimal API cost.**
129
+ > V3.6 is the only local-first layer that SKIPS repeat LLM calls (cache: 100% saved on a hit), SHRINKS prose prompts (compress: lossless-by-default, opt-in LLMLingua-2), and DISCOUNTS prefix costs (align: native KV-cache) — and remembers everything — in one install. **Your first cache hit pays for the install time. Hours of coding on repeat, minimal API cost.**
130
+ >
131
+ > **v3.6.10:** cache and compression are now **independent runtime switches** (cache-only, compress-only, both, or neither — toggle live from the dashboard, no restart). Compression was rebuilt to be **lossless by default** (the old string/array/code truncation is gone); aggressive mode adds LLMLingua-2 for **prose only** — never code, numbers, structured data, or the current turn.
129
132
 
130
133
  ### The Three Levers
131
134
 
132
135
  | Lever | Mechanism | Saving | Off by default? |
133
136
  |-------|-----------|:------:|:---------------:|
134
- | **Cache** | Skip repeat calls — exact-match SQLite lookup, vCache-gated semantic (opt-in) | **100% on a hit** (input + output) | Cache ON, Semantic OFF |
135
- | **Compress** | Shrink prompts — extractive JSON/code (lossless) + LLMLingua-2 prose (opt-in) | **60–95% on a miss** (input only) | Safe mode ON, Aggressive OFF |
137
+ | **Cache** | Skip repeat calls — exact-match SQLite lookup (zero false hits), vCache-gated semantic (opt-in) | **100% on a hit** (input + output) | Cache ON, Semantic OFF |
138
+ | **Compress** | Shrink prompts — **safe = lossless** normalization; **aggressive = LLMLingua-2 prose only** (opt-in) | Safe: small + lossless · Aggressive: large on prose | Safe mode, Aggressive OFF |
136
139
  | **Align** | Stabilize prefix — maximize provider prefix-cache discounts | **Lossless extra** | ON when compression is ON |
137
140
 
138
141
  **Memory** (v3.5's existing engine) runs in parallel — it shapes *what is in* the prompt (relevant facts); Optimize decides *whether and how* it is sent.
139
142
 
143
+ > **Independent at runtime:** enable caching only, compression only, both, or neither — from the dashboard Optimize tab, applied live (no restart). Each AI client can also get its own memory identity over HTTP MCP via `http://127.0.0.1:8765/mcp/{agent_id}`.
144
+
140
145
  ### Quick Start
141
146
 
142
147
  ```bash
@@ -152,7 +157,7 @@ slm wrap claude
152
157
  |:--------|:-------------|
153
158
  | `slm optimize status\|on\|off\|savings` | Master Optimize control + savings report (USD/INR/tokens) |
154
159
  | `slm cache status\|clear\|invalidate\|ttl\|semantic` | Cache sub-control — exact + semantic tiers |
155
- | `slm compress status\|mode\|code\|prose\|ccr\|align` | Compression control — per-channel toggles |
160
+ | `slm compress status\|mode\|prose` | Compression control — safe (lossless) / aggressive (LLMLingua-2 prose) |
156
161
  | `slm proxy [--port] [--provider]` | Start the interception proxy (port 8765) |
157
162
  | `slm wrap <agent>` | Proxy-activate an agent — one command to start saving |
158
163
  | `slm help-optimize [topic]` | Full developer reference + per-agent setup recipes |
@@ -271,6 +271,7 @@ src/superlocalmemory/mcp/__init__.py
271
271
  src/superlocalmemory/mcp/_daemon_proxy.py
272
272
  src/superlocalmemory/mcp/_pool_adapter.py
273
273
  src/superlocalmemory/mcp/_stdin_guard.py
274
+ src/superlocalmemory/mcp/agent_context.py
274
275
  src/superlocalmemory/mcp/resources.py
275
276
  src/superlocalmemory/mcp/server.py
276
277
  src/superlocalmemory/mcp/shared.py
@@ -309,8 +310,6 @@ src/superlocalmemory/optimize/cache/stampede.py
309
310
  src/superlocalmemory/optimize/compress/__init__.py
310
311
  src/superlocalmemory/optimize/compress/align.py
311
312
  src/superlocalmemory/optimize/compress/ccr.py
312
- src/superlocalmemory/optimize/compress/extractive_code.py
313
- src/superlocalmemory/optimize/compress/extractive_json.py
314
313
  src/superlocalmemory/optimize/compress/prose_llmlingua.py
315
314
  src/superlocalmemory/optimize/compress/router.py
316
315
  src/superlocalmemory/optimize/config/__init__.py
@@ -325,6 +324,7 @@ src/superlocalmemory/optimize/metrics/persistence.py
325
324
  src/superlocalmemory/optimize/proxy/__init__.py
326
325
  src/superlocalmemory/optimize/proxy/_helpers.py
327
326
  src/superlocalmemory/optimize/proxy/anthropic_surface.py
327
+ src/superlocalmemory/optimize/proxy/capture.py
328
328
  src/superlocalmemory/optimize/proxy/gemini_surface.py
329
329
  src/superlocalmemory/optimize/proxy/lifecycle.py
330
330
  src/superlocalmemory/optimize/proxy/openai_surface.py
@@ -29,6 +29,7 @@ huggingface_hub==0.36.2
29
29
  torch==2.11.0
30
30
  scikit-learn==1.8.0
31
31
  sqlite-vec==0.1.9
32
+ llmlingua==0.2.2
32
33
 
33
34
  [dev]
34
35
  pytest>=8.0