nexo-brain 0.3.4 → 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/cognitive.py +114 -6
- 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/src/server.py +6 -5
- package/src/tools_sessions.py +13 -0
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/cognitive.py
CHANGED
|
@@ -30,13 +30,13 @@ DISCRIMINATING_ENTITIES = {
|
|
|
30
30
|
# OS / Environment
|
|
31
31
|
"linux", "mac", "macos", "windows", "darwin", "ubuntu", "debian", "alpine",
|
|
32
32
|
# Platforms
|
|
33
|
-
"
|
|
33
|
+
"nexo", "other", "whatsapp", "chrome", "firefox",
|
|
34
34
|
# Languages / Runtimes
|
|
35
35
|
"python", "php", "javascript", "typescript", "node", "deno", "ruby",
|
|
36
36
|
# Versions
|
|
37
37
|
"v1", "v2", "v3", "v4", "v5", "5.6", "7.4", "8.0", "8.1", "8.2",
|
|
38
38
|
# Infrastructure
|
|
39
|
-
"
|
|
39
|
+
"vps", "local", "production", "staging",
|
|
40
40
|
# DB
|
|
41
41
|
"mysql", "sqlite", "postgresql", "postgres", "redis",
|
|
42
42
|
}
|
|
@@ -61,8 +61,8 @@ URGENCY_SIGNALS = {
|
|
|
61
61
|
"rápido", "ya", "ahora", "urgente", "asap", "inmediatamente", "corre",
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
# Trust score events
|
|
65
|
-
|
|
64
|
+
# Trust score events — default deltas (overridable via trust_event_config table)
|
|
65
|
+
_DEFAULT_TRUST_EVENTS = {
|
|
66
66
|
# Positive
|
|
67
67
|
"explicit_thanks": +3,
|
|
68
68
|
"delegation": +2, # the user delegates new task without micromanaging
|
|
@@ -77,6 +77,113 @@ TRUST_EVENTS = {
|
|
|
77
77
|
"forgot_followup": -4, # Forgot to mark followup or execute it
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
# Lazy-loaded from DB (trust_event_config table overrides defaults)
|
|
81
|
+
_trust_events_cache = None
|
|
82
|
+
_trust_events_cache_ts = 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_trust_events() -> dict:
|
|
86
|
+
"""Get trust events with deltas. DB overrides take priority over defaults."""
|
|
87
|
+
global _trust_events_cache, _trust_events_cache_ts
|
|
88
|
+
import time
|
|
89
|
+
now = time.time()
|
|
90
|
+
# Cache for 60s to avoid constant DB reads
|
|
91
|
+
if _trust_events_cache is not None and (now - _trust_events_cache_ts) < 60:
|
|
92
|
+
return _trust_events_cache
|
|
93
|
+
|
|
94
|
+
events = dict(_DEFAULT_TRUST_EVENTS)
|
|
95
|
+
try:
|
|
96
|
+
db = _get_db()
|
|
97
|
+
db.execute("""
|
|
98
|
+
CREATE TABLE IF NOT EXISTS trust_event_config (
|
|
99
|
+
event TEXT PRIMARY KEY,
|
|
100
|
+
delta REAL NOT NULL,
|
|
101
|
+
description TEXT DEFAULT '',
|
|
102
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
103
|
+
)
|
|
104
|
+
""")
|
|
105
|
+
rows = db.execute("SELECT event, delta FROM trust_event_config").fetchall()
|
|
106
|
+
for r in rows:
|
|
107
|
+
events[r[0]] = r[1]
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
_trust_events_cache = events
|
|
111
|
+
_trust_events_cache_ts = now
|
|
112
|
+
return events
|
|
113
|
+
|
|
114
|
+
# For backward compat — code that reads TRUST_EVENTS directly
|
|
115
|
+
TRUST_EVENTS = _DEFAULT_TRUST_EVENTS
|
|
116
|
+
|
|
117
|
+
# Auto-detection patterns for trust events from user text
|
|
118
|
+
# Each pattern: (event_name, keywords/phrases that trigger it, min_matches)
|
|
119
|
+
TRUST_AUTO_PATTERNS = {
|
|
120
|
+
"explicit_thanks": {
|
|
121
|
+
"patterns": [
|
|
122
|
+
"gracias", "buen trabajo", "bien hecho", "perfecto", "genial",
|
|
123
|
+
"excelente", "fenomenal", "great job", "nice work", "thank",
|
|
124
|
+
"thanks", "awesome", "amazing", "love it", "me encanta",
|
|
125
|
+
],
|
|
126
|
+
"min_matches": 1,
|
|
127
|
+
},
|
|
128
|
+
"correction": {
|
|
129
|
+
"patterns": [
|
|
130
|
+
"ya te dije", "ya te lo dije", "otra vez", "te he dicho",
|
|
131
|
+
"no es así", "eso no", "mal", "incorrecto", "equivocado",
|
|
132
|
+
"no no no", "that's wrong", "te aviso", "te avisé",
|
|
133
|
+
"2ª vez", "segunda vez", "te lo repito",
|
|
134
|
+
],
|
|
135
|
+
"min_matches": 1,
|
|
136
|
+
},
|
|
137
|
+
"repeated_error": {
|
|
138
|
+
"patterns": [
|
|
139
|
+
"otra vez lo mismo", "siempre igual", "ya te lo dije antes",
|
|
140
|
+
"cuántas veces", "no aprendes", "same mistake", "again the same",
|
|
141
|
+
"ya van", "es la 2", "es la 3", "ya te avisé",
|
|
142
|
+
],
|
|
143
|
+
"min_matches": 1,
|
|
144
|
+
},
|
|
145
|
+
"delegation": {
|
|
146
|
+
"patterns": [
|
|
147
|
+
"encárgate", "hazlo tú", "dale tú", "te lo dejo",
|
|
148
|
+
"manéjalo", "resuélvelo", "handle it", "take care of",
|
|
149
|
+
"you decide", "tú decides", "lo que veas", "como veas",
|
|
150
|
+
],
|
|
151
|
+
"min_matches": 1,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def auto_detect_trust_events(text: str) -> list[dict]:
|
|
157
|
+
"""Detect trust events from user text. Returns list of {event, delta, reason}.
|
|
158
|
+
|
|
159
|
+
Called automatically by heartbeat. Only fires once per event per heartbeat
|
|
160
|
+
to avoid double-counting.
|
|
161
|
+
"""
|
|
162
|
+
if not text or len(text.strip()) < 5:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
text_lower = text.lower()
|
|
166
|
+
events = get_trust_events()
|
|
167
|
+
detected = []
|
|
168
|
+
|
|
169
|
+
for event_name, config in TRUST_AUTO_PATTERNS.items():
|
|
170
|
+
matches = [p for p in config["patterns"] if p in text_lower]
|
|
171
|
+
if len(matches) >= config["min_matches"]:
|
|
172
|
+
delta = events.get(event_name, _DEFAULT_TRUST_EVENTS.get(event_name, 0))
|
|
173
|
+
detected.append({
|
|
174
|
+
"event": event_name,
|
|
175
|
+
"delta": delta,
|
|
176
|
+
"reason": f"auto-detected: {', '.join(matches[:3])}",
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
# Priority: if repeated_error detected, remove correction (it's a superset)
|
|
180
|
+
event_names = {d["event"] for d in detected}
|
|
181
|
+
if "repeated_error" in event_names and "correction" in event_names:
|
|
182
|
+
detected = [d for d in detected if d["event"] != "correction"]
|
|
183
|
+
# If explicit_thanks and delegation both detected, keep both (they're independent)
|
|
184
|
+
|
|
185
|
+
return detected
|
|
186
|
+
|
|
80
187
|
_model = None
|
|
81
188
|
_conn = None
|
|
82
189
|
|
|
@@ -1956,7 +2063,7 @@ def resolve_dissonance(memory_id: int, resolution: str, context: str = "") -> st
|
|
|
1956
2063
|
Args:
|
|
1957
2064
|
memory_id: The LTM memory that conflicts with the new instruction
|
|
1958
2065
|
resolution: One of:
|
|
1959
|
-
- 'paradigm_shift': the user changed
|
|
2066
|
+
- 'paradigm_shift': the user changed their mind permanently. Decay old memory,
|
|
1960
2067
|
new instruction becomes the standard.
|
|
1961
2068
|
- 'exception': This is a one-time override. Keep old memory as standard.
|
|
1962
2069
|
- 'override': Old memory was wrong. Mark as corrupted and decay to dormant.
|
|
@@ -2159,7 +2266,8 @@ def adjust_trust(event: str, context: str = "", custom_delta: float = None) -> d
|
|
|
2159
2266
|
db = _get_db()
|
|
2160
2267
|
old_score = get_trust_score()
|
|
2161
2268
|
|
|
2162
|
-
|
|
2269
|
+
events = get_trust_events()
|
|
2270
|
+
delta = custom_delta if custom_delta is not None else events.get(event, 0)
|
|
2163
2271
|
if delta == 0 and custom_delta is None:
|
|
2164
2272
|
return {"old_score": old_score, "delta": 0, "new_score": old_score, "event": event, "error": "unknown event"}
|
|
2165
2273
|
|
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"
|
package/src/server.py
CHANGED
|
@@ -52,7 +52,7 @@ mcp = FastMCP(
|
|
|
52
52
|
name="nexo",
|
|
53
53
|
instructions=(
|
|
54
54
|
"NEXO operational server. Provides session coordination, "
|
|
55
|
-
"reminders, followups, and menu for user operations.\n\n"
|
|
55
|
+
"reminders, followups, and menu for the user's operations.\n\n"
|
|
56
56
|
"When working with tool results, write down any important information "
|
|
57
57
|
"you might need later in your response, as the original tool result "
|
|
58
58
|
"may be cleared later."
|
|
@@ -75,15 +75,16 @@ def nexo_startup(task: str = "Startup") -> str:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
@mcp.tool
|
|
78
|
-
def nexo_heartbeat(sid: str, task: str) -> str:
|
|
79
|
-
"""Update session task, check inbox and pending questions.
|
|
78
|
+
def nexo_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
79
|
+
"""Update session task, check inbox and pending questions. Auto-detects trust events.
|
|
80
80
|
|
|
81
81
|
Call this at the START of every user interaction (before doing work).
|
|
82
82
|
Args:
|
|
83
83
|
sid: Your session ID from nexo_startup.
|
|
84
84
|
task: Brief description of current work (5-10 words).
|
|
85
|
+
context_hint: Last 2-3 sentences from the user or current topic. Used for sentiment detection, trust auto-scoring, and mid-session RAG. ALWAYS provide this for best results.
|
|
85
86
|
"""
|
|
86
|
-
return handle_heartbeat(sid, task)
|
|
87
|
+
return handle_heartbeat(sid, task, context_hint)
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
@mcp.tool
|
|
@@ -312,7 +313,7 @@ def nexo_learning_add(category: str, title: str, content: str, reasoning: str =
|
|
|
312
313
|
"""Add a new learning (resolved error, pattern, gotcha).
|
|
313
314
|
|
|
314
315
|
Args:
|
|
315
|
-
category: One of:
|
|
316
|
+
category: One of: nexo-ops, infrastructure, security, brain-engine, other.
|
|
316
317
|
title: Short title for the learning.
|
|
317
318
|
content: Full description with context and solution.
|
|
318
319
|
reasoning: WHY this matters — what led to discovering this (optional).
|
package/src/tools_sessions.py
CHANGED
|
@@ -102,6 +102,19 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
102
102
|
except Exception:
|
|
103
103
|
pass
|
|
104
104
|
|
|
105
|
+
# Auto-detect trust events from context_hint
|
|
106
|
+
if context_hint and len(context_hint.strip()) >= 10:
|
|
107
|
+
try:
|
|
108
|
+
import cognitive
|
|
109
|
+
auto_events = cognitive.auto_detect_trust_events(context_hint)
|
|
110
|
+
for ae in auto_events:
|
|
111
|
+
result = cognitive.adjust_trust(ae["event"], ae["reason"], ae["delta"])
|
|
112
|
+
if result.get("delta", 0) != 0:
|
|
113
|
+
parts.append("")
|
|
114
|
+
parts.append(f"TRUST AUTO: {result['old_score']:.0f} → {result['new_score']:.0f} ({result['delta']:+.0f}) [{ae['event']}] {ae['reason']}")
|
|
115
|
+
except Exception:
|
|
116
|
+
pass # Auto-trust is best-effort
|
|
117
|
+
|
|
105
118
|
# Mid-session RAG: if context_hint provided, check for context shift
|
|
106
119
|
if context_hint and len(context_hint.strip()) >= 15:
|
|
107
120
|
try:
|