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:
nexo_lifecycle_eventreturns a versionedcanonical_planwhen the action is diary-triggering and a live session_id is present. The plan is three actions (resume_session→inject_prompt→stop_session) with stableids and per-actiontimeout_ms. Status:canonical_pending.- Desktop executes the plan inline, then calls the new
nexo_lifecycle_complete_canonicaltool with a per-actionresultsarray. Brain flips the row tocanonical_done(orretryable_errorif any action failed) and recordscanonical_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
- Brain: 24/24 lifecycle tests green (
tests/test_lifecycle_events.py). New coverage: plan_id determinism, complete-canonical done/failed/rejected branches, redelivery after canonical_done, session_diary dedup (the crash-before-confirm case), retry-after-failed-inject, JSON round-trip viaget_lifecycle_event. Full pytest suite: 2050 passing, 10 pre-existing unrelated failures confirmed against main. - Desktop v0.25.0: 673/673 green. New
lifecycle-brain-bridge.test.js(8 tests on the three factories), extendedconversation-lifecycle-service.test.js(4 new canonical tests), newlifecycle-canonical-e2e.test.js(3 e2e scenarios: exactly 1 diary per close, crash-between-inject-and-confirm, retry after failed inject).
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