nexo-brain 5.8.1 → 5.8.2

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": "5.8.1",
3
+ "version": "5.8.2",
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 `5.8.1` is the current packaged-runtime line: closes a self-reinforcing `launchctl kickstart -k` loop in the watchdog that wedged deep-sleep Phase 2 between 2026-04-14 and 2026-04-17. The cron wrapper now INSERTs an in-flight row (`ended_at=NULL`) at start and traps SIGTERM/INT/HUP to close it with `exit_code=143` instead of vanishing from `cron_runs`. The watchdog interprets in-flight rows as "currently running" and only re-executes after verifying the worker process is dead. `extract.py` classifies CLI failures into transient (`overloaded_error`, rate-limit, timeout, signal retried next run) and deterministic (skipped after `MAX_POISON_ATTEMPTS`), and passes a slim shared-context (200 head lines + metadata) instead of the full 400+ KB dump. A new `auto_update._heal_deep_sleep_runtime()` repairs existing installs silently on the next `nexo update`: poisoned checkpoints, stale locks, dangling `cron_runs` rows, and bloated `.watchdog-fails` counters.
21
+ Version `5.8.2` is the current packaged-runtime line: the Brain core no longer auto-classifies `followups` and `reminders` on behalf of agents. v5.8.0's `classify_task()` heuristic (NEXO-specific ID prefixes `NF-PROTOCOL-*` / `NF-DS-*` / `NF-AUDIT-*`, Spanish user-verbs `debes` / `revisar` / `firmar`, agent keywords `monitor` / `auditoría diaria` / `checkpoint`) was fine for NEXO's own DB but bled convention into every third-party agent plugged into the shared Brain. The core now persists `internal=0` and `owner=NULL` when the caller omits them, and clients that want automatic classification (NEXO Desktop does, via its `_legacyClassifyOwner` helpers) compute it themselves and pass the result. Migration #40 keeps the columns + indexes; rows already backfilled by v5.8.0 keep their values. `normalise_owner` still explicitly rejects the string `"nexo"` so legacy hardcoding cannot sneak back in.
22
+
23
+ Previously in `5.8.1`: closes a self-reinforcing `launchctl kickstart -k` loop in the watchdog that wedged deep-sleep Phase 2 between 2026-04-14 and 2026-04-17. The cron wrapper now INSERTs an in-flight row (`ended_at=NULL`) at start and traps SIGTERM/INT/HUP to close it with `exit_code=143` instead of vanishing from `cron_runs`. The watchdog interprets in-flight rows as "currently running" and only re-executes after verifying the worker process is dead. `extract.py` classifies CLI failures into transient (`overloaded_error`, rate-limit, timeout, signal — retried next run) and deterministic (skipped after `MAX_POISON_ATTEMPTS`), and passes a slim shared-context (200 head lines + metadata) instead of the full 400+ KB dump. A new `auto_update._heal_deep_sleep_runtime()` repairs existing installs silently on the next `nexo update`: poisoned checkpoints, stale locks, dangling `cron_runs` rows, and bloated `.watchdog-fails` counters.
22
24
 
23
25
  Previously in `5.8.0`: first-class `internal` and `owner` columns on `followups` and `reminders`. Migration #40 adds both fields with an idempotent one-shot backfill, so the "who does this task belong to?" classification moves from client-side regex (Desktop) to persistent storage every MCP client shares. Taxonomy is intentionally generic — `owner in {user, waiting, agent, shared}` — so third-party agents plugging into the shared Brain can render whatever assistant label they carry without inheriting NEXO branding. `nexo_reminder_create`, `nexo_reminder_update`, `nexo_followup_create`, and `nexo_followup_update` gain optional `internal` and `owner` parameters that win over the default heuristic.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.8.1",
3
+ "version": "5.8.2",
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",
@@ -1,132 +1,54 @@
1
- """NEXO DB — Task classification helpers (internal + owner).
2
-
3
- Introduced in migration #40. Every followup and reminder carries two
4
- classification attributes so clients (Desktop Home, dashboard, future
5
- agents) do not need to compute them with client-side regex:
6
-
7
- internal (INTEGER 0/1):
8
- 1 if the task is bookkeeping the agent keeps for itself
9
- (protocol enforcer, deep-sleep housekeeping, audit trail,
10
- release gates, retroactive learnings). These are hidden from
11
- normal user views by default.
12
-
13
- owner (TEXT):
14
- 'user' — the user has to act (was 'Para ti' in Desktop).
15
- 'waiting' — blocked on an external response (was 'Esperando').
1
+ """NEXO DB — Task classification storage (internal + owner).
2
+
3
+ Migration #40 added ``internal`` and ``owner`` columns to ``followups`` and
4
+ ``reminders``. Agents creating or updating tasks pass these two fields
5
+ explicitly via the MCP tools (``nexo_followup_create``, ``nexo_reminder_create``
6
+ and their ``_update`` counterparts).
7
+
8
+ The Brain core does **not** classify tasks on behalf of agents. Up to and
9
+ including v5.8.1 the core shipped a Spanish-first regex heuristic
10
+ (``NF-PROTOCOL-*`` / ``NF-DS-*`` prefixes, user verbs like ``debes``,
11
+ ``revisar``, etc.) as a fallback for callers that left the fields blank.
12
+ That fallback bled NEXO-specific naming conventions into every deployment
13
+ of the shared Brain — third-party agents plugged into the same DB would
14
+ inherit classifications they never asked for. v5.8.2 removes it.
15
+
16
+ The module now exposes only:
17
+
18
+ VALID_OWNERS — the canonical set {user, waiting, agent, shared}.
19
+ normalise_owner — clamps an agent-supplied string to VALID_OWNERS
20
+ (or ``None`` for empty / invalid input so the
21
+ caller can decide whether to persist ``NULL``).
22
+ normalise_internal — coerces truthy / boolean / numeric agent input
23
+ into ``0`` / ``1`` (or ``None`` for empty input).
24
+
25
+ owner values:
26
+ 'user' — the user has to act.
27
+ 'waiting' — blocked on an external response.
16
28
  'agent' — the AI agent handles it autonomously. Intentionally
17
- named 'agent' and NOT 'nexo' so non-NEXO deployments
18
- render whatever label fits (e.g. 'Claude', 'Codex',
19
- hotel-assistant name). The user-facing label is
20
- resolved client-side.
21
- 'shared' — collaborative follow-up (was 'Seguimiento').
22
- NULL — unclassified; clients fall back to the legacy
23
- client-side heuristic for backward compat.
24
-
25
- Agents creating tasks via nexo_followup_create / nexo_reminder_create
26
- can override both fields explicitly. If they leave them blank, the
27
- Brain applies the heuristic below so a vanilla agent keeps sensible
28
- behaviour out of the box.
29
+ named ``agent`` (not ``nexo``) so deployments render
30
+ whatever assistant label fits client-side.
31
+ 'shared' — collaborative follow-up.
32
+ NULL — unclassified; clients are free to apply whatever
33
+ fallback they want at render time.
34
+
35
+ Clients that want automatic classification (NEXO Desktop does, via its
36
+ ``_legacyClassifyOwner`` / ``_legacyIsInternalTaskId`` helpers) compute
37
+ ``owner``/``internal`` themselves and pass them to the create/update call.
29
38
  """
30
39
 
31
40
  from __future__ import annotations
32
41
 
33
- import re
34
-
35
- # Task-ID prefixes historically owned by NEXO's own automation. They are
36
- # kept as a default heuristic because they match the existing corpus of
37
- # 468+ followups and 40+ reminders. Any agent not following this naming
38
- # convention will simply not match these patterns and its tasks will
39
- # stay visible (internal=0) unless the agent sets internal=1 explicitly
40
- # on create — which is exactly what we want for a pluralistic ecosystem.
41
- _INTERNAL_ID_PATTERNS = [
42
- re.compile(r"^NF-PROTOCOL[-_]", re.IGNORECASE),
43
- re.compile(r"^NF-DS[-_]", re.IGNORECASE),
44
- re.compile(r"^NF-AUDIT[-_]", re.IGNORECASE),
45
- re.compile(r"^NF-OPPORTUNITY[-_]", re.IGNORECASE),
46
- re.compile(r"^NF-RETRO[-_]", re.IGNORECASE),
47
- re.compile(r"^R-RELEASE[-_]", re.IGNORECASE),
48
- re.compile(r"^R-FU-NF-PROTOCOL[-_]", re.IGNORECASE),
49
- re.compile(r"^R-FU-NF-DS[-_]", re.IGNORECASE),
50
- re.compile(r"^R-FU-NF-AUDIT[-_]", re.IGNORECASE),
51
- ]
52
-
53
- # Spanish user-action verbs. The heuristic is Spanish-first because the
54
- # existing corpus is Spanish, but since every agent can override `owner`
55
- # explicitly on create, deployments in other languages are not blocked.
56
- _USER_VERB_RX = re.compile(
57
- r"\b(francisco debe|debes|llamar|responder|revisar|validar|confirmar|"
58
- r"decidir|aprobar|firmar|enviar email|mandar email|contestar|"
59
- r"reuni[óo]n|reservar|comprar)\b",
60
- re.IGNORECASE,
61
- )
62
-
63
- _WAITING_RX = re.compile(
64
- r"\b(esperando|esperar|bloqueo|bloqueado|pendiente respuesta|"
65
- r"pendiente de|en espera)\b",
66
- re.IGNORECASE,
67
- )
68
-
69
- _AGENT_RX = re.compile(
70
- r"\b(monitoreo|monitorizar|monitor|auditor[íi]a diaria|"
71
- r"promoci[óo]n diaria|seguir|seguimiento 24|72h|checkpoint|runner|cron)\b",
72
- re.IGNORECASE,
73
- )
74
42
 
75
43
  VALID_OWNERS = {"user", "waiting", "agent", "shared"}
76
44
 
77
45
 
78
- def is_internal_id(task_id: str | None) -> bool:
79
- """Return True when the ID matches a known agent-internal prefix."""
80
- tid = (task_id or "").strip()
81
- if not tid:
82
- return False
83
- return any(pat.search(tid) for pat in _INTERNAL_ID_PATTERNS)
84
-
85
-
86
- def classify_owner(
87
- task_id: str | None,
88
- description: str | None,
89
- category: str | None = None,
90
- recurrence: str | None = None,
91
- ) -> str:
92
- """Classify ownership into one of VALID_OWNERS using the legacy rules."""
93
- tid = (task_id or "").strip()
94
- desc = (description or "").strip()
95
- cat = (category or "").strip().lower()
96
- rec = (recurrence or "").strip()
97
-
98
- if cat == "waiting" or _WAITING_RX.search(desc):
99
- return "waiting"
100
- if _USER_VERB_RX.search(desc) or tid.lower().startswith("nf-protocol-"):
101
- return "user"
102
- if rec or _AGENT_RX.search(desc):
103
- return "agent"
104
- return "shared"
105
-
106
-
107
- def classify_task(
108
- task_id: str | None,
109
- description: str | None,
110
- category: str | None = None,
111
- recurrence: str | None = None,
112
- ) -> tuple[int, str]:
113
- """Compute (internal, owner) pair for a task.
114
-
115
- Returns integers for internal so the SQLite column (INTEGER DEFAULT 0)
116
- and the JSON round-trip stay consistent. Clients can truthy-check either
117
- int or bool safely.
118
- """
119
- internal = 1 if is_internal_id(task_id) else 0
120
- owner = classify_owner(task_id, description, category, recurrence)
121
- return internal, owner
122
-
123
-
124
46
  def normalise_owner(value: str | None) -> str | None:
125
47
  """Accept owner overrides from agents and clamp to VALID_OWNERS.
126
48
 
127
49
  Returns None for empty input (so the DB keeps NULL / pre-existing value)
128
50
  and coerces invalid strings to None rather than silently persisting
129
- garbage. Callers decide whether to fall back to classify_owner().
51
+ garbage.
130
52
  """
131
53
  if value is None:
132
54
  return None
@@ -8,7 +8,7 @@ import sqlite3
8
8
  from typing import Any
9
9
 
10
10
  from db._core import get_db, now_epoch
11
- from db._classification import classify_task, normalise_internal, normalise_owner
11
+ from db._classification import normalise_internal, normalise_owner
12
12
  from db._fts import fts_upsert
13
13
  from db._hot_context import capture_context_event
14
14
 
@@ -256,18 +256,18 @@ def create_reminder(
256
256
  """Create a new reminder.
257
257
 
258
258
  Agents may pass `internal` (0/1, bool, or string) and `owner`
259
- ('user'|'waiting'|'agent'|'shared') to override the default
260
- classification. When omitted, classify_task() applies the legacy
261
- heuristic so behaviour matches pre-migration #40.
259
+ ('user'|'waiting'|'agent'|'shared'). When omitted, the Brain persists
260
+ ``internal=0`` and ``owner=NULL`` the Brain core does not classify
261
+ tasks on behalf of agents. Clients that want automatic classification
262
+ compute it themselves and pass the result.
262
263
  """
263
264
  conn = get_db()
264
265
  now = now_epoch()
265
266
 
266
- auto_internal, auto_owner = classify_task(id, description, category, None)
267
267
  internal_value = normalise_internal(internal)
268
268
  if internal_value is None:
269
- internal_value = auto_internal
270
- owner_value = normalise_owner(owner) or auto_owner
269
+ internal_value = 0
270
+ owner_value = normalise_owner(owner)
271
271
 
272
272
  columns = {str(row["name"]) for row in conn.execute("PRAGMA table_info(reminders)").fetchall()}
273
273
  payload: dict[str, object] = {
@@ -615,9 +615,10 @@ def create_followup(
615
615
  ) -> dict:
616
616
  """Create a new followup with optional reasoning and recurrence.
617
617
 
618
- Agents may override the default classification via `internal` and
619
- `owner`. Omitted values are filled by classify_task() using the
620
- legacy heuristics so pre-migration callers keep working identically.
618
+ Agents may set `internal` and `owner` explicitly. Omitted values
619
+ persist as ``internal=0`` and ``owner=NULL`` the Brain core does not
620
+ classify tasks on behalf of agents. Clients that want automatic
621
+ classification compute it themselves and pass the result.
621
622
  """
622
623
  conn = get_db()
623
624
  now = now_epoch()
@@ -630,11 +631,10 @@ def create_followup(
630
631
  f"(scores: {', '.join(str(s['_similarity']) for s in similar[:3])}). Consider updating instead."
631
632
  )
632
633
 
633
- auto_internal, auto_owner = classify_task(id, description, None, recurrence)
634
634
  internal_value = normalise_internal(internal)
635
635
  if internal_value is None:
636
- internal_value = auto_internal
637
- owner_value = normalise_owner(owner) or auto_owner
636
+ internal_value = 0
637
+ owner_value = normalise_owner(owner)
638
638
 
639
639
  columns = {str(row["name"]) for row in conn.execute("PRAGMA table_info(followups)").fetchall()}
640
640
  payload: dict[str, object] = {
package/src/db/_schema.py CHANGED
@@ -939,18 +939,11 @@ def _m39_hook_runs(conn):
939
939
  def _m40_classification_columns(conn):
940
940
  """Add internal (INTEGER 0/1) and owner (TEXT) to followups and reminders.
941
941
 
942
- Background: before this migration, Desktop clients had to compute the
943
- "who does this belong to" classification client-side using Spanish regex
944
- on description and ID-prefix pattern matching (NF-PROTOCOL-*, NF-DS-*, …).
945
- That logic was hardcoded to NEXO's own ID convention and Spanish-speaking
946
- users. Any third-party agent plugging into the shared Brain would either
947
- see every task as "Seguimiento" (owner=shared fallback) or, worse, have
948
- its real user-facing tasks hidden by the Desktop 'internal' filter.
949
-
950
- Fix: make both attributes first-class columns agents can set on create.
951
- Vanilla agents that omit them get the legacy heuristic (classify_task)
952
- applied on insert and during this one-shot backfill, so existing rows
953
- preserve their current Desktop rendering.
942
+ Agents creating tasks via nexo_followup_create / nexo_reminder_create
943
+ can set both fields explicitly. The Brain core does not classify tasks
944
+ on behalf of agents clients that want automatic classification
945
+ compute it themselves (NEXO Desktop does, via its legacy client-side
946
+ helpers) and pass the result.
954
947
 
955
948
  Values:
956
949
  internal: 0 (external, visible) or 1 (agent bookkeeping, hidden).
@@ -960,8 +953,10 @@ def _m40_classification_columns(conn):
960
953
  'NEXO'.
961
954
 
962
955
  Idempotent: _migrate_add_column is a no-op when the column exists,
963
- _migrate_add_index likewise. The backfill only touches rows where
964
- owner IS NULL, so re-running never overwrites agent-set values.
956
+ _migrate_add_index likewise. Pre-v5.8.2 versions of this migration
957
+ also ran a one-shot backfill using a Spanish-first regex heuristic;
958
+ v5.8.2 removed that heuristic so the core stays neutral across
959
+ deployments. Rows that were already backfilled keep their values.
965
960
  """
966
961
  _migrate_add_column(conn, "followups", "internal", "INTEGER DEFAULT 0")
967
962
  _migrate_add_column(conn, "followups", "owner", "TEXT DEFAULT NULL")
@@ -972,32 +967,6 @@ def _m40_classification_columns(conn):
972
967
  _migrate_add_index(conn, "idx_reminders_internal", "reminders", "internal")
973
968
  _migrate_add_index(conn, "idx_reminders_owner", "reminders", "owner")
974
969
 
975
- from db._classification import classify_task
976
-
977
- rows = conn.execute(
978
- "SELECT id, description, recurrence FROM followups WHERE owner IS NULL"
979
- ).fetchall()
980
- for row in rows:
981
- internal, owner = classify_task(
982
- row["id"], row["description"], None, row["recurrence"]
983
- )
984
- conn.execute(
985
- "UPDATE followups SET internal = ?, owner = ? WHERE id = ?",
986
- (internal, owner, row["id"]),
987
- )
988
-
989
- rows = conn.execute(
990
- "SELECT id, description, category FROM reminders WHERE owner IS NULL"
991
- ).fetchall()
992
- for row in rows:
993
- internal, owner = classify_task(
994
- row["id"], row["description"], row["category"], None
995
- )
996
- conn.execute(
997
- "UPDATE reminders SET internal = ?, owner = ? WHERE id = ?",
998
- (internal, owner, row["id"]),
999
- )
1000
-
1001
970
 
1002
971
  MIGRATIONS = [
1003
972
  (1, "learnings_columns", _m1_learnings_columns),