superlocalmemory 3.5.8 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/ATTRIBUTION.md +24 -0
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +142 -35
  4. package/package.json +1 -1
  5. package/pyproject.toml +2 -1
  6. package/src/superlocalmemory/__init__.py +1 -1
  7. package/src/superlocalmemory/cli/cache_cmd.py +198 -0
  8. package/src/superlocalmemory/cli/commands.py +80 -2
  9. package/src/superlocalmemory/cli/compress_cmd.py +179 -0
  10. package/src/superlocalmemory/cli/help_cmd.py +197 -0
  11. package/src/superlocalmemory/cli/main.py +122 -0
  12. package/src/superlocalmemory/cli/optimize_cmd.py +175 -0
  13. package/src/superlocalmemory/cli/optimize_constants.py +31 -0
  14. package/src/superlocalmemory/cli/proxy_cmd.py +95 -0
  15. package/src/superlocalmemory/core/config.py +5 -0
  16. package/src/superlocalmemory/core/engine.py +23 -0
  17. package/src/superlocalmemory/core/mcp_embedder_proxy.py +89 -0
  18. package/src/superlocalmemory/llm/backbone.py +10 -4
  19. package/src/superlocalmemory/mcp/server.py +34 -0
  20. package/src/superlocalmemory/mcp/tools_v3.py +6 -2
  21. package/src/superlocalmemory/optimize/NOTICE +11 -0
  22. package/src/superlocalmemory/optimize/__init__.py +0 -0
  23. package/src/superlocalmemory/optimize/adapters/__init__.py +68 -0
  24. package/src/superlocalmemory/optimize/adapters/_agent_registry.py +120 -0
  25. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +115 -0
  26. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +125 -0
  27. package/src/superlocalmemory/optimize/adapters/wrap.py +188 -0
  28. package/src/superlocalmemory/optimize/cache/__init__.py +31 -0
  29. package/src/superlocalmemory/optimize/cache/boundary_store.py +455 -0
  30. package/src/superlocalmemory/optimize/cache/centroid_store.py +158 -0
  31. package/src/superlocalmemory/optimize/cache/context_key.py +67 -0
  32. package/src/superlocalmemory/optimize/cache/exact.py +85 -0
  33. package/src/superlocalmemory/optimize/cache/invalidation.py +36 -0
  34. package/src/superlocalmemory/optimize/cache/key_builder.py +98 -0
  35. package/src/superlocalmemory/optimize/cache/manager.py +452 -0
  36. package/src/superlocalmemory/optimize/cache/semantic.py +568 -0
  37. package/src/superlocalmemory/optimize/cache/stampede.py +50 -0
  38. package/src/superlocalmemory/optimize/compress/__init__.py +17 -0
  39. package/src/superlocalmemory/optimize/compress/align.py +153 -0
  40. package/src/superlocalmemory/optimize/compress/ccr.py +157 -0
  41. package/src/superlocalmemory/optimize/compress/extractive_code.py +311 -0
  42. package/src/superlocalmemory/optimize/compress/extractive_json.py +72 -0
  43. package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +77 -0
  44. package/src/superlocalmemory/optimize/compress/router.py +548 -0
  45. package/src/superlocalmemory/optimize/config/__init__.py +35 -0
  46. package/src/superlocalmemory/optimize/config/defaults.py +48 -0
  47. package/src/superlocalmemory/optimize/config/schema.py +255 -0
  48. package/src/superlocalmemory/optimize/config/store.py +209 -0
  49. package/src/superlocalmemory/optimize/metrics/__init__.py +8 -0
  50. package/src/superlocalmemory/optimize/metrics/counters.py +138 -0
  51. package/src/superlocalmemory/optimize/metrics/estimator.py +90 -0
  52. package/src/superlocalmemory/optimize/metrics/exporters.py +77 -0
  53. package/src/superlocalmemory/optimize/metrics/persistence.py +115 -0
  54. package/src/superlocalmemory/optimize/proxy/__init__.py +28 -0
  55. package/src/superlocalmemory/optimize/proxy/_helpers.py +257 -0
  56. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +171 -0
  57. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +121 -0
  58. package/src/superlocalmemory/optimize/proxy/lifecycle.py +126 -0
  59. package/src/superlocalmemory/optimize/proxy/openai_surface.py +125 -0
  60. package/src/superlocalmemory/optimize/proxy/server.py +151 -0
  61. package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
  62. package/src/superlocalmemory/optimize/storage/db.py +1016 -0
  63. package/src/superlocalmemory/optimize/storage/schema.py +184 -0
  64. package/src/superlocalmemory/server/routes/optimize.py +166 -0
  65. package/src/superlocalmemory/server/routes/v3_api.py +63 -1
  66. package/src/superlocalmemory/server/unified_daemon.py +105 -0
  67. package/src/superlocalmemory/ui/index.html +98 -0
  68. package/src/superlocalmemory/ui/js/ng-shell.js +2 -1
  69. package/src/superlocalmemory/ui/js/optimize.js +173 -0
  70. package/src/superlocalmemory.egg-info/PKG-INFO +2 -1
  71. package/src/superlocalmemory.egg-info/SOURCES.txt +51 -0
  72. package/src/superlocalmemory.egg-info/requires.txt +1 -0
@@ -0,0 +1,175 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Handlers for ``slm optimize status|on|off|savings``."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import dataclasses
10
+ import json
11
+ import sys
12
+ from argparse import Namespace
13
+
14
+ from superlocalmemory.cli.optimize_constants import (
15
+ DEFAULT_COST_PER_MILLION_INPUT_TOKENS,
16
+ OPTIMIZE_DEFAULT_PORT,
17
+ _PRICING_DATE,
18
+ )
19
+
20
+
21
+ def _get_store():
22
+ from superlocalmemory.optimize.config.store import ConfigStore
23
+ return ConfigStore()
24
+
25
+
26
+ def _get_cache_db():
27
+ from superlocalmemory.optimize.storage.db import CacheDB
28
+ return CacheDB()
29
+
30
+
31
+ def _write_config(**fields) -> None:
32
+ """5-step immutable config-write. Call from any handler."""
33
+ store = _get_store()
34
+ cfg = store.get()
35
+ try:
36
+ cfg = dataclasses.replace(cfg, **fields)
37
+ store.save(cfg)
38
+ except ValueError as e:
39
+ print(f"Error: {e}", file=sys.stderr)
40
+ sys.exit(1)
41
+ except OSError as e:
42
+ print(f"Error writing config: {e}", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+
46
+ def cmd_optimize(args: Namespace) -> None:
47
+ """Top-level dispatcher for ``slm optimize <subcommand>``."""
48
+ sub = getattr(args, "opt_command", None)
49
+ _dispatch = {
50
+ "status": cmd_optimize_status,
51
+ "on": cmd_optimize_on,
52
+ "off": cmd_optimize_off,
53
+ "savings": cmd_optimize_savings,
54
+ }
55
+ handler = _dispatch.get(sub or "")
56
+ if handler:
57
+ handler(args)
58
+ else:
59
+ print("Usage: slm optimize status|on|off|savings [--json] [--since N]")
60
+ sys.exit(0)
61
+
62
+
63
+ def cmd_optimize_status(args: Namespace) -> None:
64
+ """Print current Optimize module status."""
65
+ cfg = _get_store().get()
66
+ use_json = getattr(args, "json", False)
67
+
68
+ proxy_running = False
69
+ try:
70
+ from superlocalmemory.optimize.proxy import lifecycle
71
+ proxy_running = lifecycle.proxy_is_running()
72
+ except Exception:
73
+ pass
74
+
75
+ if use_json:
76
+ data = {
77
+ "status": "ok",
78
+ "optimize_enabled": cfg.enabled,
79
+ "cache_enabled": cfg.cache_enabled,
80
+ "semantic_enabled": cfg.semantic_enabled,
81
+ "compress_enabled": cfg.compress_enabled,
82
+ "compress_mode": cfg.compress_mode,
83
+ "proxy_running": proxy_running,
84
+ "proxy_port": OPTIMIZE_DEFAULT_PORT,
85
+ "config_version": cfg.config_version,
86
+ }
87
+ print(json.dumps(data, indent=2))
88
+ return
89
+
90
+ state = "ON" if cfg.enabled else "OFF"
91
+ print(f"Optimize: {state}")
92
+ print(f" Cache: {'enabled' if cfg.cache_enabled else 'disabled'}"
93
+ f" (exact: {cfg.ttl.exact_seconds}s TTL,"
94
+ f" semantic: {'OFF' if not cfg.semantic_enabled else f'{cfg.ttl.semantic_seconds}s'})")
95
+ print(f" Compress: {'enabled' if cfg.compress_enabled else 'disabled'}"
96
+ f" (mode: {cfg.compress_mode},"
97
+ f" code: {'ON' if cfg.compress_code else 'OFF'},"
98
+ f" prose: {'ON' if cfg.compress_prose else 'OFF'},"
99
+ f" CCR: {'ON' if cfg.compress_ccr else 'OFF'})")
100
+ proxy_status = f"running on :{OPTIMIZE_DEFAULT_PORT}" if proxy_running else "not running"
101
+ print(f" Proxy: {proxy_status}")
102
+ print(f" Config: ~/.superlocalmemory/optimize.json (version {cfg.config_version})")
103
+
104
+
105
+ def cmd_optimize_on(args: Namespace) -> None:
106
+ """Enable all Optimize features (cache + compress)."""
107
+ _write_config(enabled=True, cache_enabled=True, compress_enabled=True)
108
+ use_json = getattr(args, "json", False)
109
+ if use_json:
110
+ print(json.dumps({"status": "ok", "optimize_enabled": True}))
111
+ else:
112
+ print("Optimize enabled. Run 'slm proxy' to start the proxy."
113
+ " Daemon hot-reload: active within 2s.")
114
+
115
+
116
+ def cmd_optimize_off(args: Namespace) -> None:
117
+ """Disable all Optimize features. Does NOT stop proxy."""
118
+ _write_config(
119
+ enabled=False,
120
+ cache_enabled=False,
121
+ semantic_enabled=False,
122
+ compress_enabled=False,
123
+ )
124
+ use_json = getattr(args, "json", False)
125
+ if use_json:
126
+ print(json.dumps({"status": "ok", "optimize_enabled": False}))
127
+ else:
128
+ print("Optimize disabled. Proxy (if running) will pass through calls unchanged.")
129
+
130
+
131
+ def cmd_optimize_savings(args: Namespace) -> None:
132
+ """Print token/cost savings from CacheDB.metrics_load()."""
133
+ since = getattr(args, "since", 7)
134
+ provider = getattr(args, "provider", None)
135
+ use_json = getattr(args, "json", False)
136
+
137
+ if since <= 0:
138
+ print("Error: --since must be a positive integer.", file=sys.stderr)
139
+ sys.exit(1)
140
+
141
+ snap = _get_cache_db().metrics_load()
142
+ cfg = _get_store().get()
143
+
144
+ tokens_saved = snap.tokens_saved_input + snap.tokens_saved_output + snap.tokens_saved_compress
145
+ provider_key = provider or "default"
146
+ rate = DEFAULT_COST_PER_MILLION_INPUT_TOKENS.get(
147
+ provider_key,
148
+ DEFAULT_COST_PER_MILLION_INPUT_TOKENS["default"],
149
+ )
150
+ # Allow config overrides
151
+ if cfg.pricing_overrides and provider_key in cfg.pricing_overrides:
152
+ rate = cfg.pricing_overrides[provider_key].get("input_per_1m_usd", rate)
153
+
154
+ estimated_savings_usd = tokens_saved / 1_000_000 * rate
155
+
156
+ if use_json:
157
+ data = {
158
+ "exact_hits": snap.hits,
159
+ "semantic_hits": snap.misses,
160
+ "tokens_saved": tokens_saved,
161
+ "estimated_savings_usd": round(estimated_savings_usd, 6),
162
+ "pricing_date": _PRICING_DATE,
163
+ "tokens_saved_input": snap.tokens_saved_input,
164
+ "tokens_saved_output": snap.tokens_saved_output,
165
+ "tokens_saved_compress": snap.tokens_saved_compress,
166
+ }
167
+ print(json.dumps(data, indent=2))
168
+ return
169
+
170
+ print(f"Savings (last {since} days):")
171
+ print(f" Exact cache hits: {snap.hits:>5} ({snap.tokens_saved_input:,} input tokens saved)")
172
+ print(f" Semantic cache hits: {snap.misses:>5}")
173
+ print(f" Tokens saved (total): {tokens_saved:,}")
174
+ print(f" Estimated savings: ~${estimated_savings_usd:.4f} (at ${rate:.2f}/M tokens)")
175
+ print(f" Pricing date: {_PRICING_DATE}")
@@ -0,0 +1,31 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Constants shared by Optimize CLI handlers.
6
+
7
+ No imports from optimize.* at module load time (lazy import in functions only).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Final
13
+
14
+ OPTIMIZE_DEFAULT_PORT: Final[int] = 8765
15
+
16
+ # Cache skip saves BOTH input+output tokens (whole call avoided).
17
+ # For CLI display we show the conservative input-price lower bound.
18
+ DEFAULT_COST_PER_MILLION_INPUT_TOKENS: Final[dict[str, float]] = {
19
+ "anthropic": 3.00,
20
+ "openai": 2.50,
21
+ "gemini": 1.25,
22
+ "default": 3.00,
23
+ }
24
+ _PRICING_DATE: Final[str] = "2026-06-07"
25
+
26
+ AGGRESSIVE_MODE_WARNING: Final[str] = (
27
+ "WARNING: Aggressive mode may reduce output fidelity.\n"
28
+ " Do NOT use for: code generation, legal text, exact-output tasks, math.\n"
29
+ " Safe for: summarization, brainstorming, open-ended chat.\n"
30
+ " To revert: slm compress mode safe"
31
+ )
@@ -0,0 +1,95 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Handler for ``slm proxy``."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import dataclasses
10
+ import json
11
+ import sys
12
+ from argparse import Namespace
13
+
14
+ _DEFAULT_PORT: int = 8765
15
+
16
+
17
+ def _get_store():
18
+ from superlocalmemory.optimize.config.store import ConfigStore
19
+ return ConfigStore()
20
+
21
+
22
+ def _ensure_running(port: int) -> bool:
23
+ """Wrap proxy.lifecycle.ensure_running. Monkeypatchable in tests."""
24
+ try:
25
+ from superlocalmemory.optimize.proxy import lifecycle
26
+ return lifecycle.ensure_running(port=port)
27
+ except ImportError:
28
+ print("Error: proxy module not available.", file=sys.stderr)
29
+ return False
30
+
31
+
32
+ def cmd_proxy(args: Namespace) -> None:
33
+ """Start the SLM optimization proxy (or report if already running)."""
34
+ use_json = getattr(args, "json", False)
35
+ port = getattr(args, "port", _DEFAULT_PORT)
36
+ provider = getattr(args, "provider", "anthropic")
37
+ no_compress = getattr(args, "no_compress", False)
38
+ semantic = getattr(args, "semantic", False)
39
+
40
+ # CRIT-3 FIX: validate port before any config write
41
+ if not (1024 <= port <= 65535):
42
+ print(f"Error: --port must be 1024–65535, got {port}.", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ store = _get_store()
46
+ cfg = store.get()
47
+
48
+ # Build config updates
49
+ providers = dict(cfg.providers) if cfg.providers else {}
50
+ from superlocalmemory.optimize.config.schema import ProviderConfig
51
+ existing = providers.get(provider, ProviderConfig())
52
+ providers[provider] = ProviderConfig(
53
+ enabled=existing.enabled,
54
+ base_url=f"http://localhost:{port}",
55
+ )
56
+
57
+ fields: dict = {"providers": providers}
58
+ if no_compress:
59
+ fields["compress_enabled"] = False
60
+ if semantic:
61
+ fields["semantic_enabled"] = True
62
+
63
+ try:
64
+ cfg = dataclasses.replace(cfg, **fields)
65
+ store.save(cfg)
66
+ except (ValueError, OSError) as e:
67
+ print(f"Error writing config: {e}", file=sys.stderr)
68
+ sys.exit(1)
69
+
70
+ running = _ensure_running(port)
71
+
72
+ if use_json:
73
+ data = {
74
+ "status": "running" if running else "failed",
75
+ "port": port,
76
+ "anthropic_url": f"http://localhost:{port}",
77
+ "openai_url": f"http://localhost:{port}/v1",
78
+ "provider": provider,
79
+ "compress": not no_compress,
80
+ "semantic": semantic,
81
+ }
82
+ print(json.dumps(data, indent=2))
83
+ else:
84
+ if running:
85
+ print(f"SLM proxy starting on :{port}...")
86
+ print(f" Anthropic surface: http://localhost:{port}")
87
+ print(f" OpenAI surface: http://localhost:{port}/v1")
88
+ print()
89
+ print(f"Set ANTHROPIC_BASE_URL=http://localhost:{port} before running Claude Code.")
90
+ print("Or run: slm wrap claude")
91
+ print()
92
+ print("Proxy ready.")
93
+ else:
94
+ print("Error: proxy failed to start. Check logs.", file=sys.stderr)
95
+ sys.exit(1)
@@ -691,6 +691,9 @@ class SLMConfig:
691
691
  mode = Mode(data.get("mode", "a"))
692
692
  llm_data = data.get("llm", {})
693
693
  emb_data = data.get("embedding", {})
694
+ # V3.5.9: read base_dir before constructing config so for_mode() builds
695
+ # db_path from the user's directory, not DEFAULT_BASE_DIR.
696
+ raw_base_dir = Path(data.get("base_dir", str(DEFAULT_BASE_DIR)))
694
697
  config = cls.for_mode(
695
698
  mode,
696
699
  llm_provider=llm_data.get("provider", ""),
@@ -703,6 +706,7 @@ class SLMConfig:
703
706
  embedding_deployment=emb_data.get("deployment_name", ""),
704
707
  embedding_model_name=emb_data.get("model_name", ""),
705
708
  embedding_dimension=int(emb_data.get("dimension", 0) or 0),
709
+ base_dir=raw_base_dir,
706
710
  )
707
711
  config.active_profile = data.get("active_profile", "default")
708
712
 
@@ -813,6 +817,7 @@ class SLMConfig:
813
817
  data = {
814
818
  "mode": effective_mode,
815
819
  "active_profile": self.active_profile,
820
+ "base_dir": str(self.base_dir), # V3.5.9: persist so load() can restore custom paths
816
821
  "llm": {
817
822
  "provider": self.llm.provider,
818
823
  "model": self.llm.model,
@@ -126,6 +126,10 @@ class MemoryEngine:
126
126
 
127
127
  if self._capabilities is Capabilities.FULL:
128
128
  self._init_heavy_layer()
129
+ else:
130
+ # V3.5.9: LIGHT mode — try to get embedder from running daemon so that
131
+ # memories stored via MCP have real embeddings (fixes PR #30 NULL embedder).
132
+ self._try_init_proxy()
129
133
 
130
134
  self._initialized = True
131
135
  logger.info(
@@ -187,6 +191,25 @@ class MemoryEngine:
187
191
  from superlocalmemory.learning.adaptive import AdaptiveLearner
188
192
  self._adaptive_learner = AdaptiveLearner(self._db)
189
193
 
194
+ def _try_init_proxy(self) -> None:
195
+ """V3.5.9: Attach McpEmbedderProxy when running in LIGHT mode.
196
+
197
+ If the daemon is reachable, the proxy delegates embed calls over HTTP
198
+ so that MCP-stored facts have real embeddings (fixes PR #30). Silently
199
+ skips if the daemon is not running — keyword recall still works.
200
+ """
201
+ try:
202
+ from superlocalmemory.core.mcp_embedder_proxy import McpEmbedderProxy
203
+ port = getattr(self._config, "daemon_port", 8765)
204
+ proxy = McpEmbedderProxy(port=port)
205
+ if proxy.is_available():
206
+ self._embedder = proxy
207
+ logger.info("MCP embedder proxy attached (daemon port %d)", port)
208
+ else:
209
+ logger.debug("Daemon not reachable — MCP will run without embedder")
210
+ except Exception as exc:
211
+ logger.debug("McpEmbedderProxy init skipped: %s", exc)
212
+
190
213
  def _init_heavy_layer(self) -> None:
191
214
  from superlocalmemory.llm.backbone import LLMBackbone
192
215
  from superlocalmemory.core.engine_wiring import (
@@ -0,0 +1,89 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com
4
+
5
+ """V3.5.9 — McpEmbedderProxy: lightweight embedder for the MCP (LIGHT) process.
6
+
7
+ Problem: MemoryEngine(Capabilities.LIGHT) skips _init_heavy_layer(), leaving
8
+ _embedder=None permanently. Any memory stored via MCP tools has NULL embeddings
9
+ → semantic search silently broken; health() lies, reporting unavailable.
10
+
11
+ Solution: a thin proxy that delegates embed_batch() to the running daemon via
12
+ its POST /api/embed endpoint over localhost HTTP. The daemon runs a FULL engine
13
+ with one real ONNX/Ollama worker. No second ONNX process is spawned.
14
+
15
+ Usage (engine.py LIGHT branch):
16
+ proxy = McpEmbedderProxy(port=8765)
17
+ if proxy.is_available():
18
+ engine._embedder = proxy
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _DEFAULT_TIMEOUT = 5.0 # seconds — fast enough for inline store() calls
28
+
29
+
30
+ class McpEmbedderProxy:
31
+ """Proxy embedder: MCP process → daemon's /api/embed over localhost HTTP."""
32
+
33
+ def __init__(self, port: int = 8765, timeout: float = _DEFAULT_TIMEOUT) -> None:
34
+ self._base_url = f"http://127.0.0.1:{port}"
35
+ self._timeout = timeout
36
+ self._available: bool | None = None # cached after first is_available() call
37
+
38
+ def is_available(self) -> bool:
39
+ """Ping daemon's embed endpoint. Cached after first success."""
40
+ if self._available is True:
41
+ return True
42
+ try:
43
+ import httpx
44
+ resp = httpx.get(f"{self._base_url}/api/v3/embed/ping", timeout=2.0)
45
+ self._available = resp.status_code == 200
46
+ except Exception:
47
+ self._available = False
48
+ return bool(self._available)
49
+
50
+ # -- Embedder interface (matches embeddings.py / ollama_embedder.py) ------
51
+
52
+ def embed(self, text: str) -> list[float] | None:
53
+ """Embed a single text via daemon. Returns None on any error."""
54
+ results = self.embed_batch([text])
55
+ return results[0] if results else None
56
+
57
+ def embed_batch(self, texts: list[str]) -> list[list[float] | None]:
58
+ """Embed a batch of texts via daemon's /api/embed endpoint."""
59
+ if not texts:
60
+ return []
61
+ try:
62
+ import httpx
63
+ resp = httpx.post(
64
+ f"{self._base_url}/api/v3/embed",
65
+ json={"texts": texts},
66
+ timeout=self._timeout,
67
+ )
68
+ resp.raise_for_status()
69
+ data = resp.json()
70
+ embeddings = data.get("embeddings", [])
71
+ # Pad with None if daemon returned fewer results than requested
72
+ while len(embeddings) < len(texts):
73
+ embeddings.append(None)
74
+ return embeddings
75
+ except Exception as exc:
76
+ logger.debug("McpEmbedderProxy.embed_batch failed: %s", exc)
77
+ return [None] * len(texts)
78
+
79
+ def compute_fisher_params(
80
+ self, embedding: list[float]
81
+ ) -> tuple[list[float] | None, list[float] | None]:
82
+ """Fisher-Rao params stay in daemon's FULL engine; proxy returns None.
83
+
84
+ The MCP LIGHT process stores facts with embedding=<vector> but
85
+ fisher_mean=None, fisher_variance=None. The daemon's consolidation
86
+ pass fills these in asynchronously — same behaviour as the
87
+ write-through remember path.
88
+ """
89
+ return None, None
@@ -233,17 +233,23 @@ class LLMBackbone:
233
233
  self, prompt: str, system: str, max_tokens: int, temperature: float,
234
234
  ) -> tuple[str, dict[str, str], dict]:
235
235
  messages = self._make_messages(system, prompt)
236
- headers = {
237
- "Authorization": f"Bearer {self._api_key}",
238
- "Content-Type": "application/json",
239
- }
236
+ # V3.5.9: Skip Authorization header when api_key is empty so unauthenticated
237
+ # local endpoints (llama.cpp, LM Studio, etc.) don't reject with HTTP 401.
238
+ headers = {"Content-Type": "application/json"}
239
+ if self._api_key:
240
+ headers["Authorization"] = f"Bearer {self._api_key}"
240
241
  payload = {
241
242
  "model": self._model,
242
243
  "messages": messages,
243
244
  "max_tokens": max_tokens,
244
245
  "temperature": temperature,
245
246
  }
247
+ # V3.5.9: If base_url is a bare base (e.g. "http://host:port/v1") without
248
+ # the /chat/completions path, append it — matching OpenAI SDK behaviour and
249
+ # fixing the /v1/v1 duplication reported in issue #29.
246
250
  url = self._base_url or _OPENAI_URL
251
+ if self._base_url and not self._base_url.rstrip("/").endswith("chat/completions"):
252
+ url = f"{self._base_url.rstrip('/')}/chat/completions"
247
253
  return url, headers, payload
248
254
 
249
255
  def _build_ollama(
@@ -278,5 +278,39 @@ _watchdog_thread = threading.Thread(target=_parent_watchdog, daemon=True, name="
278
278
  _watchdog_thread.start()
279
279
 
280
280
 
281
+ # V3.5.9: Stdin EOF monitor — complements the parent watchdog for the case where
282
+ # the IDE starts a new MCP session WITHOUT quitting the app. The old stdio pipe
283
+ # is abandoned (write-end closed) but the parent process stays alive, so the
284
+ # watchdog never fires. Without this, each IDE reconnect adds a zombie process
285
+ # (22 seen in production causing 12 GB swap on the M5 Pro).
286
+ #
287
+ # Uses kqueue(2) KQ_EV_EOF on macOS — fires when the write-end of the stdin pipe
288
+ # closes WITHOUT consuming any bytes, so it cannot race with FastMCP's asyncio
289
+ # stdin reader. On Linux (no kqueue), the watchdog alone provides coverage.
290
+ def _stdin_eof_monitor() -> None:
291
+ """Exit when the IDE closes our stdin pipe (kqueue — macOS only)."""
292
+ import select as _sel, os as _os_eof
293
+ _mlog = logging.getLogger(__name__ + ".stdin_monitor")
294
+ if not hasattr(_sel, "kqueue"):
295
+ return # Linux / non-macOS: watchdog covers process death
296
+ try:
297
+ fd = sys.stdin.fileno()
298
+ kq = _sel.kqueue()
299
+ ke = _sel.kevent(fd, filter=_sel.KQ_FILTER_READ, flags=_sel.KQ_EV_ADD | _sel.KQ_EV_EOF)
300
+ kq.control([ke], 0) # register without waiting
301
+ while True:
302
+ evs = kq.control(None, 4, 30.0) # 30 s poll — low cost
303
+ for ev in evs:
304
+ if ev.flags & _sel.KQ_EV_EOF:
305
+ _mlog.info("stdin write-end closed (kqueue EOF), self-terminating")
306
+ _os_eof._exit(0)
307
+ except Exception as exc:
308
+ _mlog.debug("stdin EOF monitor error: %s — watchdog will cover", exc)
309
+
310
+
311
+ _stdin_monitor_thread = threading.Thread(target=_stdin_eof_monitor, daemon=True, name="stdin-eof-monitor")
312
+ _stdin_monitor_thread.start()
313
+
314
+
281
315
  if __name__ == "__main__":
282
316
  server.run(transport="stdio")
@@ -161,9 +161,13 @@ def register_v3_tools(server, get_engine: Callable) -> None:
161
161
  "fact_count": fact_count,
162
162
  }
163
163
 
164
- # Embedding service
164
+ # Embedding service — distinguish real embedder from V3.5.9 daemon proxy
165
+ _emb = engine._embedder
166
+ from superlocalmemory.core.mcp_embedder_proxy import McpEmbedderProxy
167
+ _is_proxy = isinstance(_emb, McpEmbedderProxy)
165
168
  status["components"]["embedder"] = {
166
- "status": "ok" if engine._embedder else "unavailable",
169
+ "status": "ok" if _emb else "unavailable",
170
+ "source": "daemon_proxy" if _is_proxy else "local",
167
171
  "model": engine._config.embedding.model_name,
168
172
  }
169
173
 
@@ -0,0 +1,11 @@
1
+ Third-party components used by SLM v3.6 Optimize module:
2
+
3
+ 1. Headroom (Apache-2.0)
4
+ Copyright (c) 2024 Headroom contributors
5
+ Source: https://github.com/headroom/headroom
6
+ License: Apache-2.0
7
+
8
+ 2. LLMLingua-2 (MIT)
9
+ Copyright (c) 2024 Microsoft Corporation
10
+ Source: https://github.com/microsoft/LLMLingua
11
+ License: MIT
File without changes
@@ -0,0 +1,68 @@
1
+ """optimize/adapters — SDK adapters for OpenAI / Anthropic / LangChain.
2
+
3
+ SECURITY: No pickle anywhere. JSON+pydantic only (SEC-C-04, CWE-502).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger("superlocalmemory.optimize.adapters")
12
+
13
+
14
+ def withSLM(client: Any, *, tenant_id: str = "default") -> Any:
15
+ """Wrap any supported SDK client with SLM cache hooks.
16
+
17
+ FAIL-OPEN GUARANTEE:
18
+ - If Optimize is disabled (config.enabled == False) → return client unchanged.
19
+ - If cache is unavailable → return client unchanged, log WARNING.
20
+ - If client type unrecognized → return client unchanged, log WARNING.
21
+ """
22
+ try:
23
+ from superlocalmemory.optimize.config import get_optimize_config
24
+ from superlocalmemory.optimize.cache.manager import CacheManager
25
+ except ImportError as exc:
26
+ logger.warning("SLM Optimize not available (%s) — pass-through active", exc)
27
+ return client
28
+
29
+ config = get_optimize_config()
30
+ if not config.enabled:
31
+ return client # DEFAULT STATE — zero behavioral change
32
+
33
+ try:
34
+ cache_manager = CacheManager.get_instance()
35
+ except Exception as exc:
36
+ logger.warning("SLM cache unavailable (%s) — pass-through active", exc)
37
+ return client
38
+
39
+ # Anthropic (.messages.create)
40
+ if hasattr(client, "messages") and hasattr(client.messages, "create"):
41
+ try:
42
+ from superlocalmemory.optimize.adapters.anthropic_adapter import (
43
+ SLMAnthropicAdapter,
44
+ )
45
+ return SLMAnthropicAdapter(client, cache_manager, config, tenant_id)
46
+ except Exception as exc:
47
+ logger.warning("SLM Anthropic adapter init failed: %s", exc)
48
+ return client
49
+
50
+ # OpenAI (.chat.completions)
51
+ if hasattr(client, "chat") and hasattr(client.chat, "completions"):
52
+ try:
53
+ from superlocalmemory.optimize.adapters.openai_adapter import (
54
+ SLMOpenAIAdapter,
55
+ )
56
+ return SLMOpenAIAdapter(client, cache_manager, config, tenant_id)
57
+ except Exception as exc:
58
+ logger.warning("SLM OpenAI adapter init failed: %s", exc)
59
+ return client
60
+
61
+ logger.warning(
62
+ "withSLM: unrecognized client type %s — pass-through active",
63
+ type(client).__name__,
64
+ )
65
+ return client
66
+
67
+
68
+ __all__ = ["withSLM"]