nexo-brain 7.20.23 → 7.20.24

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.20.23",
3
+ "version": "7.20.24",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,9 @@
18
18
 
19
19
  [Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
20
20
 
21
- Version `7.20.23` is the current packaged-runtime line. Patch release over v7.20.22 — Local Memory status reads the real split sidecar database read-only, reports retryable keyed failures without false zeroes, and keeps Desktop Spanish/English copy localized.
21
+ Version `7.20.24` is the current packaged-runtime line. Patch release over v7.20.23 — Local Memory performance profile writes now tolerate active indexing, retry transient SQLite busy states, and shorten indexer write locks between processed files.
22
+
23
+ Previously in `7.20.23`: patch release over v7.20.22 — Local Memory status reads the real split sidecar database read-only, reports retryable keyed failures without false zeroes, and keeps Desktop Spanish/English copy localized.
22
24
 
23
25
  Previously in `7.20.22`: patch release over v7.20.19 — Local Memory moved out of the main Brain database, MCP readiness verifies required tools, and split-aware Desktop backups validate the main DB and Local Memory sidecar separately.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.20.23",
3
+ "version": "7.20.24",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
@@ -9,11 +9,12 @@ import stat
9
9
  import hashlib
10
10
  import subprocess
11
11
  import sys
12
+ import time
12
13
  from pathlib import Path
13
14
  from typing import Any
14
15
 
15
16
  from . import embeddings
16
- from .db import LOCAL_CONTEXT_TABLES, connect_local_context_db_readonly, ensure_local_context_db, get_local_context_db
17
+ from .db import LOCAL_CONTEXT_TABLES, close_local_context_db, connect_local_context_db_readonly, ensure_local_context_db, get_local_context_db
17
18
  from .extractors import chunk_text, contains_secret, entities, extract_text, summarize
18
19
  from .logging import log_event, tail
19
20
  from .privacy import classify_path, is_local_email_tree, is_queryable_path, should_extract, should_skip_file, should_skip_tree
@@ -33,6 +34,8 @@ DEFAULT_SYSTEM_ROOT_DEPTH = int(os.environ.get("NEXO_LOCAL_INDEX_SYSTEM_ROOT_DEP
33
34
  DEFAULT_CONTEXT_MAX_CHARS = int(os.environ.get("NEXO_LOCAL_CONTEXT_MAX_CHARS", "20000") or "20000")
34
35
  DEFAULT_ROUTER_MAX_CHARS = int(os.environ.get("NEXO_LOCAL_CONTEXT_ROUTER_MAX_CHARS", "6000") or "6000")
35
36
  DEFAULT_MAX_JOB_ATTEMPTS = int(os.environ.get("NEXO_LOCAL_INDEX_MAX_JOB_ATTEMPTS", "3") or "3")
37
+ DEFAULT_SQLITE_BUSY_RETRY_ATTEMPTS = int(os.environ.get("NEXO_LOCAL_CONTEXT_BUSY_RETRY_ATTEMPTS", "5") or "5")
38
+ DEFAULT_SQLITE_BUSY_RETRY_DELAY_SECONDS = float(os.environ.get("NEXO_LOCAL_CONTEXT_BUSY_RETRY_DELAY_SECONDS", "0.35") or "0.35")
36
39
  INITIAL_INDEX_COMPLETE_KEY = "initial_index_complete"
37
40
  INITIAL_INDEX_STARTED_AT_KEY = "initial_index_started_at"
38
41
  PERFORMANCE_PROFILE_KEY = "performance_profile"
@@ -108,6 +111,27 @@ def _close_read_conn(conn) -> None:
108
111
  pass
109
112
 
110
113
 
114
+ def _sqlite_is_busy(exc: BaseException) -> bool:
115
+ return isinstance(exc, sqlite3.OperationalError) and "locked" in str(exc).lower()
116
+
117
+
118
+ def _with_sqlite_busy_retry(callback, *, attempts: int | None = None):
119
+ max_attempts = max(1, int(attempts or DEFAULT_SQLITE_BUSY_RETRY_ATTEMPTS))
120
+ last_exc = None
121
+ for attempt in range(max_attempts):
122
+ try:
123
+ return callback()
124
+ except sqlite3.OperationalError as exc:
125
+ if not _sqlite_is_busy(exc) or attempt >= max_attempts - 1:
126
+ raise
127
+ last_exc = exc
128
+ close_local_context_db()
129
+ time.sleep(DEFAULT_SQLITE_BUSY_RETRY_DELAY_SECONDS * (attempt + 1))
130
+ if last_exc:
131
+ raise last_exc
132
+ return None
133
+
134
+
111
135
  def add_root(path: str, *, mode: str = "normal", depth: int | None = None) -> dict:
112
136
  conn = _conn()
113
137
  root_path = norm_path(path)
@@ -609,9 +633,12 @@ def _set_state_conn(conn, key: str, value: str) -> None:
609
633
 
610
634
 
611
635
  def _set_state(key: str, value: str) -> None:
612
- conn = _conn()
613
- _set_state_conn(conn, key, value)
614
- conn.commit()
636
+ def write_state() -> None:
637
+ conn = _conn()
638
+ _set_state_conn(conn, key, value)
639
+ conn.commit()
640
+
641
+ _with_sqlite_busy_retry(write_state)
615
642
 
616
643
 
617
644
  def _get_state_conn(conn, key: str, default: str = "") -> str:
@@ -1745,6 +1772,7 @@ def process_jobs(*, limit: int = 100) -> dict:
1745
1772
  "UPDATE local_index_jobs SET status='running', claimed_by='local-process', lease_expires_at=?, updated_at=? WHERE job_id=?",
1746
1773
  (now() + 300, now(), job_id),
1747
1774
  )
1775
+ conn.commit()
1748
1776
  try:
1749
1777
  if row["asset_status"] != "active":
1750
1778
  raise FileNotFoundError(row["path"])
@@ -1754,6 +1782,7 @@ def process_jobs(*, limit: int = 100) -> dict:
1754
1782
  (now(), job_id),
1755
1783
  )
1756
1784
  processed += 1
1785
+ conn.commit()
1757
1786
  continue
1758
1787
  if job_type == "light_extraction":
1759
1788
  text, metadata = extract_text(Path(row["path"]))
@@ -1765,6 +1794,7 @@ def process_jobs(*, limit: int = 100) -> dict:
1765
1794
  (now(), job_id),
1766
1795
  )
1767
1796
  processed += 1
1797
+ conn.commit()
1768
1798
  continue
1769
1799
  summary = summarize(text)
1770
1800
  conn.execute(
@@ -1787,6 +1817,7 @@ def process_jobs(*, limit: int = 100) -> dict:
1787
1817
  (now(), job_id),
1788
1818
  )
1789
1819
  processed += 1
1820
+ conn.commit()
1790
1821
  except Exception as exc:
1791
1822
  failed += 1
1792
1823
  attempts = int(row["attempt_count"] or 0) + 1
@@ -1809,6 +1840,7 @@ def process_jobs(*, limit: int = 100) -> dict:
1809
1840
  technical_detail=str(exc),
1810
1841
  retryable=not terminal,
1811
1842
  )
1843
+ conn.commit()
1812
1844
  conn.commit()
1813
1845
  if processed or failed:
1814
1846
  log_event("info", "jobs_processed", "Local memory jobs processed", processed=processed, failed=failed)