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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/db/_classification.py +37 -115
- package/src/db/_reminders.py +13 -13
- package/src/db/_schema.py +9 -40
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.8.
|
|
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.
|
|
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.
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
51
|
+
garbage.
|
|
130
52
|
"""
|
|
131
53
|
if value is None:
|
|
132
54
|
return None
|
package/src/db/_reminders.py
CHANGED
|
@@ -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
|
|
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')
|
|
260
|
-
|
|
261
|
-
|
|
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 =
|
|
270
|
-
owner_value = normalise_owner(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
|
|
619
|
-
|
|
620
|
-
|
|
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 =
|
|
637
|
-
owner_value = normalise_owner(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
|
-
|
|
943
|
-
|
|
944
|
-
on
|
|
945
|
-
|
|
946
|
-
|
|
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.
|
|
964
|
-
|
|
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),
|