nexo-brain 7.9.0 → 7.9.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 +3 -1
- package/package.json +1 -1
- package/src/autonomy_mandate.py +14 -1
- package/src/guard_verbal_ack.py +13 -1
- package/src/r14_correction_learning.py +17 -6
- package/src/r16_declared_done.py +15 -4
- package/src/r17_promise_debt.py +15 -4
- package/src/semantic_reasoner.py +4 -1
- package/src/semantic_router.py +12 -2
- package/src/session_end_intent.py +12 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.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
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.9.
|
|
21
|
+
Version `7.9.1` is the current packaged-runtime line. Patch release that starts the semantic-router site migration promised after v7.9.0: six safe textual-conversational callers now route through `semantic_router.route(...)` instead of importing `enforcement_classifier.classify` directly (`session_end_intent`, `r14_correction_learning`, `r16_declared_done`, `r17_promise_debt`, `autonomy_mandate`, `guard_verbal_ack`). The patch also fixes the semantic stack's local layers to classify the live `context` text rather than letting static prompt templates dominate zero-shot decisions, and migrates the six callers to semantic labels (`session_end`/`continue_session`, `negative_feedback`/`ordinary_request`, etc.) instead of generic `yes`/`no`. Existing fail-closed behaviour and test injection seams are preserved. Targeted verification: 105 tests passing across router, reasoner, migrated call sites, and their enforcement integrations. Remaining textual/code-aware callers stay tracked under `NF-SEMANTIC-ROUTER-SITE-MIGRATION` for later focused patches. No Desktop bump.
|
|
22
|
+
|
|
23
|
+
Previously in `7.9.0`: minor release that ships the foundation of the semantic stack (router + reasoner + CLI) under the ONEPASS LLM Coverage plan, plus two product-bug fixes observed in the wild on 2026-04-23. New `src/semantic_router.py` exposes 18 named `decision_kinds` (13 textual + 5 code-aware) with a per-kind policy table and the layer chain `fast_local → semantic_reasoner → remote_fallback`. New `src/semantic_reasoner.py` adds Mode A (`multipass_local`: reuses the mDeBERTa pin with three prompt-perturbed passes + majority vote + 0.75 floor) and Mode B (`cached_llm`: wrapper over `call_model_raw` with a pid+uuid atomic-write 24h-TTL disk cache at `~/.nexo/runtime/operations/semantic-reasoner-cache.json`, SHA-256 keyed by `decision_kind` + normalized input, LRU-bounded at 2000 entries, corrupt entries dropped on read). New `scripts/semantic-classify.py` JSON-in JSON-out CLI lets external MCP clients (including the closed-source NEXO Desktop companion) query Brain as the single semantic authority. New `NEXO_SEMANTIC_REASONER` kill switch (`0`/`off`/`false`/`no`/`disable`/`disabled`) honours the plan mandate for a runtime opt-out separate from `NEXO_LOCAL_CLASSIFIER`. Bug fixes: `bin/nexo-brain.js` upgrade flow now copies `templates/` root the same way fresh install and same-version refresh already did (Maria iMac 7.1.10→7.8.1 upgrade had lost 27 core-prompts templates and broken post-update import verification); and `tool-enforcement-map.json` `nexo_startup.enforcement.inject_prompt` now instructs the model to preload the 13 `mcp__nexo__*` protocol tools via `ToolSearch` before calling `nexo_startup` when the host MCP client defers tool schemas (Claude Code with many MCPs installed). Audit-driven hardening: router/reasoner defensively use `getattr` over the `call_model_raw` module and add a trailing `except Exception` so provider errors degrade with `remote_error` instead of propagating; cache writes use pid+uuid tmp + `fsync` + `os.replace` to survive concurrent writers; `NEXO_SEMANTIC_REASONER_TTL` parse tolerates malformed values. Tests: +50 (22 router, 20 reasoner, 8 CLI). Per-site migration of existing callers (`session_end_intent`, `r14`, `r16`, `r17`, `r20`, `r34`, T4 gates, `tools_drive`, `nexo-followup-runner`) is explicitly deferred to follow-up patch releases and tracked as `NF-SEMANTIC-ROUTER-SITE-MIGRATION`; nothing in this release changes the behaviour of the existing callers. Companion coordinated release: NEXO Desktop v0.28.0.
|
|
22
24
|
|
|
23
25
|
Previously in `7.8.2`: patch release that fixes the compact-hook observability gap Francisco flagged after v7.8.1: `hook_runs.session_id` was empty for 7 out of 8 recent compaction rows (and when populated it stored the raw Claude Code token instead of the NEXO sid), so per-session queries over `hook_runs` for compact events could not be joined back to the NEXO session that actually compacted. v7.8.2 adds `src/hooks/compact_session_resolver.py` with `resolve_nexo_sid(claude_session_id)`, which walks the same rails the shell already uses: `sessions.claude_session_id` match, then `session_claude_aliases.claude_session_id` (most recent `last_seen` wins), then the per-conversation sidecar under `runtime/data/compacting/<safe-claude-id>.txt`, then the legacy global sidecar for single-conversation setups. `src/hooks/pre_compact.py` and `src/hooks/post_compact.py` now call the resolver and store the real NEXO sid in `hook_runs.session_id`; both wrappers also stash `{claude_session_id, sid_source}` in `hook_runs.metadata` so "why is this row still empty?" has a one-query answer. Nine new tests in `tests/test_hook_runs_compact_sid_resolution.py` pin the five resolver rails (sessions / alias / sidecar / legacy / none), malformed-sidecar rejection, the pre- and post-compact wrapper end-to-end paths, and the empty-state wrapper rail so a clean audit trail is written even when nothing resolves. No Desktop bump.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.1",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain \u2014 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/autonomy_mandate.py
CHANGED
|
@@ -39,6 +39,7 @@ from core_prompts import render_core_prompt
|
|
|
39
39
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
40
40
|
STATE_PATH = NEXO_HOME / "runtime" / "data" / "autonomy_mandate.json"
|
|
41
41
|
CLASSIFIER_QUESTION = render_core_prompt("autonomy-mandate-question")
|
|
42
|
+
SEMANTIC_LABELS = ("autonomy_mandate", "not_mandate")
|
|
42
43
|
|
|
43
44
|
# Marker list per NF-DS-45569A27. Case-insensitive substring match.
|
|
44
45
|
MARKERS = (
|
|
@@ -119,9 +120,21 @@ def _detect_marker(text: str, *, classifier=None) -> Optional[str]:
|
|
|
119
120
|
return marker
|
|
120
121
|
if classifier is None:
|
|
121
122
|
try:
|
|
122
|
-
from
|
|
123
|
+
from semantic_router import route as semantic_route
|
|
123
124
|
except Exception:
|
|
124
125
|
return None
|
|
126
|
+
try:
|
|
127
|
+
result = semantic_route(
|
|
128
|
+
decision_kind="autonomy_mandate",
|
|
129
|
+
question=CLASSIFIER_QUESTION,
|
|
130
|
+
context=text.strip()[:1200],
|
|
131
|
+
labels=SEMANTIC_LABELS,
|
|
132
|
+
)
|
|
133
|
+
if bool(result.ok and (result.label or result.verdict) == "autonomy_mandate"):
|
|
134
|
+
return _SEMANTIC_MARKER
|
|
135
|
+
except Exception:
|
|
136
|
+
return None
|
|
137
|
+
return None
|
|
125
138
|
try:
|
|
126
139
|
if bool(classifier(question=CLASSIFIER_QUESTION, context=text.strip()[:1200])):
|
|
127
140
|
return _SEMANTIC_MARKER
|
package/src/guard_verbal_ack.py
CHANGED
|
@@ -10,6 +10,7 @@ from core_prompts import render_core_prompt
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
CLASSIFIER_QUESTION = render_core_prompt("guard-verbal-ack-question")
|
|
13
|
+
SEMANTIC_LABELS = ("explicit_ack", "not_ack")
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def _build_context(
|
|
@@ -44,7 +45,7 @@ def detect_guard_verbal_ack(
|
|
|
44
45
|
return False
|
|
45
46
|
if classifier is None:
|
|
46
47
|
try:
|
|
47
|
-
from
|
|
48
|
+
from semantic_router import route as semantic_route
|
|
48
49
|
except Exception:
|
|
49
50
|
return False
|
|
50
51
|
context = _build_context(
|
|
@@ -54,6 +55,17 @@ def detect_guard_verbal_ack(
|
|
|
54
55
|
file_path=file_path,
|
|
55
56
|
guard_summary=guard_summary,
|
|
56
57
|
)
|
|
58
|
+
if classifier is None:
|
|
59
|
+
try:
|
|
60
|
+
result = semantic_route(
|
|
61
|
+
decision_kind="guard_verbal_ack",
|
|
62
|
+
question=CLASSIFIER_QUESTION,
|
|
63
|
+
context=context,
|
|
64
|
+
labels=SEMANTIC_LABELS,
|
|
65
|
+
)
|
|
66
|
+
return bool(result.ok and (result.label or result.verdict) == "explicit_ack")
|
|
67
|
+
except Exception:
|
|
68
|
+
return False
|
|
57
69
|
try:
|
|
58
70
|
return bool(classifier(question=CLASSIFIER_QUESTION, context=context))
|
|
59
71
|
except Exception:
|
|
@@ -9,10 +9,9 @@ Fase 2 Protocol Enforcer Fase C (Capa 2) item R14. Plan doc 1 reads:
|
|
|
9
9
|
|
|
10
10
|
Implementation contract:
|
|
11
11
|
|
|
12
|
-
- Correction detection goes through
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
is the sanctioned alternative.
|
|
12
|
+
- Correction detection goes through semantic_router decision_kind
|
|
13
|
+
``r14_correction``. Learning #122 prohibits keyword-based semantic
|
|
14
|
+
detection; the router path is the sanctioned alternative.
|
|
16
15
|
- Fail-closed: when the classifier is unavailable (no API key,
|
|
17
16
|
automation_backend=none, timeout, 5xx), is_correction returns
|
|
18
17
|
False. Downstream R28 (system prompt) and the auto_capture hook
|
|
@@ -31,6 +30,8 @@ from __future__ import annotations
|
|
|
31
30
|
from core_prompts import render_core_prompt
|
|
32
31
|
|
|
33
32
|
CLASSIFIER_QUESTION = render_core_prompt("r14-correction-learning-question")
|
|
33
|
+
SEMANTIC_LABELS = ("negative_feedback", "ordinary_request")
|
|
34
|
+
POSITIVE_LABEL = "negative_feedback"
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
INJECTION_PROMPT_TEMPLATE = render_core_prompt("r14-correction-learning-injection")
|
|
@@ -45,7 +46,7 @@ def detect_correction(user_text: str, *, classifier=None) -> bool:
|
|
|
45
46
|
Args:
|
|
46
47
|
user_text: Raw user-role text from the stream.
|
|
47
48
|
classifier: Injection point for tests. Defaults to
|
|
48
|
-
|
|
49
|
+
semantic_router.route(decision_kind="r14_correction").
|
|
49
50
|
|
|
50
51
|
Fail-closed on ClassifierUnavailableError — returns False rather
|
|
51
52
|
than raising so the caller's enforcement loop never crashes on a
|
|
@@ -62,7 +63,17 @@ def detect_correction(user_text: str, *, classifier=None) -> bool:
|
|
|
62
63
|
return False
|
|
63
64
|
if classifier is None:
|
|
64
65
|
try:
|
|
65
|
-
from
|
|
66
|
+
from semantic_router import route as semantic_route
|
|
67
|
+
except Exception:
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
result = semantic_route(
|
|
71
|
+
decision_kind="r14_correction",
|
|
72
|
+
question=CLASSIFIER_QUESTION,
|
|
73
|
+
context=text,
|
|
74
|
+
labels=SEMANTIC_LABELS,
|
|
75
|
+
)
|
|
76
|
+
return bool(result.ok and (result.label or result.verdict) == POSITIVE_LABEL)
|
|
66
77
|
except Exception:
|
|
67
78
|
return False
|
|
68
79
|
try:
|
package/src/r16_declared_done.py
CHANGED
|
@@ -10,9 +10,9 @@ Exposes detect_declared_done(assistant_text, classifier=None) → bool and
|
|
|
10
10
|
the reminder prompt template. The window-and-state tracking lives in
|
|
11
11
|
the HeadlessEnforcer / Desktop EnforcementEngine, not here.
|
|
12
12
|
|
|
13
|
-
Classifier contract: same
|
|
14
|
-
(
|
|
15
|
-
|
|
13
|
+
Classifier contract: same semantic_router yes/no path as R14
|
|
14
|
+
(``decision_kind=r16_declared_done``). Fail-closed on unavailable backend →
|
|
15
|
+
detect returns False rather than raising.
|
|
16
16
|
|
|
17
17
|
Mirror: nexo-desktop/lib/r16-declared-done.js (pending, landing in the
|
|
18
18
|
next tranche alongside the JS classifier infrastructure).
|
|
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|
|
22
22
|
from core_prompts import render_core_prompt
|
|
23
23
|
|
|
24
24
|
CLASSIFIER_QUESTION = render_core_prompt("r16-declared-done-question")
|
|
25
|
+
SEMANTIC_LABELS = ("declared_done", "not_done")
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
INJECTION_PROMPT_TEMPLATE = render_core_prompt("r16-declared-done-injection")
|
|
@@ -43,7 +44,17 @@ def detect_declared_done(assistant_text: str, *, classifier=None) -> bool:
|
|
|
43
44
|
return False
|
|
44
45
|
if classifier is None:
|
|
45
46
|
try:
|
|
46
|
-
from
|
|
47
|
+
from semantic_router import route as semantic_route
|
|
48
|
+
except Exception:
|
|
49
|
+
return False
|
|
50
|
+
try:
|
|
51
|
+
result = semantic_route(
|
|
52
|
+
decision_kind="r16_declared_done",
|
|
53
|
+
question=CLASSIFIER_QUESTION,
|
|
54
|
+
context=text,
|
|
55
|
+
labels=SEMANTIC_LABELS,
|
|
56
|
+
)
|
|
57
|
+
return bool(result.ok and (result.label or result.verdict) == "declared_done")
|
|
47
58
|
except Exception:
|
|
48
59
|
return False
|
|
49
60
|
try:
|
package/src/r17_promise_debt.py
CHANGED
|
@@ -9,9 +9,9 @@ Fase 2 Protocol Enforcer Fase D item R17. Plan doc 1 reads:
|
|
|
9
9
|
Exposes detect_promise(text, classifier) → bool. State (promise window
|
|
10
10
|
countdown) lives in the caller — mirrors the R14 / R16 pattern.
|
|
11
11
|
|
|
12
|
-
Classifier path is the same as R14 / R16:
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
Classifier path is the same as R14 / R16:
|
|
13
|
+
semantic_router decision_kind ``r17_promise_debt``. Fail-closed on any
|
|
14
|
+
unavailable backend (no promise flagged rather than a false positive).
|
|
15
15
|
|
|
16
16
|
Mirror: nexo-desktop/lib/r17-promise-debt.js (bundled with Fase D JS
|
|
17
17
|
twins at the end of the tranche).
|
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
from core_prompts import render_core_prompt
|
|
22
22
|
|
|
23
23
|
CLASSIFIER_QUESTION = render_core_prompt("r17-promise-debt-question")
|
|
24
|
+
SEMANTIC_LABELS = ("promise", "no_promise")
|
|
24
25
|
|
|
25
26
|
INJECTION_PROMPT_TEMPLATE = render_core_prompt("r17-promise-debt-injection")
|
|
26
27
|
|
|
@@ -37,7 +38,17 @@ def detect_promise(assistant_text: str, *, classifier=None) -> bool:
|
|
|
37
38
|
return False
|
|
38
39
|
if classifier is None:
|
|
39
40
|
try:
|
|
40
|
-
from
|
|
41
|
+
from semantic_router import route as semantic_route
|
|
42
|
+
except Exception:
|
|
43
|
+
return False
|
|
44
|
+
try:
|
|
45
|
+
result = semantic_route(
|
|
46
|
+
decision_kind="r17_promise_debt",
|
|
47
|
+
question=CLASSIFIER_QUESTION,
|
|
48
|
+
context=text,
|
|
49
|
+
labels=SEMANTIC_LABELS,
|
|
50
|
+
)
|
|
51
|
+
return bool(result.ok and (result.label or result.verdict) == "promise")
|
|
41
52
|
except Exception:
|
|
42
53
|
return False
|
|
43
54
|
try:
|
package/src/semantic_reasoner.py
CHANGED
|
@@ -143,6 +143,7 @@ def _reason_multipass_local(
|
|
|
143
143
|
*,
|
|
144
144
|
decision_kind: str,
|
|
145
145
|
question: str,
|
|
146
|
+
context: str = "",
|
|
146
147
|
labels: tuple[str, ...] | None,
|
|
147
148
|
confidence_floor: float,
|
|
148
149
|
):
|
|
@@ -156,7 +157,8 @@ def _reason_multipass_local(
|
|
|
156
157
|
error="multipass_local requires labels",
|
|
157
158
|
)
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
semantic_input = (context or "").strip() or question
|
|
161
|
+
votes = _collect_local_votes(semantic_input, labels)
|
|
160
162
|
label, confidence, meta = _aggregate_votes(votes, confidence_floor)
|
|
161
163
|
if label is None:
|
|
162
164
|
return RouterResult(
|
|
@@ -557,6 +559,7 @@ def reason(
|
|
|
557
559
|
return _reason_multipass_local(
|
|
558
560
|
decision_kind=decision_kind,
|
|
559
561
|
question=question,
|
|
562
|
+
context=context,
|
|
560
563
|
labels=labels_tuple,
|
|
561
564
|
confidence_floor=confidence_floor,
|
|
562
565
|
)
|
package/src/semantic_router.py
CHANGED
|
@@ -173,11 +173,19 @@ def policy_for(decision_kind: str) -> dict[str, Any] | None:
|
|
|
173
173
|
def _run_fast_local(
|
|
174
174
|
*,
|
|
175
175
|
question: str,
|
|
176
|
+
context: str = "",
|
|
176
177
|
labels: tuple[str, ...],
|
|
177
178
|
confidence_floor: float,
|
|
178
179
|
) -> RouterResult | None:
|
|
179
180
|
"""Try ``LocalZeroShotClassifier``. Return None on unavailable or
|
|
180
|
-
below-threshold so the router advances.
|
|
181
|
+
below-threshold so the router advances.
|
|
182
|
+
|
|
183
|
+
The first layer must classify the actual user/assistant payload. For
|
|
184
|
+
guard decisions the ``question`` is usually a stable prompt template and
|
|
185
|
+
the live text lives in ``context``; feeding both into a zero-shot NLI
|
|
186
|
+
classifier makes the static prompt dominate the decision. Use context
|
|
187
|
+
when present, and fall back to question for simple direct callers.
|
|
188
|
+
"""
|
|
181
189
|
try:
|
|
182
190
|
from classifier_local import LocalZeroShotClassifier
|
|
183
191
|
except Exception as exc: # pragma: no cover — install not ready
|
|
@@ -185,7 +193,8 @@ def _run_fast_local(
|
|
|
185
193
|
return None
|
|
186
194
|
|
|
187
195
|
clf = LocalZeroShotClassifier(confidence_floor=confidence_floor)
|
|
188
|
-
|
|
196
|
+
classifier_input = (context or "").strip() or question
|
|
197
|
+
result = clf.classify(classifier_input, labels)
|
|
189
198
|
if result is None:
|
|
190
199
|
return None
|
|
191
200
|
if result.confidence < confidence_floor:
|
|
@@ -403,6 +412,7 @@ def route(
|
|
|
403
412
|
if policy["fast_local_threshold"] is not None and labels_tuple:
|
|
404
413
|
fast = _run_fast_local(
|
|
405
414
|
question=question,
|
|
415
|
+
context=context,
|
|
406
416
|
labels=labels_tuple,
|
|
407
417
|
confidence_floor=float(policy["fast_local_threshold"]),
|
|
408
418
|
)
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
from core_prompts import render_core_prompt
|
|
9
9
|
|
|
10
10
|
CLASSIFIER_QUESTION = render_core_prompt("session-end-intent-question")
|
|
11
|
+
SEMANTIC_LABELS = ("session_end", "continue_session")
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def detect_session_end_intent(user_text: str, *, classifier=None) -> bool:
|
|
@@ -16,7 +17,17 @@ def detect_session_end_intent(user_text: str, *, classifier=None) -> bool:
|
|
|
16
17
|
return False
|
|
17
18
|
if classifier is None:
|
|
18
19
|
try:
|
|
19
|
-
from
|
|
20
|
+
from semantic_router import route as semantic_route
|
|
21
|
+
except Exception:
|
|
22
|
+
return False
|
|
23
|
+
try:
|
|
24
|
+
result = semantic_route(
|
|
25
|
+
decision_kind="session_end_intent",
|
|
26
|
+
question=CLASSIFIER_QUESTION,
|
|
27
|
+
context=text,
|
|
28
|
+
labels=SEMANTIC_LABELS,
|
|
29
|
+
)
|
|
30
|
+
return bool(result.ok and (result.label or result.verdict) == "session_end")
|
|
20
31
|
except Exception:
|
|
21
32
|
return False
|
|
22
33
|
try:
|