The bug this closes

In NEXO Desktop, the Home screen used two separate classifications for each task: internal (hidden from default views, decided by matching the ID against prefixes like NF-PROTOCOL-*, NF-DS-*, NF-AUDIT-*, ...) and owner (“Para ti” / “Esperando” / “NEXO” / “Seguimiento”, decided by regex on the Spanish description). They lived in two independent code paths on the client, so a task could end up marked “Para ti” and “internal” simultaneously — the second flag would hide it from the default view, swallowing the very tasks the user was supposed to act on.

Worse, both heuristics assumed NEXO’s own ID convention and Spanish-speaking users. Any third-party agent plugged into the shared Brain would either see every row as “Seguimiento” or, if their IDs happened to match one of NEXO’s prefixes, have its real user-facing work hidden.

What migration #40 does

Migration #40 adds two columns to followups and reminders:

internal INTEGER DEFAULT 0 — 1 when the row is agent bookkeeping (protocol enforcer, deep-sleep housekeeping, audits, release gates), 0 otherwise.
owner TEXT — one of user, waiting, agent, shared, or NULL.

Both columns are indexed. A one-shot backfill at the end of the migration runs the legacy heuristic against every row where owner IS NULL, so existing installs keep their current Desktop rendering identically. The migration is idempotent — _migrate_add_column is a no-op on the second run, and the backfill filters on owner IS NULL so any value an agent sets afterwards is never overwritten.

Why agent, not nexo

The owner taxonomy is deliberately generic. When the user wants to know “who is on this?”, they want to see either themselves (“Para ti”), an external party they are waiting on (“Esperando”), their AI assistant (“Agente”), or nobody in particular (“Seguimiento”). The actual name of the assistant — NEXO, Claude, Codex, a hotel-concierge persona — is a rendering concern that belongs to the UI, not the database.

NEXO Desktop already knows the configured assistant name via getAssistantLabel(). Pushing “nexo” into the database would have locked every downstream client into our branding. The normalise_owner helper in src/db/_classification.py explicitly rejects the string “nexo” so a well-meaning caller cannot leak it in.

How agents opt in

Four MCP tools gain the two new parameters:

nexo_reminder_create(id, description, ..., internal, owner)
nexo_reminder_update(id, ..., internal, owner, read_token)
nexo_followup_create(id, description, ..., internal, owner)
nexo_followup_update(id, ..., internal, owner, read_token)

Both parameters default to empty strings. When an agent leaves them blank, classify_task in src/db/_classification.py applies the same heuristics the legacy Desktop had, so a vanilla agent keeps sensible behaviour out of the box. When an agent supplies them, the Brain persists the explicit choice. Read output now surfaces the fields in nexo_reminder_get and nexo_followup_get so downstream tools can rely on them without re-running regex.

What NEXO Desktop does with this

The Desktop Home now reads internal and owner straight from the stored columns when Brain ≥ 5.8.0 is detected, and falls back to the legacy regex only when the columns are missing. On top of the stored classification, the Home gains four “who is on this?” chips (Para ti / Esperando / Agente / Seguimiento) with live counts, plus a transparency badge on the “Tareas internas” toggle showing how many rows are currently being hidden by it. The Agente chip label resolves dynamically from the configured assistant name, so non-NEXO deployments never see the word “NEXO” in the UI.

Upgrading

Run nexo update. Migration #40 runs automatically on the next init_db() boot, the backfill pass classifies every pending row, and clients on the new schema start reading internal/owner directly. Clients on pre-5.8.0 Brain keep working because the tools accept empty overrides and the columns default to sensible values.