superlocalmemory 3.0.32 → 3.0.33

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.0.32",
3
+ "version": "3.0.33",
4
4
  "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
5
5
  "keywords": [
6
6
  "ai-memory",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.0.32"
3
+ version = "3.0.33"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -5,13 +5,14 @@
5
5
  """SuperLocalMemory V3 — Database Manager.
6
6
 
7
7
  SQLite with WAL, profile-scoped CRUD, FTS5 search, BM25 persistence.
8
- All connections use try/finally. Only ``except sqlite3.Error``.
8
+ Concurrent-safe: WAL mode + busy_timeout + retry on SQLITE_BUSY.
9
+ Multiple processes (MCP, CLI, integrations) can read/write safely.
9
10
 
10
11
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
12
  """
12
13
  from __future__ import annotations
13
14
 
14
- import json, logging, sqlite3, threading
15
+ import json, logging, sqlite3, threading, time
15
16
  from contextlib import contextmanager
16
17
  from pathlib import Path
17
18
  from types import ModuleType
@@ -37,11 +38,22 @@ def _jd(val: Any) -> str | None:
37
38
  return json.dumps(val) if val is not None else None
38
39
 
39
40
 
41
+ _BUSY_TIMEOUT_MS = 10_000 # 10 seconds — wait for other writers
42
+ _MAX_RETRIES = 5 # retry on transient SQLITE_BUSY
43
+ _RETRY_BASE_DELAY = 0.1 # seconds — exponential backoff base
44
+
45
+
40
46
  class DatabaseManager:
41
- """Thread-safe SQLite manager with WAL, profile isolation, and FTS5.
47
+ """Concurrent-safe SQLite manager with WAL, profile isolation, and FTS5.
48
+
49
+ Designed for multi-process access: MCP server, CLI, LangChain, CrewAI,
50
+ and other integrations can all read/write the same database safely.
42
51
 
43
- Per-call connections outside transactions; shared connection inside
44
- a ``transaction()`` block. Thread-safe via threading.Lock.
52
+ Concurrency model:
53
+ - WAL mode: readers never block writers, writers never block readers
54
+ - busy_timeout: writers wait up to 10s for other writers instead of failing
55
+ - Retry with backoff: transient SQLITE_BUSY errors are retried automatically
56
+ - Per-call connections: no shared state between processes
45
57
  """
46
58
 
47
59
  def __init__(self, db_path: str | Path) -> None:
@@ -55,6 +67,7 @@ class DatabaseManager:
55
67
  conn = sqlite3.connect(str(self.db_path))
56
68
  try:
57
69
  conn.execute("PRAGMA journal_mode=WAL")
70
+ conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
58
71
  conn.execute("PRAGMA foreign_keys=ON")
59
72
  conn.commit()
60
73
  finally:
@@ -62,9 +75,8 @@ class DatabaseManager:
62
75
 
63
76
  def initialize(self, schema_module: ModuleType) -> None:
64
77
  """Create all tables. *schema_module* must expose ``create_all_tables(conn)``."""
65
- conn = sqlite3.connect(str(self.db_path))
78
+ conn = self._connect()
66
79
  try:
67
- conn.execute("PRAGMA foreign_keys=ON")
68
80
  schema_module.create_all_tables(conn)
69
81
  conn.commit()
70
82
  logger.info("Schema initialized at %s", self.db_path)
@@ -81,8 +93,9 @@ class DatabaseManager:
81
93
  self.close()
82
94
 
83
95
  def _connect(self) -> sqlite3.Connection:
84
- conn = sqlite3.connect(str(self.db_path))
96
+ conn = sqlite3.connect(str(self.db_path), timeout=_BUSY_TIMEOUT_MS / 1000)
85
97
  conn.row_factory = sqlite3.Row
98
+ conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
86
99
  conn.execute("PRAGMA foreign_keys=ON")
87
100
  return conn
88
101
 
@@ -103,16 +116,36 @@ class DatabaseManager:
103
116
  conn.close()
104
117
 
105
118
  def execute(self, sql: str, params: tuple[Any, ...] = ()) -> list[sqlite3.Row]:
106
- """Execute SQL. Uses shared conn inside transaction, else per-call."""
119
+ """Execute SQL with automatic retry on SQLITE_BUSY.
120
+
121
+ Uses shared conn inside transaction, else per-call with retry.
122
+ """
107
123
  if self._txn_conn is not None:
108
124
  return self._txn_conn.execute(sql, params).fetchall()
109
- conn = self._connect()
110
- try:
111
- rows = conn.execute(sql, params).fetchall()
112
- conn.commit()
113
- return rows
114
- finally:
115
- conn.close()
125
+
126
+ last_error: Exception | None = None
127
+ for attempt in range(_MAX_RETRIES):
128
+ conn = self._connect()
129
+ try:
130
+ rows = conn.execute(sql, params).fetchall()
131
+ conn.commit()
132
+ return rows
133
+ except sqlite3.OperationalError as exc:
134
+ last_error = exc
135
+ if "locked" in str(exc).lower() or "busy" in str(exc).lower():
136
+ delay = _RETRY_BASE_DELAY * (2 ** attempt)
137
+ logger.debug(
138
+ "DB busy (attempt %d/%d), retrying in %.1fs: %s",
139
+ attempt + 1, _MAX_RETRIES, delay, exc,
140
+ )
141
+ time.sleep(delay)
142
+ continue
143
+ raise
144
+ finally:
145
+ conn.close()
146
+
147
+ logger.warning("DB operation failed after %d retries: %s", _MAX_RETRIES, last_error)
148
+ raise last_error # type: ignore[misc]
116
149
 
117
150
  def store_memory(self, record: MemoryRecord) -> str:
118
151
  """Persist a raw memory record. Returns memory_id."""