# Nucleic — Agent Resuscitation (Covalence) > **Status (2026-07-13): design draft; nothing built.** This doc specifies automatic failover > of live agent sessions across the Covalence mesh: when the host/runner that owns a running > session goes offline, another eligible host reconstructs and resumes that session from the > data already streamed over the mesh — the mirrored transcript, the agent's native `--resume` > memory, and the durable git state — **without the dead host's cooperation**. Design derived > from a 4-lane code reconnaissance of the transcript-mirror, session-transfer, presence/ > fencing, and git-durability subsystems (exact seams cited inline). > > Scope decisions locked for the first implementation wave: **(1)** target model is > **automatic failover** (elected, single-flight, fencing-guarded), with a manual "Resuscitate" > action shipped first as the proving rung; **(2)** the first wave recovers **transcript + > agent `--resume` memory + committed/pushed git state**; preserving *uncommitted* working-tree > edits is a scoped follow-up deliberately deferred — originally sketched in §7 as a > checkpoint-bundle stream, **now redesigned and specified as the Carbon shard layer in > [CARBON_SHARDING.md](CARBON_SHARDING.md)** (encrypted, content-addressed shards of the repo > delta + worktree/index snapshots, replicated to a deterministic replica set). §7 below is > kept as a superseded stub. Builds on [MESH_TRANSFER.md](MESH_TRANSFER.md) (the transfer engine whose **destination half** is reused verbatim), [COVALENCE_RUNNER.md](COVALENCE_RUNNER.md) (the runner-pool epoch fence this generalizes to session grain), and [CLOUD_RUNTIME.md](CLOUD_RUNTIME.md) (§3.5, the R2 checkpoint whose mesh analogue §7 becomes). Where MESH_TRANSFER is a *cooperative, source-driven* handoff, this doc is its *unilateral, source-dead* counterpart. --- ## 0. Thesis **Session Transfer already moves a live session between hosts. Agent Resuscitation is the same destination-side reconstruction, driven when the source is dead instead of when the source volunteers.** The two differ only in who produces the payload and how the single-writer invariant is protected: | | Session Transfer (built) | Agent Resuscitation (this doc) | |---|---|---| | Source host | **alive**, drives a 2-phase commit | **dead / unreachable** | | Payload producer | live source packages a git bundle + transcript ([`SessionCarbonCopy`](../Sources/NucleicCore/Transfer/SessionCarbonCopy.swift)) | a surviving mesh member reconstructs from its **local mirror** + the git **remote** | | Trigger | user picks "Move to Mac" | host confirmed **offline** → elected resuscitator claims | | Single-writer safety | cooperative `movedToDeviceID` tombstone written by the live source | **new per-session owner-generation fence** — the returning original detects supersession and goes inert *before it writes* | | Destination code | importer, staging, `createFromTransfer`, `activateTransferredSession`, stranded-recovery | **the same code, unchanged** | The reconnaissance confirmed the enabling fact: **a sibling Mac already holds everything a `reconstructController` call needs**, mirrored proactively and byte-faithfully, before any host dies. Resuscitation is therefore mostly *wiring existing primitives together behind a fence*, not a new data path — plus one genuinely new safety primitive (the session epoch) and one deferred fidelity subsystem (the worktree checkpoint). --- ## 1. What the mesh already holds (the reconstruction substrate) Everything below is **implemented today** and is what resuscitation reads instead of a live source. - **Full canonical transcript, byte-faithful, persisted.** Every Mac runs [`TranscriptSyncCoordinator`](../Sources/NucleicCore/Mirror/TranscriptSyncCoordinator.swift) (wired at [`AppStore.swift:8255`](../Sources/NucleicCore/AppStore.swift)), which reconciles each active session's tip (`SessionSummary.lastSeq`) against the local mirror and backfills the gap owner-first (`:62-79`, `:135`). [`TranscriptMirrorStore`](../Sources/NucleicCore/Mirror/TranscriptMirrorStore.swift) persists `///transcript.jsonl` (`:120-124`) with **header as line 0 + every event verbatim** through `TranscriptWriter.appendVerbatim` (`writeFull` `:59`, `append` `:86`), preserving source-assigned seqs. `heldSeq` (`:36`) is rebuilt by scanning on launch, so the mirror survives restarts. The host can already re-serve this mirror — it is fallback #3 in [`AppStore.transcriptEvents`](../Sources/NucleicCore/AppStore.swift) (`:9457`). - **The agent's native `--resume` id and worktree shape.** The mirrored **header** carries `SessionHeader.backendSessionID` ([`Transcript.swift:16`](../Sources/NucleicCore/Transcript.swift)), `worktree` (`:17`), `model` (`:18`), and `nativeTranscriptPath` (`:19`). A Mac that mirrored a session durably holds the id needed for `claude --resume`. - **Session metadata gossip.** [`SessionSummary`](../Sources/NucleicProtocol/Sync/WireMessages.swift) (`:444-510`) carries `model`/`effort`/`auto`/`autoShip`/`shipBranch`/`branch`/`projectID`/`projectName`/`status`/ `lastSeq`/`title`/… — enough to redraw the session and re-configure the backend. - **The wire verb to pull any gap.** [`fetchTranscript`](../Sources/NucleicProtocol/Sync/TranscriptSyncMessages.swift) (`TranscriptFetch` `:27`, `TranscriptChunk` `:52`, `TranscriptFetchComplete` w/ `headerJSON` `:77-81`, `TranscriptUnavailable` `:96`), gated on `WireCapabilities.canSyncTranscripts`. **Two substrate limits that shape the design:** 1. **Only Macs (and other hosts) mirror faithfully; phones do not.** iOS persists a header-less, 1500-event tail (`SessionCache`, "a projection cache, never canonical state") and drops `TranscriptFetchComplete.headerJSON` on the floor ([`HostConnection.swift:641-645`](../ios/NucleicRemote/NucleicRemote/Models/HostConnection.swift)). **A phone can never be a resuscitation source.** Only members advertising a new `canResuscitate` capability (Macs, runners) are. 2. **The native transcript is *not* proactively mirrored.** `backendSessionID` (the id) rides the header, but the native `.jsonl` file the CLI actually resumes from ([`TransferChannel.swift:116-132`](../Sources/NucleicCore/Transfer/TransferChannel.swift)) is only shipped inside a *live* transfer package (`.nativeTranscript` item), never mirrored. So first-wave resuscitation restores agent memory via the **fresh-context fallback** the importer already implements (`SessionTransferImporter.swift:368-390`): the id is dropped, and the agent re-primes from the Nucleic transcript, which is fully present. Proactively mirroring the native file to lift this to true `--resume` fidelity is §7 follow-up. --- ## 2. The three gaps a dead source forces ### 2.1 Uncommitted git work is not durable off the dying host The load-bearing constraint. Agent edits are **uncommitted mid-turn** — commits are deferred to ship time ([`WorktreeManager.swift:717`](../Sources/NucleicCore/Git/WorktreeManager.swift)); there is **no periodic WIP commit, no `nucleic/*` ref push, and no R2 checkpoint** (all three are `CLOUD_RUNTIME.md` §3.5 *plans*; nucleicd's SIGTERM handler is a clean shutdown only, [`Nucleicd.swift:219-236`](../Sources/nucleicd/Nucleicd.swift)). A session commits onto its `nucleic/` branch at exactly three synchronous call sites — ship/integrate (`SessionController.swift:1667`), transfer quiesce (`AppStore.swift:9194`), archived-worktree reclaim (`WorktreeManager.swift:913`) — and even those pushes stay in the host's local `.git` (only `createPR` pushes to a remote, `WorktreeManager.swift:673-703`). **Consequence:** with no new machinery, resuscitation recovers only work that was previously committed *and pushed* (integrate-then-push or a PR), plus the last common ancestor `baseSHA`. Live working-tree edits at the instant of death are gone. The first wave **states this honestly** (§8) and recovers the transcript + agent memory so the resumed agent can *redo* the lost edits from its own recollection; the gap is closed by the **Carbon shard layer** ([CARBON_SHARDING.md](CARBON_SHARDING.md)) — cadence snapshots of each dirty worktree, sharded content-addressed across the mesh, restorable to within one snapshot interval (RPO ≈ 20 s). ### 2.2 There is no per-session fencing epoch in the Swift core The single-writer invariant ([MESH_TRANSFER.md](MESH_TRANSFER.md) locked invariant #1) is upheld today only by the **cooperative** `movedToDeviceID` tombstone ([`Session.swift:122`](../Sources/NucleicCore/Session.swift); `loadSessions` refuses to rebuild a moved session, [`AppStore.swift:1374-1376`](../Sources/NucleicCore/AppStore.swift)) plus the in-memory `transferringSessions` single-flight guard (`AppStore.swift:1181`). That tombstone is written on the **source** row by the **live** source during a 2-phase commit. A *dead* source never writes it — so if a resuscitating host simply rebuilds the session and the original later returns (it was partitioned, not destroyed), **both would be live writers**: split-brain. The primitive that already solves this exists, but only in the **runner pool**, at host grain: a monotonic `epoch` stamped at boot ([`pool.ts:72-74`](../cloud/nucleic-runner/src/pool.ts), `bumpEpoch` `:203-205`); a heartbeat with a stale epoch gets **HTTP 409** (`:377-389`); nucleicd maps 409 → `.fenced` → `exit(75)` **before it writes** ([`Nucleicd.swift:196-204`](../Sources/nucleicd/Nucleicd.swift), `:360-378`). §4 generalizes exactly this to **session grain in the Swift core** — the one new safety primitive the feature requires. ### 2.3 Offline detection is edge-triggered, with no grace timer `PeerClient` concludes "offline" the instant a channel's event stream ends ([`PeerClient.swift:762-768`](../Sources/NucleicCore/Sync/PeerClient.swift)); the only backoff is the reconnect delay (`:769-773`). There is no level-triggered "dead for N seconds" signal — a transient blip flips the presence dot gray and back. Resuscitation must add its own debounce, and should gate on the **authoritative** relay signal: `RelayPresence.hasHost == false` for the host's room ([`RelayTransport.swift:99`](../Sources/NucleicProtocol/Sync/RelayTransport.swift), the fail-fast at `:586-587`), which reflects whether the host holds *any* socket in its own room (`roomID == DeviceIdentity.hostID`, [`RelayAccess.swift:68-69`](../Sources/NucleicCore/Sync/RelayAccess.swift)) rather than one dialer's reachability. --- ## 3. Topology ``` ┌─────────── the dead host (Mac or runner) ──────────┐ │ nucleicd / NucleicApp — was single-writer of S │ ✗────┤ SessionController(S) · TranscriptWriter(S) │ ← process dies / partitions │ worktree(S) on local disk (uncommitted work lost) │ └────────────────────────────────────────────────────┘ │ (before death) streamed over the mesh ▼ relay room ── RelayPresence: hasHost=false ──► authoritative "offline" signal │ ┌─────────────── surviving mesh members (each already mirrors S) ───────────────┐ │ Mac A Mac B runner R iPhone (mirror: tail only)│ │ TranscriptMirrorStore(S) = header + all events verbatim, persisted │ │ SessionSummary(S) gossip = model/effort/branch/project/lastSeq │ └───────────────────────────────┬───────────────────────────────────────────────┘ │ 1. detect offline (debounced + relay-gated) │ 2. ELECT one resuscitator (deterministic, single-flight) ▼ ┌── elected resuscitator (Mac B, say) ──┐ │ 3. claim: bump session owner epoch │ ← the fence (§4) │ 4. ensure project (match / createProject clone from origin URL) │ 5. place mirrored transcript on disk │ │ 6. materialize worktree from git remote │ (committed+pushed only, wave 1) │ 7. reconstructController(S) + broadcast │ (reuses AppStore.swift:1568) └───────────────────────┬─────────────────┘ │ S now live here, epoch = N+1 ┌───────────────────────────┘ ▼ original host returns ──► learns epoch advanced (gossip / DB reconcile) ──► self-tombstones, refuses to rebuild S (loadSessions guard) ──► inert. No split-brain. ``` --- ## 4. The session owner-generation fence (the one new safety primitive) **Goal:** make "who owns session S" a monotonic, mesh-visible fact, so a unilateral seizure of a dead peer's session is safe and a returning original demotes itself before writing. ### 4.1 Model Every session gains a durable **ownership stamp** `(ownerDeviceID, ownerGeneration)`: - `ownerGeneration: UInt64` — monotonic, starts at 0 at `createSession`. A **claim** (the only writer of this field other than create) sets `ownerDeviceID = self`, `ownerGeneration = max(seen generations) + 1`. - The current writer must hold the **highest generation it has ever observed** for S. Any host that observes — via `SessionSummary` gossip (§5) or a DB reconcile on reconnect — a generation **greater than its own** for a session it is running **immediately quiesces and self-tombstones that session** (`ownerGeneration` mismatch ⇒ "I have been superseded"), the session-grain analogue of nucleicd's 409 → `exit(75)`. It does *not* fight for the session; monotonicity makes a late writer harmless (the transfer engine's `updatedAt`-newest-wins discipline, generalized). This is deliberately **not** consensus. It is the runner-pool fence (§2.2) lifted to session grain: a single scalar that only advances, checked at two moments — before a claim and on every observation of a higher value. ### 4.2 Anti-split-brain argument (the correctness core) - **Two hosts try to resuscitate simultaneously.** Both compute `max+1` from the same gossiped max, so they *tie* on generation. The tie is broken by `ownerDeviceID` (lexicographically lowest hostID wins — deterministic, no coordination), and the loser observes the winner's stamp (same generation, different owner, winner's hostID lower) and stands down. Election (§6) makes a double-claim rare; the fence makes it *safe* when it happens. - **The original was only partitioned and returns mid-turn.** On reconnect it receives the gossiped `SessionSummary(S)` with `ownerGeneration = N+1 > N`. Before its `SessionController` emits another event, the observation handler quiesces it (interrupt the turn, flush transcript) and writes the tombstone. Its own `loadSessions` guard (`AppStore.swift:1374-1376`) then keeps it inert across any future relaunch. **Window of overlap:** bounded by gossip latency; during it the original may emit events that never reach the new owner's canonical stream — acceptable because the new owner's transcript is authoritative from `lastSeq` forward and the original's post-death events are discarded, not merged (no CRDT, per invariant #1). Documented as a known limit (§8). - **A resuscitated session's writes never collide with a stale mirror re-serve.** Mirrors are read-only; only a controller writes, and only the highest-generation owner has one. ### 4.3 Persistence + reconstruction guard - **GRDB v25:** `ALTER TABLE session ADD owner_device_id TEXT`, `ADD owner_generation INTEGER NOT NULL DEFAULT 0` (mirrors the v23/v24 additive column pattern, [`GRDBMetadataStore.swift:203-238`](../Sources/NucleicCore/Persistence/GRDBMetadataStore.swift)). Back-fill: existing rows are generation 0 owned by this device (they *are* the owner today). - **Store methods:** `claimSession(id:owner:generation:)` (atomic CAS: write only if `generation > current`), `sessionOwnership(id:)`. Sits beside `tombstoneSession` / `activateTransferredSession` (`:486-510`). - **`reconstructController` guard:** extend the existing `movedToDeviceID` skip (`AppStore.swift:1374-1376`) so `loadSessions` also refuses to rebuild a session whose persisted `ownerDeviceID != self` at a generation this host did not write — the returning-original inert path. - **Reuse the tombstone for demotion:** a superseded host writes `movedToDeviceID = newOwner` (exactly the existing "Moved to " read-only surface), so the UX for "this session left me" is already built — resuscitation just reaches it via the fence instead of a cooperative move. --- ## 5. Wire additions (additive, capability-gated — the COVALENCE_RUNNER §11.4 recipe) All changes are additive, decode-defaulted, and gated so old peers bounce cleanly (`ClientMsg` throws on unknown tags; `HostMsg` falls back to `.unknown`; new struct fields use `decodeIfPresent`). New payloads land in a new `ResuscitationMessages.swift` beside `TransferMessages.swift`. 1. **Capabilities** ([`WireMessages.swift`](../Sources/NucleicProtocol/Sync/WireMessages.swift)): - `WireClientCapabilities.canResuscitate` — a member that faithfully mirrors and can host (Mac/runner; **never** a phone). Advertised in `Hello`/`Welcome`. - `WireCapabilities.canBeResuscitated` — a host permits its sessions to be reconstructed elsewhere on death (default on for runners; a per-project/per-host opt-out in Settings, §9). 2. **Ownership on the gossip** (`SessionSummary`, additive trailing fields, `:506-510` pattern): `ownerDeviceID: String?`, `ownerGeneration: UInt64?` (decode-default `nil`/`0`). This is the whole fence's transport — no new verb needed for the *observation* path; it rides the existing `sessionUpdated`/`sessionList` broadcasts. 3. **Clonable origin URL on the descriptor.** `ProjectDescriptor.normalizedRemote` ([`TransferMessages.swift:161-189`](../Sources/NucleicProtocol/Sync/TransferMessages.swift)) is a *fingerprint* (scheme + creds stripped, `TransferGit.swift:90-106`) — **not clonable**. Add `ProjectDescriptor.cloneURL: String?` (the real `origin`) so a resuscitator that has never seen the project can `createProject` it (`AppStore.swift:9676-9696`). Gossiped in a new lightweight `ResuscitableSessionInfo` (below). 4. **Claim coordination verbs** (for automatic mode; the manual rung needs none of these): - `ClientMsg.resuscitationClaim(ResuscitationClaim {sessionID, ownerDeviceID, ownerGeneration, roomID})` — broadcast to the mesh at the moment of claim, gated on the *receivers* advertising `canResuscitate`. Purely advisory (the fence in §4 is the real guard); it lets other candidates stand down *before* wasting a rebuild. - `HostMsg.resuscitableSessions([ResuscitableSessionInfo])` — a member's answer to "what sessions of host X do you hold a complete mirror of?" (`{sessionID, heldSeq, hasNativeTranscript, cloneURL, ownerGeneration}`), so the elector picks the most-complete source and confirms coverage before claiming. Answered from `TranscriptMirrorStore.heldSeq`. 5. **Optional native-memory pull** (bridges toward §7 fidelity without full checkpointing): `ClientMsg.fetchNativeTranscript(sessionID)` → chunked reply, so a resuscitator that lacks the native `.jsonl` can pull it from a *peer that transferred the session earlier* if one exists. Absent a holder ⇒ fresh-context fallback (§1 limit 2). Gated on a new `canServeNativeTranscript`. Per the recipe: each new `ClientMsg`/`SyncClient.Event` case must be handled in all four exhaustive switches — `ConnectionHandler.messageLoop`, `SyncClient.messageLoop`, iOS `RemoteStore` (×2), iOS `HostConnection` — the iOS ones visible only under `xcodebuild` (COVALENCE_RUNNER §11.2). On a phone every new case is **inert** (a phone neither resuscitates nor is resuscitated). --- ## 6. Detection, election, and the reconstruction sequence ### 6.1 Confirming "truly offline" (`ResuscitationMonitor`, new, in NucleicCore) A host is a **resuscitation candidate target** when, for a debounce window `T_dead` (default 45 s, Settings-tunable), **both**: - `PeerClient` presence for that host stays `connected == false` across the whole window (not a single blip), **and** - the relay reports `RelayPresence.hasHost == false` for that host's room (`RelayTransport.swift:99`) — the authoritative signal that the host holds no socket anywhere, not just that *this* dialer can't reach it. Gating on the relay signal is what distinguishes "the host died" from "my link to it flapped." If the relay itself is unreachable to the observer, it **does not resuscitate** (fail-safe: a network partition on the observer's side must never trigger a seizure) — it waits. ### 6.2 Election (single-flight, deterministic, no coordinator) Among mesh members that (a) advertise `canResuscitate`, (b) hold a **complete** mirror of S (`heldSeq == SessionSummary.lastSeq`), and (c) can reach the relay, the resuscitator is chosen by a pure function — **lowest `hostID`** by default (deterministic, requires no messages), or the **user-designated backup host/runner** if one is configured (§9). Once the Carbon shard layer ships, eligibility gains a criterion and the tie-break gains a preference: prefer **replica-set members holding the newest mesh-durable `worktreeSnapshot` head** for S (CARBON_SHARDING §9.2), then lowest `hostID`. The winner claims; others observe the claim (§5.4) or the bumped generation (§4) and stand down. `resuscitatingSessions: Set` is the in-memory single-flight guard, mirroring `transferringSessions` (`AppStore.swift:1181`). Ties/races are *safe* by §4.2, not merely *rare* by election. ### 6.3 Reconstruction (reuses the transfer destination half wholesale) Once elected and claimed, the resuscitator runs a `SessionResuscitator` (new, NucleicCore) that is the importer's `stage → activate` pipeline with a **local/remote producer** instead of a network source: 1. **Claim** — `store.claimSession(id:owner:self:generation:max+1)` (§4.3); add to `resuscitatingSessions`. 2. **Ensure the project** — `resolveTransferProject` (`AppStore.swift:9063-9077`) by UUID / root commit / normalized remote; if absent, `createProject(cloneURL)` (§5.3) — the one reason we add `cloneURL` to the gossip. 3. **Place the transcript** — copy the local `TranscriptMirrorStore` file to `transcriptsDir//transcript.jsonl` and rewrite its header for the new worktree via `TranscriptHeaderRewriter.destinationHeader` (exactly `SessionTransferImporter.swift:358-397`). If `heldSeq < lastSeq`, top up the gap from another mirror-holder via `fetchTranscript` first. 4. **Materialize the worktree** — `git fetch origin && git worktree add ` at the last durably-recoverable SHA (the pushed branch tip, else `baseSHA`). Wave 1 recovers committed+pushed state only (§2.1); once Carbon shards are held for S, this step is replaced by the shard restore (CARBON_SHARDING §9.1: apply `history` + `worktreeSnapshot` packs, branch at the recorded tip, re-dirty the tree and replay the index — uncommitted and staged edits byte-recovered). 5. **Native memory** — if a peer can serve `.jsonl` (§5.5) place it and keep the id; else drop the id → fresh-context fallback (`SessionTransferImporter.swift:368-390`), agent re-primes from the Nucleic transcript. 6. **Reconstruct + broadcast** — `reconstructController(for:in:events:)` (`AppStore.swift:1568-1588`) → register → observe → broadcast `sessionUpdated` with the new `ownerDeviceID`/`ownerGeneration`. Stamp `arrivedFrom` (provenance, reusing the v24 columns) so the UI shows "Resuscitated from ". 7. **Clear** single-flight; the session is now live and owned here. Idempotency & crash-recovery reuse the transfer machinery: a `session_transfer`-style lock row (direction `.resuscitating`) makes step 6 replayable, and `recoverInterruptedTransfers` (`AppStore.swift:8982-9011`) is extended to finish or discard a half-done resuscitation on relaunch, exactly as it does `.tombstoned`/`.ready` today. --- ## 7. Deferred fidelity — **superseded by [CARBON_SHARDING.md](CARBON_SHARDING.md)** > **This section's original design (streaming whole checkpoint bundles, keep-newest-only) is > superseded.** The §2.1 gap is now closed by the **Carbon shard layer**, specified in full in > CARBON_SHARDING.md. Rationale for the redesign (CARBON_SHARDING §9): a bundle per checkpoint > re-ships the entire snapshot and is opaque — no dedup across checkpoints, no set-difference > reconciliation, no partial backfill — whereas content-addressed shards of the *incremental > pack* ship only changed objects, satisfying "no constant git ops, no syncing of entire > worktrees." What resuscitation needs to know about the shard layer: - **Producer (owning host):** `CarbonSnapshotter` — on a cadence (default 20 s while dirty) + at turn boundaries, a throwaway-index `git write-tree` snapshot capturing **both** the working files and the staged index (untracked captured, `.gitignore` respected; the `finalize(.auto)` recipe at `WorktreeManager.swift:449-476` generalized so the session branch/index are never touched) → thin incremental pack → ≤2 MiB encrypted content-addressed shards + a generation-stamped manifest (CARBON_SHARDING §6). - **Mirror (replica set):** `CarbonShardStore` + `CarbonSyncCoordinator`, the `TranscriptMirrorStore`/`TranscriptSyncCoordinator` pattern with set-difference reconciliation (CARBON_SHARDING §7–8). Shards go to a deterministic **replica set** (R=2 beyond the owner, hybrid rendezvous + pinned always-on members), not to every member. - **Restore:** §6.3 step 4 is replaced by CARBON_SHARDING §9.1 — apply the manifest-chain packs, branch at the recorded tip, two-tree `read-tree` to re-dirty the worktree, replay the index. RPO = one snapshot cadence, published honestly (§8). - **Fencing composes:** manifests carry `(ownerDeviceID, ownerGeneration)`; a stale-generation manifest never advances a head (CARBON_SHARDING §10), so the shard layer inherits §4's fence rather than weakening it. - The shard layer's `nativeTranscript` stream also supersedes the §5.5 peer-pull as the durable path to true `--resume` fidelity. It remains additive to everything above: a resuscitation with no shards held simply falls back to committed+pushed state. --- ## 8. Risks / honesty notes - **Lost uncommitted work is the headline limit (wave 1).** Copy must say plainly: *"Resuscitation restores your session's conversation, the agent's memory, and code you had committed and pushed. Uncommitted edits at the moment of the crash are re-derived by the agent from its restored memory, not byte-recovered"* — until the Carbon shard layer ships ([CARBON_SHARDING.md](CARBON_SHARDING.md)); after it, the honest claim becomes "recovered to within one snapshot interval (~20 s)." Never imply a crash is lossless. - **Single-writer is protected by monotonicity, not consensus.** The correctness rests entirely on §4.2. The bounded overlap window (a partitioned original emitting events after the new owner claims, before it sees the gossip) is real and its events are *discarded, not merged* — consistent with invariant #1, but it means a few seconds of the original's post-death output can be lost. This must be a documented, tested property, not an accident. - **Never resuscitate on the observer's own partition.** If the observer can't reach the relay it must **not** seize (it can't tell dead from partitioned). Fail-safe = wait. This is the inverse of the runner fence and the easiest thing to get dangerously wrong. - **Phones are viewers only.** A phone can *see* a session was resuscitated and *trigger* the manual action against an eligible Mac/runner, but never be a source or target (§1 limit 1). - **Credentials must already be on the resuscitator.** A rebuilt session's next turn needs the agent credentials; the credential mesh (COVALENCE_RUNNER §6) already puts them on every eligible host, but a resuscitator lacking a kind must surface the existing `credentialNeeded` path, not fail silently. - **Thundering herd.** N eligible Macs all detecting death at once is bounded by §6.2 election + §4.2 fencing; still, the claim broadcast (§5.4) should carry a small deterministic jitter by `hostID` so the lowest-hostID host claims first and the rest observe before rebuilding. - **Automatic mode is opt-in and reversible.** Ship the manual "Resuscitate" action first (§6.3 steps 2-7, user-initiated); enable automatic failover per-host/per-project in Settings only after the fence is proven, because an over-eager auto-seizure is worse than a missed one. - **Runner interplay.** A runner already self-fences on the pool epoch (`Nucleicd.swift:196`); the session fence composes with it (a runner that `exit(75)`s has its sessions resuscitated by the mesh; a runner that returns finds its sessions superseded and stays inert). Keep the two fences independent — host-grain (pool) and session-grain (this doc) — never conflate them. --- ## 9. Settings surface (Settings ▸ Covalence ▸ Resuscitation) - **Master toggle** `nucleic.resuscitation.enabled` (default off in wave 1; the manual action is always available when a host is confirmed offline). - **Automatic failover** `nucleic.resuscitation.auto` + a **designated backup** picker (a host/ runner from the mesh roster; blank ⇒ lowest-hostID election, §6.2). - **Dead-debounce** `nucleic.resuscitation.deadSeconds` (default 45, clamp 15…300). - **Per-host / per-project opt-out** feeding `canBeResuscitated` (§5.1) — some projects (org policy, side effects) must never auto-restart elsewhere. - Read-only status: which of this host's sessions are eligible (mirror-complete on ≥1 peer), and a "Resuscitated from " provenance badge on arrived sessions (reusing the `arrivedFrom` chevron, MESH_TRANSFER Phase 5). --- ## 10. Phasing (ordered work queue) 1. **The session fence (§4).** GRDB v25 columns + `claimSession`/`sessionOwnership` + the `reconstructController`/`loadSessions` supersession guard + `SessionSummary` ownership fields (§5.2). Tests first — this is the safety core and must be bullet-proof before anything seizes. 2. **Manual reconstruction (§6.3 steps 2-7) behind the fence.** A user-invoked "Resuscitate on this Mac" against a confirmed-offline host, reusing the importer pipeline with the local mirror + `cloneURL` producer. Proves the whole data path with a human in the loop. Includes `cloneURL` on the descriptor (§5.3) and the resuscitation lock + `recoverInterruptedTransfers` extension (§6.3). 3. **Detection + election (§6.1-6.2).** `ResuscitationMonitor` (relay-gated debounce) + deterministic election + the claim/`resuscitableSessions` verbs (§5.4). Still manual-confirm by default. 4. **Automatic failover (§9).** Flip election to auto-claim behind the opt-in toggle + designated backup; jitter + thundering-herd hardening; the "Resuscitated from " UX (Mac + iOS, the latter `xcodebuild`-only). 5. **Native-memory pull (§5.5)** — lift the fresh-context fallback to true `--resume` where a peer can serve the native transcript. (Interim rung only — the Carbon `nativeTranscript` stream makes this durable rather than peer-luck; skip straight to 6 if sequencing allows.) 6. **The Carbon shard layer** ([CARBON_SHARDING.md](CARBON_SHARDING.md), its own phased queue) — the uncommitted-work fidelity upgrade; the largest, last, and fully additive. Its phase 4 is the integration point back into §6.3 step 4 and the §6.2 election preference. --- ## 11. Handoff — the seams to build against Everything the four-lane reconnaissance surfaced, so the next agent starts from map, not search. **Reuse verbatim (destination half of transfer):** - Importer pipeline & staging — [`SessionTransferImporter.swift`](../Sources/NucleicCore/Transfer/SessionTransferImporter.swift): `receiveOffer` `:61-124`, `stage` (transcript place `:358-366`, native `:368-390`, header rewrite `:392-397`, `staged-session.json` `:408-413`), `recoverableInboundTransfers` `:282-304`, `activateRecoveredTransfer` `:311-327`. - `createFromTransfer` (verify → fetch bundle → worktree add) — [`WorktreeManager.swift:834-865`](../Sources/NucleicCore/Git/WorktreeManager.swift). - `activateTransferredSession` / `reconstructController` (row → runnable controller) — [`AppStore.swift`](../Sources/NucleicCore/AppStore.swift) `:9082-9089`, `:1568-1588` (transcript mandatory `:1571-1574`; worktree needs path+branch+baseSHA `:1577-1581`). - Persistence pattern — GRDB v23/v24 additive columns + `session_transfer` lock ([`GRDBMetadataStore.swift:203-238`](../Sources/NucleicCore/Persistence/GRDBMetadataStore.swift)), `SessionTransferLock` states, `tombstoneSession` `:486-497`, `activateTransferredSession` `:499-510`. **Read as substrate (mirror + gossip + native path):** - [`TranscriptMirrorStore.swift`](../Sources/NucleicCore/Mirror/TranscriptMirrorStore.swift) (`heldSeq` `:36`, `writeFull` `:59`, path `:120-124`), [`TranscriptSyncCoordinator.swift`](../Sources/NucleicCore/Mirror/TranscriptSyncCoordinator.swift) (reconcile `:62-79`), wired `AppStore.swift:8255`. - `SessionHeader.backendSessionID`/`worktree`/`model` — [`Transcript.swift:16-19`](../Sources/NucleicCore/Transcript.swift). - `SessionSummary` `:444-510`, `fetchTranscript` verbs [`TranscriptSyncMessages.swift`](../Sources/NucleicProtocol/Sync/TranscriptSyncMessages.swift) `:27/:52/:77-81`, native resume path [`TransferChannel.swift:116-132`](../Sources/NucleicCore/Transfer/TransferChannel.swift), `ClaudeCodeBackend.resume` [`:452-453`](../Sources/NucleicCore/Claude/ClaudeCodeBackend.swift). **Model to generalize (host-grain fence → session-grain):** - Runner epoch — [`pool.ts:72-74/203-205/377-389`](../cloud/nucleic-runner/src/pool.ts); nucleicd honor path [`Nucleicd.swift:185-204/360-378`](../Sources/nucleicd/Nucleicd.swift) (409 → `exit(75)`). **Trigger inputs:** - Presence — [`PeerClient.swift`](../Sources/NucleicCore/Sync/PeerClient.swift) presence `:221`, drop `:762-768`, backoff `:769-773`; `AppStore` `peerPresenceTask` `:8262-8268`, `meshPeers` `:1064`. Relay — [`RelayTransport.swift:99/586-587`](../Sources/NucleicProtocol/Sync/RelayTransport.swift), [`RelayListener.swift:112-156`](../Sources/NucleicCore/Sync/RelayListener.swift), room = hostID [`RelayAccess.swift:68-69`](../Sources/NucleicCore/Sync/RelayAccess.swift). **Trip-wires (learned from transfer's own hardening, MESH_TRANSFER Phase 5):** - `ClientMsg` decode **throws** on unknown tags — gate every new client→host verb on an advertised `WireCapabilities` flag, or old hosts disconnect (COVALENCE_RUNNER §11.2). - A new `ClientMsg`/`SyncClient.Event` case breaks **four** exhaustive switches incl. two iOS ones the `swift build` won't catch — build iOS with `xcodebuild` (COVALENCE_RUNNER §11.1). - The "never runs in two places" gap the transfer review found (`transferringSessions`, tombstone hides from `sessionSummaries`/`snapshot`, refuses `sendInput`) is *exactly* the property the fence must give resuscitation — port that discipline, don't reinvent it (MESH_TRANSFER Phase 5 hardening). - `resolveTransferProject`'s UUID fast-path must not match an archived project; the importer validates `transferID`/`sessionID`/branch as safe path components before building any filesystem path (path-traversal fix, MESH_TRANSFER Phase 5) — reuse both guards. ## 12. Tests (required deliverables, per house discipline) - **Fence unit tests:** `claimSession` CAS monotonicity; two-host simultaneous claim resolves to the lowest hostID; a returning-original observation of a higher generation quiesces + tombstones + refuses relaunch; `loadSessions` never rebuilds a superseded session. This is the correctness proof for §4.2 and must exist before step 2 lands. - **Reconstruction path** over a real GRDB + real temp git repos + real `TranscriptMirrorStore` (the loopback style of `SessionTransferTests`): mirror-complete session → reconstruct → runnable controller, byte-faithful transcript, fresh-context fallback when no native transcript, and `createProject`-clone when the project is absent. - **Detection/election:** relay-gated debounce (blip does not trigger; sustained offline + `!hasHost` does; observer-side relay outage does **not** seize); deterministic election single-flight. - **Crash recovery:** a half-done resuscitation is finished-or-discarded by `recoverInterruptedTransfers` on relaunch, never double-running S. - **Cross-platform:** protocol + core suites green on **Linux** (the runner is a first-class resuscitator/target) and Darwin; iOS builds under `xcodebuild` with every new case inert.