nexo-brain 4.0.0 → 4.0.1

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": "4.0.0",
3
+ "version": "4.0.1",
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
@@ -87,7 +87,7 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
87
87
  - when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
88
88
  - NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
89
89
 
90
- Version `4.0.0` closes the next memory-surface gap:
90
+ Version `4.0.1` keeps the 4.0 release aligned across channels while preserving the next memory-surface gap closure:
91
91
 
92
92
  - non-text artifacts now have a first-class multimodal reference layer instead of living outside the memory model
93
93
  - pre-compaction auto-flush now persists actionable session state before context compression can erase it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
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",
@@ -1,7 +1,7 @@
1
1
  """NEXO Cognitive — Trust scoring, sentiment, dissonance."""
2
2
  import re
3
3
  import numpy as np
4
- from datetime import UTC, datetime, timedelta
4
+ from datetime import datetime, timedelta, timezone
5
5
  from cognitive._core import _get_db, embed, cosine_similarity, _blob_to_array
6
6
  from cognitive._core import POSITIVE_SIGNALS, NEGATIVE_SIGNALS, URGENCY_SIGNALS
7
7
 
@@ -412,7 +412,7 @@ def adjust_trust(event: str, context: str = "", custom_delta: float = None) -> d
412
412
  def get_trust_history(days: int = 7) -> dict:
413
413
  """Get trust score history and sentiment summary."""
414
414
  db = _get_db()
415
- cutoff = (datetime.now(UTC) - timedelta(days=days)).isoformat()
415
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
416
416
 
417
417
  # Trust events
418
418
  events = db.execute(
@@ -16,6 +16,28 @@ WRITE_LIKE_TOOLS = {"Edit", "MultiEdit", "Write"}
16
16
  DELETE_LIKE_TOOLS = {"Delete"}
17
17
  NEXO_CODE_ROOT = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent))).expanduser().resolve()
18
18
  LIVE_REPO_ROOT = NEXO_CODE_ROOT.parent if NEXO_CODE_ROOT.name == "src" else NEXO_CODE_ROOT
19
+ PUBLIC_REPO_DIRS = {
20
+ ".claude-plugin",
21
+ ".github",
22
+ "bin",
23
+ "clawhub-skill",
24
+ "community",
25
+ "docs",
26
+ "hooks",
27
+ "openclaw-plugin",
28
+ "src",
29
+ "templates",
30
+ "tests",
31
+ }
32
+ PUBLIC_REPO_FILES = {
33
+ ".mcp.json",
34
+ "CHANGELOG.md",
35
+ "LICENSE",
36
+ "README.md",
37
+ "docker-compose.yml",
38
+ "package-lock.json",
39
+ "package.json",
40
+ }
19
41
 
20
42
 
21
43
  def _operation_kind(tool_name: str) -> str:
@@ -54,11 +76,31 @@ def _automation_live_repo_guard_enabled() -> bool:
54
76
  )
55
77
 
56
78
 
79
+ def _has_git_marker(root: Path) -> bool:
80
+ return (root / ".git").exists()
81
+
82
+
83
+ def _is_public_repo_surface(candidate: Path) -> bool:
84
+ try:
85
+ relative = candidate.relative_to(LIVE_REPO_ROOT)
86
+ except ValueError:
87
+ return False
88
+
89
+ parts = relative.parts
90
+ if not parts:
91
+ return False
92
+ if parts[0] in PUBLIC_REPO_DIRS:
93
+ return True
94
+ return len(parts) == 1 and parts[0] in PUBLIC_REPO_FILES
95
+
96
+
57
97
  def _is_live_repo_path(path: str) -> bool:
58
98
  if not str(path or "").strip():
59
99
  return False
60
100
  try:
61
- return _is_relative_to(_resolve_runtime_path(path), LIVE_REPO_ROOT)
101
+ if not _has_git_marker(LIVE_REPO_ROOT):
102
+ return False
103
+ return _is_public_repo_surface(_resolve_runtime_path(path))
62
104
  except Exception:
63
105
  return False
64
106
 
@@ -542,6 +542,17 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
542
542
  except Exception:
543
543
  pass # guard_log table may not exist in older installs
544
544
 
545
+ if context_hint and _hint_suggests_correction(context_hint):
546
+ try:
547
+ if not _recent_learning_capture_exists(conn, sid, window_seconds=300):
548
+ parts.append("")
549
+ parts.append(
550
+ "⚠ LEARNING REMINDER: This looks like a user correction and no recent learning was captured. "
551
+ "If it revealed a reusable pattern, write `nexo_learning_add` NOW."
552
+ )
553
+ except Exception:
554
+ pass # Best-effort reminder only
555
+
545
556
  return "\n".join(parts)
546
557
 
547
558
 
@@ -816,6 +827,64 @@ def _hint_suggests_code_edit(hint: str) -> bool:
816
827
  return any(signal in hint_lower for signal in edit_signals)
817
828
 
818
829
 
830
+ def _hint_suggests_correction(hint: str) -> bool:
831
+ """Detect explicit user correction signals in a heartbeat context hint."""
832
+ hint_lower = hint.lower()
833
+ correction_signals = [
834
+ "that's wrong",
835
+ "that is wrong",
836
+ "wrong approach",
837
+ "not like that",
838
+ "fix this",
839
+ "fix it",
840
+ "está mal",
841
+ "esta mal",
842
+ "mal hecho",
843
+ "incorrecto",
844
+ "te equivocas",
845
+ "te has equivocado",
846
+ "lo hiciste mal",
847
+ "no era eso",
848
+ "corrige esto",
849
+ "corrígelo",
850
+ "corrigelo",
851
+ "ya te dije",
852
+ "otra vez el mismo",
853
+ "de nuevo el mismo",
854
+ "no deberías",
855
+ "no deberias",
856
+ "shouldn't have",
857
+ "should not have",
858
+ ]
859
+ return any(signal in hint_lower for signal in correction_signals)
860
+
861
+
862
+ def _recent_learning_capture_exists(conn, sid: str, window_seconds: int = 300) -> bool:
863
+ """Check whether a recent learning was captured manually or via protocol task close."""
864
+ cutoff_epoch = time.time() - window_seconds
865
+
866
+ row = conn.execute(
867
+ "SELECT 1 FROM learnings WHERE created_at >= ? LIMIT 1",
868
+ (cutoff_epoch,),
869
+ ).fetchone()
870
+ if row:
871
+ return True
872
+
873
+ row = conn.execute(
874
+ """
875
+ SELECT 1
876
+ FROM protocol_tasks
877
+ WHERE session_id = ?
878
+ AND learning_id IS NOT NULL
879
+ AND closed_at IS NOT NULL
880
+ AND CAST(strftime('%s', closed_at) AS INTEGER) >= ?
881
+ LIMIT 1
882
+ """,
883
+ (sid, int(cutoff_epoch)),
884
+ ).fetchone()
885
+ return bool(row)
886
+
887
+
819
888
  def _toolbox_summary(conn) -> str:
820
889
  """Quick count of available skills and behavioral learnings for startup reminder."""
821
890
  try:
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  """Inspectable user-state model built from multiple NEXO signals."""
4
4
 
5
5
  import json
6
- from datetime import UTC, datetime, timedelta
6
+ from datetime import datetime, timedelta, timezone
7
7
 
8
8
  import cognitive
9
9
  from db import get_db
@@ -32,7 +32,7 @@ def init_tables() -> None:
32
32
 
33
33
 
34
34
  def _recent_correction_count(days: int) -> int:
35
- cutoff = (datetime.now(UTC) - timedelta(days=days)).isoformat()
35
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
36
36
  row = cognitive._get_db().execute(
37
37
  "SELECT COUNT(*) FROM memory_corrections WHERE created_at >= ?",
38
38
  (cutoff,),
@@ -41,7 +41,7 @@ def _recent_correction_count(days: int) -> int:
41
41
 
42
42
 
43
43
  def _recent_trust_event_count(days: int, event_name: str) -> int:
44
- cutoff = (datetime.now(UTC) - timedelta(days=days)).isoformat()
44
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
45
45
  row = cognitive._get_db().execute(
46
46
  "SELECT COUNT(*) FROM trust_score WHERE created_at >= ? AND event = ?",
47
47
  (cutoff, event_name),
@@ -50,7 +50,7 @@ def _recent_trust_event_count(days: int, event_name: str) -> int:
50
50
 
51
51
 
52
52
  def _recent_diary_signal_count(days: int) -> int:
53
- cutoff = (datetime.now(UTC) - timedelta(days=days)).isoformat(timespec="seconds")
53
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat(timespec="seconds")
54
54
  row = get_db().execute(
55
55
  "SELECT COUNT(*) FROM session_diary WHERE created_at >= ? AND user_signals != ''",
56
56
  (cutoff,),
@@ -157,7 +157,7 @@ def list_user_state_snapshots(limit: int = 20) -> list[dict]:
157
157
 
158
158
  def user_state_stats(days: int = 30) -> dict:
159
159
  init_tables()
160
- cutoff = (datetime.now(UTC) - timedelta(days=days)).isoformat(timespec="seconds")
160
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat(timespec="seconds")
161
161
  rows = get_db().execute(
162
162
  "SELECT state_label, COUNT(*) AS cnt FROM user_state_snapshots WHERE created_at >= ? GROUP BY state_label",
163
163
  (cutoff,),