NEXO 7.5.0 — canonical authority of session-end: Brain decides, Desktop executes, no polling

Published 2026-04-22. Minor release over v7.4.1.

v7.4.1 shipped with an explicit followup — NF-V75-CANONICAL-DIARY-AUTHORITY — acknowledging that diary+stop injection still lived on the Desktop side, inside closeConversationGraceful. Francisco's call was simple: no wait, do it now. v7.5.0 is that work.

What changed

nexo_lifecycle_event is no longer a ledger + reconciliation authority. It is the canonical authority of session-end. Brain now decides the prompt, the sequence, and the timing of diary+stop for every close/delete/archive/app-exit with a live session_id. Desktop v0.25.0 — the closed-source companion at nexo-desktop.com — is the conduit that executes Brain's plan against the live Claude process stdin. No other process has access to that stdin, so only Desktop can execute; but no other process decides what to execute anymore either.

The 2-call contract

No polling, no periodic ticks, no background loops waiting for state to converge. The contract is explicit:

  1. nexo_lifecycle_event returns a versioned canonical_plan when the action is diary-triggering and a live session_id is present. The plan is three actions (resume_sessioninject_promptstop_session) with stable ids and per-action timeout_ms. Status: canonical_pending.
  2. Desktop executes the plan inline, then calls the new nexo_lifecycle_complete_canonical tool with a per-action results array. Brain flips the row to canonical_done (or retryable_error if any action failed) and records canonical_done_at.

That is the whole handshake. The pipeline is one event, one plan, one confirm — three writes against lifecycle_events.

Deterministic plan id

canonical_plan_id is computed as "cpl-" + sha256(event_id + "|v" + plan_version)[:24]. Retries reuse the same id, so clients can deduplicate by (event_id, canonical_plan_id) without consulting Brain. If a replay produces a different plan_id, something is wrong — that is a hard signal, not a degraded path.

Idempotency via session_diary, not just event_id

The harder case is not the first call. It is the second call after Desktop crashed between running the inject and sending the confirm. The event_id still exists on the queue; the model already wrote a session_diary row; Brain has no canonical_done_at yet. A naive re-dispatch would mint a second diary.

v7.5.0's record_lifecycle_event checks the session_diary table directly: for the existing row in canonical_pending, is there any diary row with created_at > canonical_dispatched_at for the same session_id? If yes — short-circuit to already_processed and refuse to re-dispatch. If no — re-hand the same plan (same canonical_plan_id) so Desktop resumes execution, not restarts it. This is the contract test test_session_diary_dedup_guards_against_duplicate_plan_execution.

Seven explicit delivery_status values

The old 5-state machine (processed / already_processed / accepted / retryable_error / rejected) grew by two: canonical_pending (plan dispatched, awaiting confirm) and canonical_done (confirm landed with all actions ok). Every transition is visible: canonical_dispatched_at and canonical_done_at are persistent columns, not derived.

Migration m52

Non-destructive. Adds six columns to lifecycle_events (canonical_plan_id, canonical_plan_version, canonical_actions_json, canonical_dispatched_at, canonical_done_at, canonical_done_results) plus idx_lifecycle_events_plan_id. Pre-v7.5 rows carry NULL in the new columns; no backfill needed.

What stays observational

switch and window-close never produce a canonical_plan, even with a live session_id. A switch away or a window hide is not a real close; issuing diary+stop for it would be wrong. These still land on the ledger for observability but return plain processed. Pinned by test_switch_never_produces_canonical_plan_even_with_session_id and test_window_close_never_produces_canonical_plan_even_with_session_id.

Fallback legacy

Desktop v0.25.0 still carries the hardcoded FALLBACK_SESSION_END_PROMPTS from v0.24.x, but only as a fallback: when Brain is < v7.5 (older install), or when the canonical call times out, or when Brain returns processed without a plan. The v7.5 happy path is the canonical call; the legacy path is a safety net that keeps older Brains working.

Tests

Tool count

262 → 263. The new tool is nexo_lifecycle_complete_canonical; nexo_lifecycle_event and nexo_lifecycle_status keep their signatures but nexo_lifecycle_event now returns the canonical_plan payload when applicable.

What this unlocks

Before v7.5 any change to the session-end prompt needed a Desktop release. Now the prompt lives in src/lifecycle_prompts.py and ships with Brain. That is the load-bearing property: iterating on the diary prompt, the plan shape, or the timeouts no longer requires rebuilding a DMG. The clients just execute whatever plan Brain hands them, versioned and idempotent.

Related: full v7.5.0 changelog · v7.4.1 release notes · source on GitHub