NEXO 7.8.0 — compaction continuity closed end-to-end

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

v7.7 closed the six obedience gaps from the constructor-guardian-90 pass. Francisco flagged the next load-bearing piece right after: PostCompact continuity. The shell script already existed but was never actually registered, so compaction in multi-conversation Desktop was fragile in concrete ways — wrong conv restored, cross-conv leaks possible, the map's pre_compaction / post_compaction on_event rules never fired from the live stream. v7.8 closes all of that.

PostCompact is a real hook now

New wrapper src/hooks/post_compact.py mirrors pre_compact.py. It delegates to post-compact.sh, proxies the shell's stdout verbatim (so Claude Code still receives the systemMessage JSON the shell emits), and records a hook_runs row with the real CLAUDE_SESSION_ID for audit. Both manifests register the new event:

Exact session targeting

Pre-v7.8 pre-compact.sh resolved the session like this:

LATEST_SID=$(sqlite3 ... "SELECT sid FROM sessions ORDER BY last_update_epoch DESC LIMIT 1")

That was actively wrong in multi-conversation Desktop. The "latest active" session frequently belonged to a different conversation than the one Claude Code was compacting in this specific hook invocation.

v7.8 uses CLAUDE_SESSION_ID — the token Claude Code passes to every hook — and resolves it to a NEXO SID via sessions.claude_session_id (primary) or session_claude_aliases (fallback). No LATEST_SID. The sidecar also moves out of /tmp into $NEXO_HOME/runtime/data/compacting-sid.txt so two concurrent compactions on two conversations cannot race on /tmp.

Fail-closed, not fall-back-to-latest

The pre-v7.8 post-compact.sh had a silent "latest checkpoint" fallback: if no exact SID was found, it restored whatever checkpoint was most recently updated. That meant conversation A could hydrate conversation B's state. v7.8 removes that fallback. When the exact SID is missing or mismatches the env-resolved one, the hook emits a small diagnostic systemMessage ("SID mismatch: skipping rehydration to avoid cross-conv leak" or "no checkpoint for this exact session") and exits cleanly. Restoring nothing is better than restoring the wrong conversation.

Additionally the hook cross-checks the sidecar SID against the env-resolved SID. If they disagree, we log a post_compaction event with status: mismatch for observability and refuse the restore.

on_event rules finally fire from the live stream

Hooks run in separate processes so they cannot call EnforcementEngine.raise_event() directly. v7.8 closes that gap with a small NDJSON queue:

This is the bridge that lets the map's pre_compaction → nexo_checkpoint_save and post_compaction → nexo_checkpoint_read rules actually run in the live stream, not only via test harnesses.

compaction_count honesty

The counter now increments only inside the real-restore branch of post-compact.sh (the if [ -n "$CHECKPOINT" ] guard). A dedicated test (test_compaction_count_is_incremented_only_on_real_restore) walks the source and asserts the UPDATE session_checkpoints SET compaction_count = compaction_count + 1 statement lives under the successful-restore path, not under the empty fallback. This was the existing behaviour — the test now pins it so a future refactor cannot quietly start ticking on failed restores.

Tests

New file tests/test_v78_compaction_continuity.py pins 11 invariants across 10 rails:

Pytest 2086 passing (+16 vs v7.7.0). test_manifest_has_nine_hooks replaces the old 8-hook floor.

No Desktop bump

Everything in v7.8 is server-side (Brain hooks + engine consumer). Desktop's MCP contract is unchanged; v0.27.0 continues to ship.

Install verification

Once you run nexo update to 7.8.0, the new PostCompact block is synced into both:

Compact a conversation in Desktop and you should see a post_compact row appear in hook_runs with the correct session_id. If you compact a conv whose SID mismatches the sidecar (edge case where two conversations raced), the systemMessage will say so instead of pretending the restore worked.

Related: full v7.8.0 changelog · v7.7.0 release notes · source on GitHub