What v5.8.0 did (and why it was a problem)
Two weeks ago v5.8.0 added internal and owner as first-class columns on followups and reminders, with four MCP tools to set them (nexo_followup_create, nexo_followup_update, and the two reminder counterparts). That part is fine and stays.
What came with it was a regex auto-classifier in src/db/_classification.py that fired automatically when an agent left the new fields blank on create. The heuristic was specific to NEXO in three ways:
• ID prefixes: NF-PROTOCOL-*, NF-DS-*, NF-AUDIT-*, NF-OPPORTUNITY-*, NF-RETRO-*, R-RELEASE-* — NEXO’s own naming convention.
• Spanish user-verbs: debes, revisar, firmar, llamar, responder, aprobar, …
• Agent keywords: monitor, auditoría diaria, checkpoint, runner, cron.
For the reference install with 468 followups and 40 reminders written in Spanish with NEXO prefixes, this was a reasonable bootstrap. For a third-party agent plugged into the shared Brain — Claude Code, Codex, a hotel-assistant persona, an English-speaking deployment — it meant either misclassification or silent hiding: rows that happened to match NF-PROTOCOL-* would be flagged internal=1 and disappear from default views; English user-facing tasks would fall back to owner=shared and lose their priority signalling.
What 5.8.2 removes
src/db/_classification.py loses _INTERNAL_ID_PATTERNS, _USER_VERB_RX, _WAITING_RX, _AGENT_RX, is_internal_id(), classify_owner(), and classify_task(). What stays is the pure normalisation layer: VALID_OWNERS (the canonical {user, waiting, agent, shared} set), normalise_owner() (clamp to valid values or return None), and normalise_internal() (coerce truthy / numeric input into {0, 1}). normalise_owner() still explicitly rejects the string "nexo" so legacy hardcoding cannot sneak back in through agent input.
src/db/_reminders.py stops calling classify_task. When an agent creates a task and omits the new fields, the stored values are internal=0 and owner=NULL. Explicit overrides still work, still go through normalise_*, still reject invalid values silently.
src/db/_schema.py’s _m40_classification_columns keeps the four _migrate_add_column calls and the four _migrate_add_index calls. The one-shot backfill loop that used to apply classify_task to every owner IS NULL row is gone. Migrations that already ran keep their backfilled data — _migrate_add_column is a no-op when the column exists and never touches row data.
What clients do now
NEXO Desktop has had its own _legacyClassifyOwner and _legacyIsInternalTaskId helpers in main.js since before v5.8.0 (for compat with Brain < 5.8.0). They stay exactly as they are. Now they run on the current Brain too whenever a row comes back with owner=NULL — which is every row an agent inserted without setting it explicitly. Desktop users see no visual change.
Third-party agents that want automatic classification have two options. They can implement their own client-side rules (language, conventions, ID patterns of their choice) and pass the result to nexo_followup_create. Or they can accept the defaults (internal=0, owner=NULL) and let the client renderer deal with it.
Why it matters
The shared Brain has one job: persist memory and expose it through tools. The moment it also encodes opinions about what kind of task a row represents — based on regex over a specific language and a specific ID convention — it stops being shared. It becomes NEXO’s brain, and everyone else borrows it.
v5.8.0 shipped owner=agent instead of owner=nexo for exactly this reason: so deployments render whatever assistant label they carry. v5.8.2 extends the same principle to classification itself. The core knows schemas, not semantics.
Upgrading
Run nexo update. Migration #40 is already applied on any install that passed through v5.8.0, so the schema change is a no-op. Rows previously backfilled keep their values. Agents that used to rely on the Brain’s implicit classification will see internal=0 and owner=NULL on their next create; if that matters to them, they pass the fields explicitly or classify client-side.