6.0.1 is a patch on top of yesterday's 6.0.0 release, not a new direction. Two concrete bugs surfaced during the NEXO Desktop 0.12.0 coordination window, and this release closes both of them.

The Strictness Detector Missed Electron

v6.0.0 removed the user-facing NEXO_PROTOCOL_STRICTNESS knob and replaced it with a single rule: interactive TTY sessions run strict, everything else runs lenient. The test was sys.stdin.isatty() and sys.stdout.isatty(). Simple, clean — and wrong for Electron.

NEXO Desktop 0.12.0 spawns claude via Node's child_process.spawn, wiring both stdin and stdout to pipes so the Electron process can stream the conversation into its chat UI. Both isatty() calls return False. v6.0.0 classified that as "headless cron" and fell through to lenient. Effect: every session a user opened in NEXO Desktop silently ran in the laxer mode, skipping the protocol gates that exist for interactive work.

v6.0.1 adds a second interactive signal — NEXO_INTERACTIVE=1 — that the Desktop side exports in the child env:

def _is_interactive() -> bool:
    if os.environ.get("NEXO_INTERACTIVE") == "1":
        return True
    try:
        return bool(sys.stdin.isatty() and sys.stdout.isatty())
    except Exception:
        return False

The override only accepts the exact string "1". Typos like "true", "yes", "on", or a stray space get rejected, so a misconfigured launchd plist cannot silently re-strict a headless cron. NEXO_INTERACTIVE is a contract between Brain and its interactive clients — not user-facing, not documented to operators, and not a resurrection of NEXO_PROTOCOL_STRICTNESS. The actual strictness still follows the interactive test; this variable only signals that a human is present.

Autopilot Sessions Never Saw Their Inbox

The other bug has been there for a while but got visible during 0.12.0 testing. nexo_heartbeat is the tool that drains the session inbox; it fires at the start of every user turn. When a session goes into autopilot — long streams of tool calls with no user messages — the heartbeat never runs. Incoming nexo_send messages from other sessions sit in the inbox while the agent keeps working. Francisco had to manually say "you have a message waiting" during the 0.12.0 release window because the executor session genuinely had no way to notice.

v6.0.1 wires inbox detection into the PostToolUse hook. After every tool call, the hook checks three conditions:

When all three hold, the hook prints a single-line JSON systemMessage telling the agent to run nexo_heartbeat and consume its inbox before continuing. Rate-limited via a new tiny table:

CREATE TABLE IF NOT EXISTS hook_inbox_reminders (
    sid TEXT PRIMARY KEY,
    last_reminder_ts REAL NOT NULL
);

One row per SID, UPSERT on each emission. The rate limit is 60 seconds by default (configurable via NEXO_INBOX_CHECK_THRESHOLD_SECONDS) so long tool-call streams get at most one reminder per minute. Any internal error in the hook collapses to a silent no-op: the tool pipeline is never blocked by this check.

New Column: sessions.last_heartbeat_ts

To answer "how long since the last heartbeat?" cheaply, nexo_heartbeat now stamps sessions.last_heartbeat_ts on every successful invocation. The PostToolUse hook reads that column directly. Migration m42 adds both the column and hook_inbox_reminders idempotently — second-run is a no-op, pre-existing rows keep a NULL stamp (the hook treats NULL as "too new to reason about" and skips the reminder for brand-new sessions).

Tests

Six new pytest modules cover every path: test_protocol_strictness_nexo_interactive.py (override plus typo rejection), test_inbox_autodetect.py (the three gate conditions), test_inbox_reminder_rate_limit.py (emit / silent / emit sequence with the UPSERT row), test_heartbeat_updates_last_ts.py (heartbeat stamps within 1s of time.time()), test_v6_0_1_migration.py (idempotency + schema_migrations row), and test_inbox_autodetect_e2e.py (the full session-A-sends-B, B-on-autopilot-65s scenario). Full suite: 1065 passed, 1 skipped.

Full changelog →