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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.0",
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.0` is the current packaged-runtime line. 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.
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.0",
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",
@@ -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 enforcement_classifier import classify as classifier # type: ignore
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
@@ -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 enforcement_classifier import classify as classifier # type: ignore
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 the enforcement_classifier
13
- (triple-reinforced yes/no on call_model_raw). Learning #122
14
- prohibits keyword-based semantic detection; the classifier path
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
- enforcement_classifier.classify.
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 enforcement_classifier import classify as classifier # type: ignore
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:
@@ -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 triple-reinforced yes/no path as R14
14
- (enforcement_classifier.classify → call_model_raw). Fail-closed on
15
- unavailable backend → detect returns False rather than raising.
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 enforcement_classifier import classify as classifier # type: ignore
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:
@@ -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: enforcement_classifier.classify
13
- routes through call_model_raw with triple reinforcement. Fail-closed on
14
- any unavailable backend (no promise flagged rather than a false positive).
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 enforcement_classifier import classify as classifier # type: ignore
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:
@@ -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
- votes = _collect_local_votes(question, labels)
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
  )
@@ -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
- result = clf.classify(question, labels)
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 enforcement_classifier import classify as classifier # type: ignore
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: