nexo-brain 0.3.5 → 0.3.6
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 +1 -1
- package/src/db.py +72 -15
- package/src/scripts/nexo-catchup.py +2 -1
- package/src/scripts/nexo-cognitive-decay.py +5 -1
- package/src/scripts/nexo-daily-self-audit.py +8 -7
- package/src/scripts/nexo-immune.py +3 -2
- package/src/scripts/nexo-postmortem-consolidator.py +6 -3
- package/src/scripts/nexo-sleep.py +2 -1
- package/src/scripts/nexo-synthesis.py +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
6
6
|
"bin": {
|
package/src/db.py
CHANGED
|
@@ -7,6 +7,7 @@ import secrets
|
|
|
7
7
|
import string
|
|
8
8
|
import datetime
|
|
9
9
|
import pathlib
|
|
10
|
+
import threading
|
|
10
11
|
|
|
11
12
|
DB_PATH = os.environ.get(
|
|
12
13
|
"NEXO_TEST_DB",
|
|
@@ -21,31 +22,35 @@ SESSION_STALE_SECONDS = 900 # 15 min (documented TTL)
|
|
|
21
22
|
MESSAGE_TTL_SECONDS = 3600 # 1 hour
|
|
22
23
|
QUESTION_TTL_SECONDS = 600 # 10 min
|
|
23
24
|
|
|
24
|
-
#
|
|
25
|
+
# Single shared connection per process with write serialization.
|
|
26
|
+
# SQLite allows only one writer at a time. Using a shared connection with
|
|
27
|
+
# check_same_thread=False and a write lock ensures:
|
|
28
|
+
# - No FTS5 corruption from concurrent write connections
|
|
29
|
+
# - Reads can happen freely (WAL allows concurrent readers)
|
|
30
|
+
# - Writes are serialized via _write_lock to prevent 'database is locked' errors
|
|
25
31
|
_shared_conn: sqlite3.Connection | None = None
|
|
32
|
+
_write_lock = threading.RLock() # RLock allows re-entrant locking (function A calls B, both serialize)
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
def get_db() -> sqlite3.Connection:
|
|
29
|
-
"""Get shared database connection with WAL mode.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- wal_autocheckpoint keeps WAL file from growing unbounded
|
|
35
|
-
- Autocommit via isolation_level=None prevents implicit transactions
|
|
36
|
-
that hold locks across multiple operations
|
|
36
|
+
"""Get shared database connection with WAL mode.
|
|
37
|
+
|
|
38
|
+
Returns a _SerializedConnection wrapper that serializes all execute
|
|
39
|
+
calls via _write_lock, preventing race conditions and FTS5 corruption
|
|
40
|
+
under concurrent thread access.
|
|
37
41
|
"""
|
|
38
42
|
global _shared_conn
|
|
39
43
|
if _shared_conn is None:
|
|
40
|
-
|
|
44
|
+
raw = sqlite3.connect(
|
|
41
45
|
DB_PATH, timeout=30, check_same_thread=False,
|
|
42
46
|
isolation_level=None, # autocommit — no implicit BEGIN holding locks
|
|
43
47
|
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
raw.execute("PRAGMA journal_mode=WAL")
|
|
49
|
+
raw.execute("PRAGMA busy_timeout=30000")
|
|
50
|
+
raw.execute("PRAGMA foreign_keys=ON")
|
|
51
|
+
raw.execute("PRAGMA wal_autocheckpoint=100")
|
|
52
|
+
raw.row_factory = sqlite3.Row
|
|
53
|
+
_shared_conn = _SerializedConnection(raw)
|
|
49
54
|
return _shared_conn
|
|
50
55
|
|
|
51
56
|
|
|
@@ -60,6 +65,58 @@ def close_db():
|
|
|
60
65
|
_shared_conn = None
|
|
61
66
|
|
|
62
67
|
|
|
68
|
+
def _get_raw_conn() -> sqlite3.Connection:
|
|
69
|
+
"""Get the raw unwrapped connection (for PRAGMA queries that need direct access)."""
|
|
70
|
+
conn = get_db()
|
|
71
|
+
if isinstance(conn, _SerializedConnection):
|
|
72
|
+
return conn._conn
|
|
73
|
+
return conn
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _SerializedConnection:
|
|
77
|
+
"""Wrapper around sqlite3.Connection that serializes all execute calls.
|
|
78
|
+
|
|
79
|
+
SQLite with a single shared connection and check_same_thread=False needs
|
|
80
|
+
serialization to prevent:
|
|
81
|
+
- Stale lastrowid when concurrent INSERTs happen
|
|
82
|
+
- FTS5 index corruption from concurrent writes
|
|
83
|
+
- 'NoneType' errors from interleaved INSERT+SELECT sequences
|
|
84
|
+
|
|
85
|
+
All execute/executemany/executescript calls go through _write_lock.
|
|
86
|
+
Property access (row_factory etc.) passes through directly.
|
|
87
|
+
"""
|
|
88
|
+
def __init__(self, conn: sqlite3.Connection):
|
|
89
|
+
self._conn = conn
|
|
90
|
+
|
|
91
|
+
def execute(self, *args, **kwargs):
|
|
92
|
+
with _write_lock:
|
|
93
|
+
return self._conn.execute(*args, **kwargs)
|
|
94
|
+
|
|
95
|
+
def executemany(self, *args, **kwargs):
|
|
96
|
+
with _write_lock:
|
|
97
|
+
return self._conn.executemany(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
def executescript(self, *args, **kwargs):
|
|
100
|
+
with _write_lock:
|
|
101
|
+
return self._conn.executescript(*args, **kwargs)
|
|
102
|
+
|
|
103
|
+
def commit(self):
|
|
104
|
+
with _write_lock:
|
|
105
|
+
return self._conn.commit()
|
|
106
|
+
|
|
107
|
+
def close(self):
|
|
108
|
+
return self._conn.close()
|
|
109
|
+
|
|
110
|
+
def __getattr__(self, name):
|
|
111
|
+
return getattr(self._conn, name)
|
|
112
|
+
|
|
113
|
+
def __setattr__(self, name, value):
|
|
114
|
+
if name == '_conn':
|
|
115
|
+
super().__setattr__(name, value)
|
|
116
|
+
else:
|
|
117
|
+
setattr(self._conn, name, value)
|
|
118
|
+
|
|
119
|
+
|
|
63
120
|
def init_db():
|
|
64
121
|
"""Create tables if they don't exist."""
|
|
65
122
|
conn = get_db()
|
|
@@ -25,6 +25,7 @@ from datetime import datetime, timedelta
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
27
|
HOME = Path.home()
|
|
28
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
|
|
28
29
|
LOG_DIR = HOME / "claude" / "logs"
|
|
29
30
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
30
31
|
LOG_FILE = LOG_DIR / "catchup.log"
|
|
@@ -139,7 +140,7 @@ def main():
|
|
|
139
140
|
subprocess.run(
|
|
140
141
|
[PYTHON_BREW if os.path.exists(PYTHON_BREW) else PYTHON_SYS, str(update_script)],
|
|
141
142
|
capture_output=True, text=True, timeout=60,
|
|
142
|
-
env={**os.environ, "HOME": str(HOME), "NEXO_HOME":
|
|
143
|
+
env={**os.environ, "HOME": str(HOME), "NEXO_HOME": NEXO_HOME}
|
|
143
144
|
)
|
|
144
145
|
except Exception as e:
|
|
145
146
|
log(f" Update check failed: {e}")
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
"""NEXO Cognitive Decay — Daily Ebbinghaus sweep + STM→LTM promotion."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from datetime import datetime
|
|
8
9
|
|
|
10
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
|
|
11
|
+
sys.path.insert(0, NEXO_HOME)
|
|
12
|
+
# Fallback for development installs
|
|
9
13
|
sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
|
|
10
14
|
import cognitive
|
|
11
15
|
|
|
12
|
-
STATE_FILE = Path
|
|
16
|
+
STATE_FILE = Path(NEXO_HOME) / ".catchup-state.json"
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
def update_catchup_state():
|
|
@@ -14,10 +14,11 @@ import hashlib
|
|
|
14
14
|
from datetime import datetime, timedelta
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
NEXO_HOME_PATH = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
18
|
+
LOG_DIR = NEXO_HOME_PATH / "logs"
|
|
18
19
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
19
20
|
LOG_FILE = LOG_DIR / "self-audit.log"
|
|
20
|
-
NEXO_DB =
|
|
21
|
+
NEXO_DB = NEXO_HOME_PATH / "nexo.db"
|
|
21
22
|
# Configure this to point to your main project repo for uncommitted-changes check
|
|
22
23
|
PROJECT_REPO_DIR = Path(os.environ.get("NEXO_PROJECT_REPO", str(Path.home() / "projects" / "main")))
|
|
23
24
|
HASH_REGISTRY = Path.home() / "claude" / "scripts" / ".watchdog-hashes"
|
|
@@ -245,7 +246,7 @@ def check_watchdog_registry():
|
|
|
245
246
|
# ── Check 13: Snapshot drift on protected recovery files ────────────────
|
|
246
247
|
def check_snapshot_sync():
|
|
247
248
|
pairs = [
|
|
248
|
-
(
|
|
249
|
+
(NEXO_HOME_PATH / "db.py", SNAPSHOT_GOLDEN / "nexo-mcp" / "db.py"),
|
|
249
250
|
(Path.home() / "claude" / "cortex" / "cortex-wrapper.py", SNAPSHOT_GOLDEN / "cortex" / "cortex-wrapper.py"),
|
|
250
251
|
(Path.home() / "claude" / "cortex" / "evolution_cycle.py", SNAPSHOT_GOLDEN / "cortex" / "evolution_cycle.py"),
|
|
251
252
|
]
|
|
@@ -341,7 +342,7 @@ def check_watchdog_smoke():
|
|
|
341
342
|
# ── Check 18: Cognitive memory health ────────────────────────────────
|
|
342
343
|
def check_cognitive_health():
|
|
343
344
|
"""Check cognitive.db health and run weekly GC on Sundays."""
|
|
344
|
-
cognitive_db =
|
|
345
|
+
cognitive_db = NEXO_HOME_PATH / "cognitive.db"
|
|
345
346
|
if not cognitive_db.exists():
|
|
346
347
|
finding("WARN", "cognitive", "cognitive.db not found")
|
|
347
348
|
return
|
|
@@ -362,7 +363,7 @@ def check_cognitive_health():
|
|
|
362
363
|
|
|
363
364
|
# Metrics report (spec section 9)
|
|
364
365
|
try:
|
|
365
|
-
sys.path.insert(0, str(
|
|
366
|
+
sys.path.insert(0, str(NEXO_HOME_PATH))
|
|
366
367
|
import cognitive as cog
|
|
367
368
|
|
|
368
369
|
metrics = cog.get_metrics(days=7)
|
|
@@ -403,7 +404,7 @@ def check_cognitive_health():
|
|
|
403
404
|
|
|
404
405
|
# Phase triggers monitoring (spec section 10)
|
|
405
406
|
try:
|
|
406
|
-
sys.path.insert(0, str(
|
|
407
|
+
sys.path.insert(0, str(NEXO_HOME_PATH))
|
|
407
408
|
import cognitive as cog
|
|
408
409
|
|
|
409
410
|
db_cog = cog._get_db()
|
|
@@ -459,7 +460,7 @@ def check_cognitive_health():
|
|
|
459
460
|
if datetime.now().weekday() == 6:
|
|
460
461
|
log(" Running weekly cognitive GC (Sunday)...")
|
|
461
462
|
try:
|
|
462
|
-
sys.path.insert(0, str(
|
|
463
|
+
sys.path.insert(0, str(NEXO_HOME_PATH))
|
|
463
464
|
import cognitive as cog
|
|
464
465
|
|
|
465
466
|
# 1. Delete STM with strength < 0.1 and > 30 days
|
|
@@ -60,6 +60,7 @@ CLAUDE_DIR = HOME / "claude"
|
|
|
60
60
|
COORD_DIR = CLAUDE_DIR / "coordination"
|
|
61
61
|
BRAIN_DIR = CLAUDE_DIR / "brain"
|
|
62
62
|
SCRIPTS_DIR = CLAUDE_DIR / "scripts"
|
|
63
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
|
|
63
64
|
|
|
64
65
|
IMMUNE_STATUS = COORD_DIR / "immune-status.json"
|
|
65
66
|
IMMUNE_LOG = COORD_DIR / "immune-log.json"
|
|
@@ -368,8 +369,8 @@ def check_databases():
|
|
|
368
369
|
results = []
|
|
369
370
|
|
|
370
371
|
dbs = [
|
|
371
|
-
("nexo.db", Path
|
|
372
|
-
("cognitive.db", Path
|
|
372
|
+
("nexo.db", Path(NEXO_HOME) / "nexo.db"),
|
|
373
|
+
("cognitive.db", Path(NEXO_HOME) / "cognitive.db"),
|
|
373
374
|
("claude-mem.db", CLAUDE_MEM_DB),
|
|
374
375
|
]
|
|
375
376
|
|
|
@@ -26,12 +26,15 @@ from collections import Counter
|
|
|
26
26
|
from datetime import datetime, date, timedelta
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
|
-
# Add
|
|
29
|
+
# Add NEXO_HOME to path for cognitive engine
|
|
30
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
|
|
31
|
+
sys.path.insert(0, NEXO_HOME)
|
|
32
|
+
# Fallback for development installs
|
|
30
33
|
sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
|
|
31
34
|
|
|
32
35
|
HOME = Path.home()
|
|
33
|
-
NEXO_DB =
|
|
34
|
-
SESSION_BUFFER =
|
|
36
|
+
NEXO_DB = Path(NEXO_HOME) / "nexo.db"
|
|
37
|
+
SESSION_BUFFER = Path(NEXO_HOME) / "brain" / "session_buffer.jsonl"
|
|
35
38
|
MEMORY_DIR = HOME / ".claude" / "projects" / f"-Users-{os.environ.get('USER', 'user')}" / "memory"
|
|
36
39
|
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
|
|
37
40
|
CONSOLIDATION_LOG = HOME / "claude" / "logs" / "postmortem-consolidation.log"
|
|
@@ -54,8 +54,9 @@ HEARTBEAT_LOG = COORD_DIR / "heartbeat-log.json"
|
|
|
54
54
|
REFLECTION_LOG = COORD_DIR / "reflection-log.json"
|
|
55
55
|
SLEEP_LOG = COORD_DIR / "sleep-log.json"
|
|
56
56
|
|
|
57
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
|
|
57
58
|
MEMORY_MD = Path.home() / ".claude" / "projects" / f"-Users-{os.environ.get('USER', 'user')}" / "memory" / "MEMORY.md"
|
|
58
|
-
NEXO_DB = Path
|
|
59
|
+
NEXO_DB = Path(NEXO_HOME) / "nexo.db"
|
|
59
60
|
CLAUDE_MEM_DB = Path.home() / ".claude-mem" / "claude-mem.db"
|
|
60
61
|
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
61
62
|
|
|
@@ -21,8 +21,9 @@ from pathlib import Path
|
|
|
21
21
|
HOME = Path.home()
|
|
22
22
|
CLAUDE_DIR = HOME / "claude"
|
|
23
23
|
COORD_DIR = CLAUDE_DIR / "coordination"
|
|
24
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
|
|
24
25
|
|
|
25
|
-
NEXO_DB =
|
|
26
|
+
NEXO_DB = Path(NEXO_HOME) / "nexo.db"
|
|
26
27
|
CLAUDE_MEM_DB = HOME / ".claude-mem" / "claude-mem.db"
|
|
27
28
|
|
|
28
29
|
OUTPUT_FILE = COORD_DIR / "daily-synthesis.md"
|