superlocalmemory 3.7.1 → 3.7.3

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 (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +16 -8
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/requirements.txt +1 -1
  6. package/plugin/scripts/slm-launch +11 -3
  7. package/plugin/scripts/slm-launch.bat +8 -2
  8. package/plugin-src/manifest.json +1 -1
  9. package/plugin-src/requirements.txt +1 -1
  10. package/plugin-src/scripts/slm-launch +11 -3
  11. package/plugin-src/scripts/slm-launch.bat +8 -2
  12. package/pyproject.toml +1 -1
  13. package/src/superlocalmemory/__init__.py +1 -1
  14. package/src/superlocalmemory/cli/main.py +2 -2
  15. package/src/superlocalmemory/cli/scale_engine_cmd.py +13 -1
  16. package/src/superlocalmemory/core/backend_orchestrator.py +15 -0
  17. package/src/superlocalmemory/core/embedding_worker.py +7 -1
  18. package/src/superlocalmemory/core/embeddings.py +8 -0
  19. package/src/superlocalmemory/core/engine.py +1 -3
  20. package/src/superlocalmemory/core/engine_wiring.py +0 -5
  21. package/src/superlocalmemory/core/ram_lock.py +42 -4
  22. package/src/superlocalmemory/core/scale_engine.py +450 -35
  23. package/src/superlocalmemory/evolution/budget.py +43 -8
  24. package/src/superlocalmemory/hooks/claude_code_hooks.py +2 -1
  25. package/src/superlocalmemory/hooks/context_payload.py +2 -1
  26. package/src/superlocalmemory/mcp/http_transport.py +2 -1
  27. package/src/superlocalmemory/mcp/tools_core.py +46 -29
  28. package/src/superlocalmemory/mesh/broker.py +111 -61
  29. package/src/superlocalmemory/optimize/proxy/server.py +2 -1
  30. package/src/superlocalmemory/retrieval/spreading_activation.py +53 -3
  31. package/src/superlocalmemory/server/routes/brain.py +2 -1
  32. package/src/superlocalmemory/server/unified_daemon.py +24 -7
@@ -20,7 +20,6 @@ Author: Varun Pratap Bhardwaj / Qualixar
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
- import fcntl
24
23
  import logging
25
24
  import os
26
25
  import sqlite3
@@ -35,8 +34,44 @@ from superlocalmemory.core.security_primitives import safe_resolve_identifier
35
34
 
36
35
  logger = logging.getLogger(__name__)
37
36
 
37
+ try: # POSIX advisory locks (macOS/Linux)
38
+ import fcntl as _fcntl
39
+ except ImportError: # Windows has no fcntl; use its stdlib byte-range lock.
40
+ _fcntl = None
38
41
 
39
- # M-P-07: path-component hints for filesystems where ``fcntl.flock`` is
42
+
43
+ def _acquire_profile_lock(fd: int) -> None:
44
+ """Acquire a non-blocking cross-process lock on every supported OS."""
45
+ if _fcntl is not None:
46
+ _fcntl.flock(fd, _fcntl.LOCK_EX | _fcntl.LOCK_NB)
47
+ return
48
+
49
+ import msvcrt
50
+
51
+ # ``msvcrt.locking`` cannot lock an empty file. Reserve one marker byte;
52
+ # the file content is diagnostic only and never a correctness signal.
53
+ if os.fstat(fd).st_size == 0:
54
+ os.write(fd, b"\0")
55
+ os.lseek(fd, 0, os.SEEK_SET)
56
+ try:
57
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
58
+ except OSError as exc:
59
+ raise BlockingIOError("evolution profile lock is held") from exc
60
+
61
+
62
+ def _release_profile_lock(fd: int) -> None:
63
+ """Release the lock acquired by :func:`_acquire_profile_lock`."""
64
+ if _fcntl is not None:
65
+ _fcntl.flock(fd, _fcntl.LOCK_UN)
66
+ return
67
+
68
+ import msvcrt
69
+
70
+ os.lseek(fd, 0, os.SEEK_SET)
71
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
72
+
73
+
74
+ # M-P-07: path-component hints for filesystems where advisory locking is
40
75
  # known to degrade to a no-op. We do NOT abort — some users accept the
41
76
  # risk — but we emit a one-shot warning so a double-cycle is at least
42
77
  # attributable to a known root cause. If/when we ship a threading-only
@@ -127,7 +162,7 @@ class EvolutionBudget:
127
162
  )
128
163
  self._lock_path = safe_stem.with_suffix(".lock")
129
164
  # M-P-07: warn once per lock-path when we detect the lock sits on
130
- # a known sync-backed filesystem. ``fcntl.flock`` silently
165
+ # a known sync-backed filesystem. Advisory locking silently
131
166
  # degrades on iCloud/OneDrive/Dropbox — two concurrent cycles
132
167
  # would each acquire and burn LLM budget. Documentation-only for
133
168
  # now; a future release may refuse to run until the lock_dir is
@@ -141,7 +176,7 @@ class EvolutionBudget:
141
176
  if key not in _WARNED_SYNC_PATHS:
142
177
  _WARNED_SYNC_PATHS.add(key)
143
178
  logger.warning(
144
- "evolution lock at %s lives under %r — fcntl.flock "
179
+ "evolution lock at %s lives under %r — advisory locking "
145
180
  "may silently no-op on sync-backed filesystems. "
146
181
  "Concurrent cycles could double-bill LLM cost. Move "
147
182
  "the lock_dir off the sync root to make single-flight "
@@ -258,12 +293,12 @@ class EvolutionBudget:
258
293
  f"cap={MAX_CYCLES_PER_DAY}",
259
294
  )
260
295
 
261
- # Single-flight lock (non-blocking flock). A second concurrent
296
+ # Single-flight lock (non-blocking advisory lock). A second concurrent
262
297
  # acquire raises BlockingIOError — surface as BudgetExhausted.
263
298
  fd = os.open(str(self._lock_path), os.O_CREAT | os.O_RDWR, 0o600)
264
299
  try:
265
300
  try:
266
- fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
301
+ _acquire_profile_lock(fd)
267
302
  except BlockingIOError as e:
268
303
  os.close(fd)
269
304
  raise BudgetExhausted(
@@ -296,7 +331,7 @@ class EvolutionBudget:
296
331
  # Release lock before propagating — a failed cycle-record should
297
332
  # not leave the lock dangling.
298
333
  try:
299
- fcntl.flock(fd, fcntl.LOCK_UN)
334
+ _release_profile_lock(fd)
300
335
  finally:
301
336
  os.close(fd)
302
337
  self._lock_fd = None
@@ -312,7 +347,7 @@ class EvolutionBudget:
312
347
  finally:
313
348
  self._cycle_start_mono = None
314
349
  try:
315
- fcntl.flock(fd, fcntl.LOCK_UN)
350
+ _release_profile_lock(fd)
316
351
  finally:
317
352
  try:
318
353
  os.close(fd)
@@ -28,6 +28,7 @@ import sys
28
28
  import tempfile
29
29
  from pathlib import Path
30
30
 
31
+ from superlocalmemory import __version__
31
32
  from superlocalmemory.infra.data_root import canonical_data_root
32
33
  from superlocalmemory.infra.data_root import state_path as runtime_state_path
33
34
 
@@ -40,7 +41,7 @@ _DEFAULT_DISABLED_FILE = _DEFAULT_VERSION_DIR / ".hooks-disabled"
40
41
  VERSION_DIR = _DEFAULT_VERSION_DIR
41
42
  VERSION_FILE = _DEFAULT_VERSION_FILE
42
43
  DISABLED_FILE = _DEFAULT_DISABLED_FILE
43
- HOOKS_VERSION = "3.7.1"
44
+ HOOKS_VERSION = __version__
44
45
 
45
46
  # Cross-platform temp dir and backwards-compatible marker overrides. Runtime
46
47
  # defaults are root-namespaced and resolved when hook definitions are built.
@@ -19,10 +19,11 @@ from datetime import datetime, timezone
19
19
  from pathlib import Path
20
20
  from typing import Callable, Iterable
21
21
 
22
+ from superlocalmemory import __version__
22
23
  from superlocalmemory.core.security_primitives import redact_secrets
23
24
 
24
25
 
25
- VERSION = "3.7.1"
26
+ VERSION = __version__
26
27
  DEFAULT_TOP_K = 10
27
28
  DEFAULT_DECISIONS_K = 5
28
29
  DEFAULT_MEMORIES_K = 10
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
  from mcp.server.fastmcp import FastMCP
17
17
  from sse_starlette.sse import EventSourceResponse
18
18
  from starlette.types import Receive, Scope, Send
19
+ from superlocalmemory import __version__
19
20
 
20
21
 
21
22
  class ClosingEventSourceResponse(EventSourceResponse):
@@ -40,7 +41,7 @@ def install_streamable_http_resource_guard() -> None:
40
41
  class SLMFastMCP(FastMCP):
41
42
  """FastMCP with SLM release identity and deterministic SSE cleanup."""
42
43
 
43
- def __init__(self, *args, product_version: str = "3.7.1", **kwargs) -> None:
44
+ def __init__(self, *args, product_version: str = __version__, **kwargs) -> None:
44
45
  super().__init__(*args, **kwargs)
45
46
  # FastMCP delegates the initialize response to the low-level MCP
46
47
  # server. Without an explicit value it reports the installed ``mcp``
@@ -149,35 +149,52 @@ def register_core_tools(server, get_engine: Callable) -> None:
149
149
  # is_daemon_running() and daemon_request() both use blocking urllib
150
150
  # against the same uvicorn server — run in threads so the MCP
151
151
  # event loop stays unblocked (#34 class bug).
152
- if await _asyncio.to_thread(is_daemon_running):
153
- resp = await _asyncio.to_thread(daemon_request, "POST", "/remember", {
154
- "content": content, "tags": tags, "metadata": meta,
155
- "scope": scope, "shared_with": _shared_list,
156
- "session_id": session_id,
157
- "idempotency_key": effective_idempotency_key or None,
158
- })
159
- if resp and (resp.get("fact_ids") is not None or resp.get("ok")):
160
- fids = resp.get("fact_ids") or []
161
- materialization_state = resp.get("materialization_state")
162
- if materialization_state is None:
163
- materialization_state = (
164
- "complete" if resp.get("status") == "stored" else "queryable"
165
- )
166
- pending = materialization_state != "complete"
167
- return {
168
- "success": True,
169
- "fact_ids": fids,
170
- "count": int(resp.get("count", len(fids))),
171
- "pending": pending,
172
- "pending_id": resp.get("pending_id") if pending else None,
173
- "operation_id": resp.get("operation_id"),
174
- "materialization_state": materialization_state,
175
- "message": (
176
- "Stored through canonical daemon ingestion."
177
- if not pending
178
- else "Queryable now; canonical enrichment is still running."
179
- ),
180
- }
152
+ daemon_owned = await _asyncio.to_thread(is_daemon_running)
153
+ if daemon_owned:
154
+ # A positively identified daemon owns this database. Never
155
+ # spawn a WorkerPool writer after a transient daemon failure:
156
+ # that creates the competing writers which SQLite WAL cannot
157
+ # support. Retry the canonical path, then return an explicit
158
+ # retryable result to the MCP client.
159
+ for attempt in range(3):
160
+ resp = await _asyncio.to_thread(daemon_request, "POST", "/remember", {
161
+ "content": content, "tags": tags, "metadata": meta,
162
+ "scope": scope, "shared_with": _shared_list,
163
+ "session_id": session_id,
164
+ "idempotency_key": effective_idempotency_key or None,
165
+ })
166
+ if resp and (resp.get("fact_ids") is not None or resp.get("ok")):
167
+ fids = resp.get("fact_ids") or []
168
+ materialization_state = resp.get("materialization_state")
169
+ if materialization_state is None:
170
+ materialization_state = (
171
+ "complete" if resp.get("status") == "stored" else "queryable"
172
+ )
173
+ pending = materialization_state != "complete"
174
+ return {
175
+ "success": True,
176
+ "fact_ids": fids,
177
+ "count": int(resp.get("count", len(fids))),
178
+ "pending": pending,
179
+ "pending_id": resp.get("pending_id") if pending else None,
180
+ "operation_id": resp.get("operation_id"),
181
+ "materialization_state": materialization_state,
182
+ "message": (
183
+ "Stored through canonical daemon ingestion."
184
+ if not pending
185
+ else "Queryable now; canonical enrichment is still running."
186
+ ),
187
+ }
188
+ if attempt < 2:
189
+ await _asyncio.sleep(0.05 * (attempt + 1))
190
+ return {
191
+ "success": False,
192
+ "retryable": True,
193
+ "error": (
194
+ "Canonical daemon is temporarily unavailable; retry the "
195
+ "same remember operation without starting a second writer."
196
+ ),
197
+ }
181
198
  except Exception as dexc:
182
199
  logger.debug("MCP remember via daemon failed, pending fallback: %s", dexc)
183
200
 
@@ -19,7 +19,7 @@ import time
19
19
  import uuid
20
20
  from datetime import datetime, timezone
21
21
  from pathlib import Path
22
- from typing import Any
22
+ from typing import Any, Callable, TypeVar
23
23
 
24
24
  logger = logging.getLogger("superlocalmemory.mesh")
25
25
  import os as _os
@@ -38,6 +38,11 @@ MAX_MESSAGE_SIZE = 4096 # 4KB cap — mesh messages are notifications, not data
38
38
  MESSAGE_TTL_HOURS = 48 # Offline messages expire after 48h
39
39
  MAX_QUEUED_PER_TARGET = 50 # Max unread messages per broadcast/project target
40
40
 
41
+ _T = TypeVar("_T")
42
+ _WRITE_RETRY_ATTEMPTS = 6
43
+ _WRITE_RETRY_BASE_SECONDS = 0.025
44
+ _WRITE_BUSY_TIMEOUT_MS = 250
45
+
41
46
 
42
47
  class MeshBroker:
43
48
  """Lightweight mesh broker for SLM's unified daemon.
@@ -109,22 +114,64 @@ class MeshBroker:
109
114
  # -- Connection helper --
110
115
 
111
116
  def _conn(self) -> sqlite3.Connection:
112
- conn = sqlite3.connect(self._db_path)
113
- conn.execute("PRAGMA journal_mode=WAL")
114
- conn.execute("PRAGMA busy_timeout=5000")
117
+ # WAL is configured during database initialization and persists with the
118
+ # database. Reissuing journal_mode=WAL for every short-lived mesh
119
+ # connection is itself a schema-level write that can contend with the
120
+ # daemon. Mesh writes below use bounded whole-transaction retries.
121
+ conn = sqlite3.connect(
122
+ self._db_path,
123
+ timeout=_WRITE_BUSY_TIMEOUT_MS / 1000,
124
+ )
125
+ conn.execute(f"PRAGMA busy_timeout={_WRITE_BUSY_TIMEOUT_MS}")
115
126
  conn.row_factory = sqlite3.Row
116
127
  return conn
117
128
 
129
+ @staticmethod
130
+ def _is_transient_lock(exc: sqlite3.OperationalError) -> bool:
131
+ message = str(exc).lower()
132
+ return "database is locked" in message or "database is busy" in message
133
+
134
+ def _write_with_retry(
135
+ self,
136
+ operation: Callable[[sqlite3.Connection], _T],
137
+ ) -> _T:
138
+ """Run one idempotent mesh mutation with a bounded SQLite retry budget.
139
+
140
+ SQLite WAL lets reads continue while a write is in progress, but it
141
+ still permits a single writer. Retrying the entire short transaction on
142
+ a fresh connection avoids leaking a transient writer collision to an
143
+ agent heartbeat or mesh command.
144
+ """
145
+ last_error: sqlite3.OperationalError | None = None
146
+ for attempt in range(_WRITE_RETRY_ATTEMPTS):
147
+ conn = self._conn()
148
+ try:
149
+ return operation(conn)
150
+ except sqlite3.OperationalError as exc:
151
+ if not self._is_transient_lock(exc):
152
+ raise
153
+ last_error = exc
154
+ try:
155
+ conn.rollback()
156
+ except sqlite3.Error:
157
+ pass
158
+ finally:
159
+ conn.close()
160
+
161
+ if attempt < _WRITE_RETRY_ATTEMPTS - 1:
162
+ time.sleep(_WRITE_RETRY_BASE_SECONDS * (2 ** attempt))
163
+
164
+ assert last_error is not None
165
+ raise last_error
166
+
118
167
  # -- Peers --
119
168
 
120
169
  def register_peer(self, session_id: str, summary: str = "",
121
170
  host: str = "", port: int = 0,
122
171
  project_path: str = "", agent_type: str = "unknown") -> dict:
123
- conn = self._conn()
124
- try:
172
+ def _register(conn: sqlite3.Connection) -> dict:
125
173
  now = datetime.now(timezone.utc).isoformat()
126
- if not host:
127
- host = self._host
174
+ effective_host = host or self._host
128
175
  # Idempotent: update if same session_id exists
129
176
  existing = conn.execute(
130
177
  "SELECT peer_id FROM mesh_peers WHERE session_id = ?",
@@ -135,7 +182,7 @@ class MeshBroker:
135
182
  conn.execute(
136
183
  "UPDATE mesh_peers SET summary=?, host=?, port=?, last_heartbeat=?, "
137
184
  "status='active', project_path=?, agent_type=? WHERE peer_id=?",
138
- (summary, host, port, now, project_path, agent_type, peer_id),
185
+ (summary, effective_host, port, now, project_path, agent_type, peer_id),
139
186
  )
140
187
  else:
141
188
  peer_id = str(uuid.uuid4())[:12]
@@ -143,7 +190,7 @@ class MeshBroker:
143
190
  "INSERT INTO mesh_peers (peer_id, session_id, summary, status, host, port, "
144
191
  "registered_at, last_heartbeat, project_path, agent_type) "
145
192
  "VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)",
146
- (peer_id, session_id, summary, host, port, now, now, project_path, agent_type),
193
+ (peer_id, session_id, summary, effective_host, port, now, now, project_path, agent_type),
147
194
  )
148
195
  self._log_event(conn, "peer_registered", peer_id, {
149
196
  "session_id": session_id, "project_path": project_path,
@@ -153,12 +200,11 @@ class MeshBroker:
153
200
  # v3.4.6: Deliver pending broadcast/project messages on registration
154
201
  pending = self._get_pending_for_peer(conn, peer_id, project_path)
155
202
  return {"peer_id": peer_id, "ok": True, "pending_messages": len(pending)}
156
- finally:
157
- conn.close()
203
+
204
+ return self._write_with_retry(_register)
158
205
 
159
206
  def deregister_peer(self, peer_id: str) -> dict:
160
- conn = self._conn()
161
- try:
207
+ def _deregister(conn: sqlite3.Connection) -> dict:
162
208
  row = conn.execute("SELECT 1 FROM mesh_peers WHERE peer_id=?", (peer_id,)).fetchone()
163
209
  if not row:
164
210
  return {"ok": False, "error": "peer not found"}
@@ -166,12 +212,11 @@ class MeshBroker:
166
212
  self._log_event(conn, "peer_deregistered", peer_id)
167
213
  conn.commit()
168
214
  return {"ok": True}
169
- finally:
170
- conn.close()
215
+
216
+ return self._write_with_retry(_deregister)
171
217
 
172
218
  def heartbeat(self, peer_id: str) -> dict:
173
- conn = self._conn()
174
- try:
219
+ def _heartbeat(conn: sqlite3.Connection) -> dict:
175
220
  now = datetime.now(timezone.utc).isoformat()
176
221
  cursor = conn.execute(
177
222
  "UPDATE mesh_peers SET last_heartbeat=?, status='active' WHERE peer_id=?",
@@ -181,12 +226,11 @@ class MeshBroker:
181
226
  return {"ok": False, "error": "peer not found"}
182
227
  conn.commit()
183
228
  return {"ok": True}
184
- finally:
185
- conn.close()
229
+
230
+ return self._write_with_retry(_heartbeat)
186
231
 
187
232
  def update_summary(self, peer_id: str, summary: str) -> dict:
188
- conn = self._conn()
189
- try:
233
+ def _update_summary(conn: sqlite3.Connection) -> dict:
190
234
  cursor = conn.execute(
191
235
  "UPDATE mesh_peers SET summary=? WHERE peer_id=?",
192
236
  (summary, peer_id),
@@ -195,8 +239,8 @@ class MeshBroker:
195
239
  return {"ok": False, "error": "peer not found"}
196
240
  conn.commit()
197
241
  return {"ok": True}
198
- finally:
199
- conn.close()
242
+
243
+ return self._write_with_retry(_update_summary)
200
244
 
201
245
  def list_peers(self) -> list[dict]:
202
246
  conn = self._conn()
@@ -219,8 +263,19 @@ class MeshBroker:
219
263
  return {"ok": False, "error": f"message too large ({len(content)} bytes, max {MAX_MESSAGE_SIZE}). "
220
264
  "Mesh messages are notifications — reference a file path instead."}
221
265
 
222
- conn = self._conn()
223
- try:
266
+ # Remote delivery is an external side effect, so do it outside the
267
+ # retry envelope. Local writes below are retried as a whole short
268
+ # transaction when another daemon-owned operation has SQLite's writer.
269
+ if to_peer in self._remote_peers and self._sync_client:
270
+ return self._sync_client.send_to_remote(to_peer, {
271
+ "from_peer": from_peer,
272
+ "to": to_peer,
273
+ "content": content,
274
+ "type": msg_type,
275
+ })
276
+
277
+ def _send(conn: sqlite3.Connection) -> dict:
278
+ nonlocal to_peer, project_path
224
279
  now = datetime.now(timezone.utc).isoformat()
225
280
  expires_at = self._compute_expires(now)
226
281
 
@@ -233,14 +288,6 @@ class MeshBroker:
233
288
  to_peer = "project"
234
289
  else:
235
290
  target_type = "peer"
236
- # Check if this is a remote peer — proxy to remote SLM
237
- if to_peer in self._remote_peers and self._sync_client:
238
- return self._sync_client.send_to_remote(to_peer, {
239
- "from_peer": from_peer,
240
- "to": to_peer,
241
- "content": content,
242
- "type": msg_type,
243
- })
244
291
  # Verify recipient exists for direct messages
245
292
  if not conn.execute("SELECT 1 FROM mesh_peers WHERE peer_id=?", (to_peer,)).fetchone():
246
293
  return {"ok": False, "error": "recipient peer not found"}
@@ -272,8 +319,8 @@ class MeshBroker:
272
319
  conn.commit()
273
320
  return {"ok": True, "id": cursor.lastrowid, "target_type": target_type,
274
321
  "expires_at": expires_at}
275
- finally:
276
- conn.close()
322
+
323
+ return self._write_with_retry(_send)
277
324
 
278
325
  def get_inbox(self, peer_id: str, project_path: str = "") -> list[dict]:
279
326
  """Get all messages for this peer: direct + broadcast + project."""
@@ -331,8 +378,7 @@ class MeshBroker:
331
378
  conn.close()
332
379
 
333
380
  def mark_read(self, peer_id: str, message_ids: list[int]) -> dict:
334
- conn = self._conn()
335
- try:
381
+ def _mark_read(conn: sqlite3.Connection) -> dict:
336
382
  now = datetime.now(timezone.utc).isoformat()
337
383
  for msg_id in message_ids:
338
384
  # Check if this is a direct message or broadcast/project
@@ -356,8 +402,8 @@ class MeshBroker:
356
402
  )
357
403
  conn.commit()
358
404
  return {"ok": True, "marked": len(message_ids)}
359
- finally:
360
- conn.close()
405
+
406
+ return self._write_with_retry(_mark_read)
361
407
 
362
408
  # -- State --
363
409
 
@@ -370,8 +416,7 @@ class MeshBroker:
370
416
  conn.close()
371
417
 
372
418
  def set_state(self, key: str, value: str, set_by: str) -> dict:
373
- conn = self._conn()
374
- try:
419
+ def _set_state(conn: sqlite3.Connection) -> dict:
375
420
  now = datetime.now(timezone.utc).isoformat()
376
421
  conn.execute(
377
422
  "INSERT INTO mesh_state (key, value, set_by, updated_at) VALUES (?, ?, ?, ?) "
@@ -380,8 +425,8 @@ class MeshBroker:
380
425
  )
381
426
  conn.commit()
382
427
  return {"ok": True}
383
- finally:
384
- conn.close()
428
+
429
+ return self._write_with_retry(_set_state)
385
430
 
386
431
  def get_state_key(self, key: str) -> dict | None:
387
432
  conn = self._conn()
@@ -396,8 +441,23 @@ class MeshBroker:
396
441
  # -- Locks --
397
442
 
398
443
  def lock_action(self, file_path: str, locked_by: str, action: str) -> dict:
399
- conn = self._conn()
400
- try:
444
+ if action == "query":
445
+ conn = self._conn()
446
+ try:
447
+ row = conn.execute(
448
+ "SELECT locked_by, locked_at FROM mesh_locks WHERE file_path=?",
449
+ (file_path,),
450
+ ).fetchone()
451
+ if row:
452
+ return {"locked": True, "by": row["locked_by"], "since": row["locked_at"]}
453
+ return {"locked": False}
454
+ finally:
455
+ conn.close()
456
+
457
+ if action not in {"acquire", "release"}:
458
+ return {"ok": False, "error": f"unknown action: {action}"}
459
+
460
+ def _lock_action(conn: sqlite3.Connection) -> dict:
401
461
  now = datetime.now(timezone.utc).isoformat()
402
462
 
403
463
  if action == "acquire":
@@ -429,18 +489,9 @@ class MeshBroker:
429
489
  return {"ok": False, "action": "not_released",
430
490
  "error": "no lock held by this peer for that file"}
431
491
 
432
- elif action == "query":
433
- row = conn.execute(
434
- "SELECT locked_by, locked_at FROM mesh_locks WHERE file_path=?",
435
- (file_path,),
436
- ).fetchone()
437
- if row:
438
- return {"locked": True, "by": row["locked_by"], "since": row["locked_at"]}
439
- return {"locked": False}
492
+ raise AssertionError("validated action was not handled")
440
493
 
441
- return {"ok": False, "error": f"unknown action: {action}"}
442
- finally:
443
- conn.close()
494
+ return self._write_with_retry(_lock_action)
444
495
 
445
496
  # -- Helpers (v3.4.6) --
446
497
 
@@ -527,8 +578,7 @@ class MeshBroker:
527
578
  logger.debug("Mesh cleanup error: %s", exc)
528
579
 
529
580
  def _run_cleanup(self) -> None:
530
- conn = self._conn()
531
- try:
581
+ def _cleanup(conn: sqlite3.Connection) -> None:
532
582
  now = datetime.now(timezone.utc)
533
583
  now_iso = now.isoformat()
534
584
  # Mark stale peers (no heartbeat for 5 min)
@@ -572,5 +622,5 @@ class MeshBroker:
572
622
  (now_iso,),
573
623
  )
574
624
  conn.commit()
575
- finally:
576
- conn.close()
625
+
626
+ self._write_with_retry(_cleanup)
@@ -12,12 +12,13 @@ from fastapi import APIRouter
12
12
  from fastapi.requests import Request
13
13
  from fastapi.responses import Response
14
14
 
15
+ from superlocalmemory import __version__
15
16
  from superlocalmemory.optimize.config.schema import OptimizeConfig
16
17
  from superlocalmemory.optimize.proxy.lifecycle import HookChain
17
18
 
18
19
  logger = logging.getLogger("slm.optimize.proxy")
19
20
 
20
- _PROXY_VERSION = "3.7.1"
21
+ _PROXY_VERSION = __version__
21
22
  _REQUEST_TIMEOUT_S = 300.0
22
23
  _CONNECT_TIMEOUT_S = 10.0
23
24
  _MAX_CONNECTIONS = 100
@@ -93,7 +93,7 @@ class SpreadingActivation:
93
93
  def __init__(
94
94
  self,
95
95
  db: Any,
96
- vector_store: Any,
96
+ vector_store: Any | None,
97
97
  config: SpreadingActivationConfig | None = None,
98
98
  ) -> None:
99
99
  self._db = db
@@ -122,8 +122,11 @@ class SpreadingActivation:
122
122
  include_shared = bool(getattr(self, "include_shared", False))
123
123
  try:
124
124
  # Step 0: Get seed nodes from VectorStore KNN
125
- seed_results = self._vector_store.search(
126
- query, top_k=self._config.top_m, profile_id=profile_id,
125
+ seed_results = self._seed_search(
126
+ query,
127
+ profile_id,
128
+ include_global=include_global,
129
+ include_shared=include_shared,
127
130
  )
128
131
  # Owner-partitioned vector indexes cannot discover opted-in peers.
129
132
  # Add visible external embeddings with the same cosine seed signal.
@@ -216,6 +219,53 @@ class SpreadingActivation:
216
219
  )
217
220
  return []
218
221
 
222
+ def _seed_search(
223
+ self,
224
+ query: Any,
225
+ profile_id: str,
226
+ *,
227
+ include_global: bool,
228
+ include_shared: bool,
229
+ ) -> list[tuple[str, float]]:
230
+ """Return bounded semantic graph seeds from vec0 or canonical SQLite.
231
+
232
+ sqlite-vec is an acceleration projection, not a prerequisite for the
233
+ graph retrieval layer. When it is unavailable on a platform, use the
234
+ canonical stored embeddings and the same cosine signal as the
235
+ cross-profile supplement below. This keeps spreading activation
236
+ present and truthful rather than silently removing a retrieval layer.
237
+ """
238
+ if self._vector_store is not None and getattr(
239
+ self._vector_store, "available", False,
240
+ ):
241
+ return self._vector_store.search(
242
+ query, top_k=self._config.top_m, profile_id=profile_id,
243
+ )
244
+
245
+ q_vec = np.array(query, dtype=np.float32)
246
+ q_norm = float(np.linalg.norm(q_vec))
247
+ if q_norm <= 1e-8:
248
+ return []
249
+ facts = self._db.get_all_facts(
250
+ profile_id,
251
+ include_global=include_global,
252
+ include_shared=include_shared,
253
+ )
254
+ scored: list[tuple[str, float]] = []
255
+ for fact in facts:
256
+ embedding = getattr(fact, "embedding", None)
257
+ if embedding is None:
258
+ continue
259
+ fact_vec = np.array(embedding, dtype=np.float32)
260
+ if fact_vec.shape != q_vec.shape:
261
+ continue
262
+ denominator = q_norm * float(np.linalg.norm(fact_vec))
263
+ if denominator <= 1e-8:
264
+ continue
265
+ score = (float(np.dot(q_vec, fact_vec) / denominator) + 1.0) / 2.0
266
+ scored.append((fact.fact_id, score))
267
+ return sorted(scored, key=lambda item: item[1], reverse=True)[:self._config.top_m]
268
+
219
269
  def _propagate(
220
270
  self,
221
271
  seeds: list[tuple[str, float]],
@@ -43,6 +43,7 @@ from pathlib import Path
43
43
  from typing import Any
44
44
 
45
45
  from fastapi import APIRouter, Depends, HTTPException, Request
46
+ from superlocalmemory import __version__
46
47
 
47
48
  from superlocalmemory.core.security_primitives import (
48
49
  redact_secrets,
@@ -64,7 +65,7 @@ router = APIRouter(prefix="/api/v3", tags=["brain"])
64
65
  # LLD-03 v2 stratum space = 4 query types × 3 entity bins × 4 time buckets.
65
66
  _STRATA_TOTAL: int = 48
66
67
 
67
- _VERSION: str = "3.7.1"
68
+ _VERSION: str = __version__
68
69
 
69
70
  # Banned metric names (LLD-04 U4). Kept as a tuple for grep visibility;
70
71
  # the source-level test asserts we don't accidentally reintroduce them.