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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cognitive/_trust.py +2 -2
- package/src/hook_guardrails.py +43 -1
- package/src/tools_sessions.py +69 -0
- package/src/user_state_model.py +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "4.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.
|
|
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.
|
|
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",
|
package/src/cognitive/_trust.py
CHANGED
|
@@ -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
|
|
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(
|
|
415
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
|
416
416
|
|
|
417
417
|
# Trust events
|
|
418
418
|
events = db.execute(
|
package/src/hook_guardrails.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/tools_sessions.py
CHANGED
|
@@ -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:
|
package/src/user_state_model.py
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,),
|