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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "0.3.4",
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
- "shopify", "whatsapp", "chrome", "firefox",
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
- "cloudrun", "gcloud", "vps", "local", "production", "staging",
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 and their point values
65
- TRUST_EVENTS = {
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 his mind permanently. Decay old memory,
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
- delta = custom_delta if custom_delta is not None else TRUST_EVENTS.get(event, 0)
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
- # Shared connection per process — avoids file descriptor leak with multiple MCP sessions
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. One connection per process.
30
-
31
- Uses WAL2-style pragmas for safe multi-process concurrent access:
32
- - WAL mode allows readers to not block writers
33
- - busy_timeout prevents instant SQLITE_BUSY failures
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
- _shared_conn = sqlite3.connect(
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
- _shared_conn.execute("PRAGMA journal_mode=WAL")
45
- _shared_conn.execute("PRAGMA busy_timeout=30000")
46
- _shared_conn.execute("PRAGMA foreign_keys=ON")
47
- _shared_conn.execute("PRAGMA wal_autocheckpoint=100")
48
- _shared_conn.row_factory = sqlite3.Row
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": str(HOME / "claude" / "nexo-mcp")}
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.home() / "claude" / "operations" / ".catchup-state.json"
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
- LOG_DIR = Path.home() / "claude" / "logs"
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 = Path.home() / "claude" / "nexo-mcp" / "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
- (Path.home() / "claude" / "nexo-mcp" / "db.py", SNAPSHOT_GOLDEN / "nexo-mcp" / "db.py"),
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 = Path.home() / "claude" / "nexo-mcp" / "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(Path.home() / "claude" / "nexo-mcp"))
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(Path.home() / "claude" / "nexo-mcp"))
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(Path.home() / "claude" / "nexo-mcp"))
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.home() / "claude" / "nexo-mcp" / "nexo.db"),
372
- ("cognitive.db", Path.home() / "claude" / "nexo-mcp" / "cognitive.db"),
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 nexo-mcp to path for cognitive engine
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 = HOME / "claude" / "nexo-mcp" / "nexo.db"
34
- SESSION_BUFFER = HOME / "claude" / "brain" / "session_buffer.jsonl"
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.home() / "claude" / "nexo-mcp" / "nexo.db"
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 = HOME / "claude" / "nexo-mcp" / "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: general, code, infrastructure, api, database, security, deployment, testing, performance, ux.
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).
@@ -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: