37 KiB
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
--resumememory, 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
--resumememory + 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 (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 (the transfer engine whose destination half is reused verbatim), COVALENCE_RUNNER.md (the runner-pool epoch fence this generalizes to session grain), and 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) |
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(wired atAppStore.swift:8255), which reconciles each active session's tip (SessionSummary.lastSeq) against the local mirror and backfills the gap owner-first (:62-79,:135).TranscriptMirrorStorepersists<root>/<sourceDeviceID>/<sessionID>/transcript.jsonl(:120-124) with header as line 0 + every event verbatim throughTranscriptWriter.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 inAppStore.transcriptEvents(:9457). - The agent's native
--resumeid and worktree shape. The mirrored header carriesSessionHeader.backendSessionID(Transcript.swift:16),worktree(:17),model(:18), andnativeTranscriptPath(:19). A Mac that mirrored a session durably holds the id needed forclaude --resume. - Session metadata gossip.
SessionSummary(:444-510) carriesmodel/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(TranscriptFetch:27,TranscriptChunk:52,TranscriptFetchCompletew/headerJSON:77-81,TranscriptUnavailable:96), gated onWireCapabilities.canSyncTranscripts.
Two substrate limits that shape the design:
- 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 dropsTranscriptFetchComplete.headerJSONon the floor (HostConnection.swift:641-645). A phone can never be a resuscitation source. Only members advertising a newcanResuscitatecapability (Macs, runners) are. - The native transcript is not proactively mirrored.
backendSessionID(the id) rides the header, but the native<backendSessionID>.jsonlfile the CLI actually resumes from (TransferChannel.swift:116-132) is only shipped inside a live transfer package (.nativeTranscriptitem), 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--resumefidelity 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); 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). A session commits onto its
nucleic/<slug> 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) — 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 locked invariant #1) is upheld
today only by the cooperative movedToDeviceID tombstone
(Session.swift:122; loadSessions refuses to rebuild a
moved session, AppStore.swift:1374-1376) 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,
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,
: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); 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, 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)
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 atcreateSession. A claim (the only writer of this field other than create) setsownerDeviceID = 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
SessionSummarygossip (§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 (ownerGenerationmismatch ⇒ "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'supdatedAt-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+1from the same gossiped max, so they tie on generation. The tie is broken byownerDeviceID(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)withownerGeneration = N+1 > N. Before itsSessionControlleremits another event, the observation handler quiesces it (interrupt the turn, flush transcript) and writes the tombstone. Its ownloadSessionsguard (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 fromlastSeqforward 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). 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 ifgeneration > current),sessionOwnership(id:). Sits besidetombstoneSession/activateTransferredSession(:486-510). reconstructControllerguard: extend the existingmovedToDeviceIDskip (AppStore.swift:1374-1376) soloadSessionsalso refuses to rebuild a session whose persistedownerDeviceID != selfat 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.
- Capabilities (
WireMessages.swift):WireClientCapabilities.canResuscitate— a member that faithfully mirrors and can host (Mac/runner; never a phone). Advertised inHello/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).
- Ownership on the gossip (
SessionSummary, additive trailing fields,:506-510pattern):ownerDeviceID: String?,ownerGeneration: UInt64?(decode-defaultnil/0). This is the whole fence's transport — no new verb needed for the observation path; it rides the existingsessionUpdated/sessionListbroadcasts. - Clonable origin URL on the descriptor.
ProjectDescriptor.normalizedRemote(TransferMessages.swift:161-189) is a fingerprint (scheme + creds stripped,TransferGit.swift:90-106) — not clonable. AddProjectDescriptor.cloneURL: String?(the realorigin) so a resuscitator that has never seen the project cancreateProjectit (AppStore.swift:9676-9696). Gossiped in a new lightweightResuscitableSessionInfo(below). - 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 advertisingcanResuscitate. 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 fromTranscriptMirrorStore.heldSeq.
- Optional native-memory pull (bridges toward §7 fidelity without full checkpointing):
ClientMsg.fetchNativeTranscript(sessionID)→ chunked reply, so a resuscitator that lacks the native<backendSessionID>.jsonlcan 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 newcanServeNativeTranscript.
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:
PeerClientpresence for that host staysconnected == falseacross the whole window (not a single blip), and- the relay reports
RelayPresence.hasHost == falsefor 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<SessionID> 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:
- Claim —
store.claimSession(id:owner:self:generation:max+1)(§4.3); add toresuscitatingSessions. - Ensure the project —
resolveTransferProject(AppStore.swift:9063-9077) by UUID / root commit / normalized remote; if absent,createProject(cloneURL)(§5.3) — the one reason we addcloneURLto the gossip. - Place the transcript — copy the local
TranscriptMirrorStorefile totranscriptsDir/<sessionID>/transcript.jsonland rewrite its header for the new worktree viaTranscriptHeaderRewriter.destinationHeader(exactlySessionTransferImporter.swift:358-397). IfheldSeq < lastSeq, top up the gap from another mirror-holder viafetchTranscriptfirst. - Materialize the worktree —
git fetch origin && git worktree add <path> <branch>at the last durably-recoverable SHA (the pushed branch tip, elsebaseSHA). 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: applyhistory+worktreeSnapshotpacks, branch at the recorded tip, re-dirty the tree and replay the index — uncommitted and staged edits byte-recovered). - Native memory — if a peer can serve
<backendSessionID>.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. - Reconstruct + broadcast —
reconstructController(for:in:events:)(AppStore.swift:1568-1588) → register → observe → broadcastsessionUpdatedwith the newownerDeviceID/ownerGeneration. StamparrivedFrom(provenance, reusing the v24 columns) so the UI shows "Resuscitated from ". - 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
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-indexgit write-treesnapshot capturing both the working files and the staged index (untracked captured,.gitignorerespected; thefinalize(.auto)recipe atWorktreeManager.swift:449-476generalized 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, theTranscriptMirrorStore/TranscriptSyncCoordinatorpattern 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-treeto 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
nativeTranscriptstream also supersedes the §5.5 peer-pull as the durable path to true--resumefidelity.
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); 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
credentialNeededpath, 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
hostIDso 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 thatexit(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
arrivedFromchevron, MESH_TRANSFER Phase 5).
10. Phasing (ordered work queue)
- The session fence (§4). GRDB v25 columns +
claimSession/sessionOwnership+ thereconstructController/loadSessionssupersession guard +SessionSummaryownership fields (§5.2). Tests first — this is the safety core and must be bullet-proof before anything seizes. - 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 +
cloneURLproducer. Proves the whole data path with a human in the loop. IncludescloneURLon the descriptor (§5.3) and the resuscitation lock +recoverInterruptedTransfersextension (§6.3). - Detection + election (§6.1-6.2).
ResuscitationMonitor(relay-gated debounce) + deterministic election + the claim/resuscitableSessionsverbs (§5.4). Still manual-confirm by default. - 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). - Native-memory pull (§5.5) — lift the fresh-context fallback to true
--resumewhere a peer can serve the native transcript. (Interim rung only — the CarbonnativeTranscriptstream makes this durable rather than peer-luck; skip straight to 6 if sequencing allows.) - The Carbon shard layer (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: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.activateTransferredSession/reconstructController(row → runnable controller) —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_transferlock (GRDBMetadataStore.swift:203-238),SessionTransferLockstates,tombstoneSession:486-497,activateTransferredSession:499-510.
Read as substrate (mirror + gossip + native path):
TranscriptMirrorStore.swift(heldSeq:36,writeFull:59, path:120-124),TranscriptSyncCoordinator.swift(reconcile:62-79), wiredAppStore.swift:8255.SessionHeader.backendSessionID/worktree/model—Transcript.swift:16-19.SessionSummary:444-510,fetchTranscriptverbsTranscriptSyncMessages.swift:27/:52/:77-81, native resume pathTransferChannel.swift:116-132,ClaudeCodeBackend.resume:452-453.
Model to generalize (host-grain fence → session-grain):
- Runner epoch —
pool.ts:72-74/203-205/377-389; nucleicd honor pathNucleicd.swift:185-204/360-378(409 →exit(75)).
Trigger inputs:
- Presence —
PeerClient.swiftpresence:221, drop:762-768, backoff:769-773;AppStorepeerPresenceTask:8262-8268,meshPeers:1064. Relay —RelayTransport.swift:99/586-587,RelayListener.swift:112-156, room = hostIDRelayAccess.swift:68-69.
Trip-wires (learned from transfer's own hardening, MESH_TRANSFER Phase 5):
ClientMsgdecode throws on unknown tags — gate every new client→host verb on an advertisedWireCapabilitiesflag, or old hosts disconnect (COVALENCE_RUNNER §11.2).- A new
ClientMsg/SyncClient.Eventcase breaks four exhaustive switches incl. two iOS ones theswift buildwon't catch — build iOS withxcodebuild(COVALENCE_RUNNER §11.1). - The "never runs in two places" gap the transfer review found (
transferringSessions, tombstone hides fromsessionSummaries/snapshot, refusessendInput) 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 validatestransferID/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:
claimSessionCAS monotonicity; two-host simultaneous claim resolves to the lowest hostID; a returning-original observation of a higher generation quiesces + tombstones + refuses relaunch;loadSessionsnever 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 ofSessionTransferTests): mirror-complete session → reconstruct → runnable controller, byte-faithful transcript, fresh-context fallback when no native transcript, andcreateProject-clone when the project is absent. - Detection/election: relay-gated debounce (blip does not trigger; sustained offline +
!hasHostdoes; observer-side relay outage does not seize); deterministic election single-flight. - Crash recovery: a half-done resuscitation is finished-or-discarded by
recoverInterruptedTransferson 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
xcodebuildwith every new case inert.