# Upgrade Guide — vNEXT

<!-- assembled-by: assemble-next-md -->
<!-- bump: patch -->

## What Changed

The **Autonomous Liveness Reconciler** — a level-triggered control loop that self-heals an autonomous run whose per-topic state file still says `active: true` (with time remaining) but has **no live session** executing it ("dead but marked active"). Born from the 2026-06-16 incident where an `age-limit` recycle on topic 12476 was tagged `midWork:false`, never offered to the resume queue, and silently died with ~15h left — and nothing watched for the contradiction. Ships **DARK on the fleet** (`monitoring.autonomousLivenessReconciler.enabled` OMITTED in ConfigDefaults → the dev-agent gate resolves it live-on-dev / dark-fleet) and **dryRun-FIRST** on dev (the component code-defaults `dryRun:true` — it only LOGS `would-respawn` + a shadow `would-have-capped` until a deliberate `dryRun:false` flip).

This effort ships TWO changes in the same PR (a backstop must not mask a deterministic bug):

1. **Root-cause edge fix (`server.ts` `considerEnqueue`):** at the reap instant, when `getTopicForSession` returns null (the session-name→topic map can be stale/evicted exactly when an `age-limit` recycle should resume), the active-autonomous-run injection branch was silently skipped and the run died. The fix falls back to resolving the topic from the session NAME (the same parse used by the coherence-journal emit) and adopts it ONLY when the run-state file confirms an active run — so the `autonomousRunRemainingForTopic` check still runs and the recycle is offered to the resume queue.
2. **The reconciler (`src/monitoring/AutonomousLivenessReconciler.ts`):** a level-triggered backstop for the residual orphaning the edge fix can't cover (a crash, sleep/wake, a missed enqueue, a drainer that gave up). Per tick it reads the active runs (reusing `activeAutonomousJobs` + `autonomousRunRemainingForTopic`), evaluates 8 candidate criteria (active+remaining+CURRENT-generation, not-paused, not-operator-stopped, not-mid-move, owned+lease-held by THIS machine, no-live-session, not-already-spawning, unambiguous-binding), debounces across N ticks + a window, then respawns through the SAME `spawnSessionForTopic` primitive the ResumeQueueDrainer uses — bounded by a timeout, a bounded pressure gate, the same quota/session-count/migration gates, an atomic in-process claim + actuation-instant recheck + post-spawn settle-kill, and a P19 loop brake with SEPARATED redie / spawn-failure counters (the redie cap unified with the queue's resurrection count). The respawned session is tagged `midWork:true` so a later reaper kill is revived by the queue, attacking the incident root cause. All respawn inputs come from authoritative sources (`_topicResumeMap`, the topic-binding registry realpath-jailed to the agent home) — NEVER the untrusted state file; a missing resume UUID / unsafe cwd / ambiguous binding raises ONE deduped attention item and refuses, never spawns against a guess.

The drainer's shared dep closures (`topicOwnerElsewhere`, `operatorStopSince`, the quota/session-count/migration gates, the live-session resolver) were **extracted to named consts** and passed by the SAME reference into BOTH the drainer and the reconciler — drift-prevention so "the reaper, the drainer, and the reconciler agree" is structurally true. New read-only route `GET /autonomous/liveness` (503 when dark, Bearer-auth'd, content-free: topic ids + counters + conditions only).

## What to Tell Your User

Nothing changes for you yet — this ships off on the fleet and observe-only on the dev agent. Once it's flipped live, if one of my autonomous runs ever loses its live session but still has work + time remaining (a crash, a sleep/wake, a recycle that didn't resume), I'll notice within a couple minutes and quietly bring it back — picking up where it left off — instead of sitting silently dead until you message me. If asked "why did my autonomous run come back by itself?" the answer is: my state said the run was still active but no session was executing it, so the reconciler self-healed it (bounded, capped, and respecting any stop you issued).

## Summary of New Capabilities

Level-triggered autonomous-run self-heal (dark/dryRun-first): detects a run marked active with no live session and respawns it conversation-preserving — debounced, lease-gated, quota-gated, bounded by a pressure gate + a P19 cap that gives up loudly, with an atomic-claim + settle-kill that can't bypass an operator stop. Plus the root-cause edge fix that stops a stale session→topic map from silently dropping an `age-limit` recycle. New read surface: `GET /autonomous/liveness`.

## Evidence

- 41 unit tests (both sides of each of the 8 criteria; the stable-live-vs-blip debounce reset; the two separated counters incl. the queue-unification; the bounded pressure gate deferring-then-acting/escalating; the atomic-claim settle-kill on a stop-during-spawn; the untrusted-state refusals; generation rejection of an obsolete run; durable cap restore; dryRun would-respawn + shadow would-have-capped), 4 integration tests (`GET /autonomous/liveness` → 401 unauth / 503 dark / 200 wired + wiring-integrity that the route serves the SAME live instance), 10 e2e tests (the Phase-1 "feature is alive" 200-not-503 mirroring the production init path; the seeded orphaned→respawn lifecycle; operator-stopped/paused/mid-move/in-queue/not-lease-holder → NOT respawned; pressure deferral then act; cap → one loud attention item; the root-cause parse + run-state confirmation). All 55 green.
- `tsc --noEmit` clean; the no-silent-fallbacks + no-empty-catch ratchets pass (every new fail-toward-safe catch is tagged `@silent-fallback-ok` with a real justification); the notification burst-invariant test passes (all give-up surfaces are per-topic-per-episode deduped).
- Side-effects review artifact: `upgrades/side-effects/autonomous-liveness-reconciler.md`. Spec: `docs/specs/autonomous-liveness-reconciler.md` (converged round 4, approved under standing autonomous authorization).
