Zero-impact updates: the recovery contract
Status: Planned (D-196, roadmap ZU0–ZU3). Nothing here is implemented yet; today a Node restart ends every process it started. This document is the ZU0 contract: it fixes which start-up recovery rules an adopted process keeps, which it re-earns, and which still apply in full. ZU1a implements it, and the adoption checks built with it (using the dummy Session) are its oracle.
Words
- Helper: the process host (
maia-process-host) that owns one launched tree. Today its control channel is a pipe the Node holds, so a Node exit ends the tree. - Adopt: a new Node proves a surviving helper is the one it launched and takes its control channel back, without restarting the tree.
- Lost owner: any owner the new Node cannot prove. A lost owner meets today's recovery, unchanged.
The rule
Adoption is a step before any destructive recovery. Anything unproven is lost, so doubt always falls back to today's behavior (fail closed).
Node start
|
v
1. ADOPT list helper sockets; for each, prove
| helper id + pid + start time + lease token == the Session/permit record
| -> every owner is `adopted` or `lost`
v
2. database init recovery (recoverNodeRuntimeStorage) <- must skip adopted owners
v
3. reconcileBeforeAccept <- must skip adopted owners
v
4. accept requests
Today step 2 runs inside database initialization
(storage/database/recovery.ts), before step 3
(application/run-desktop-node.ts). Adoption therefore has to run before the
database recovery, not after it.
One row per destructive pass
| Pass (today) | Adopted owner | Lost owner | Proof in the suite |
|---|---|---|---|
Terminals: observers deleted, controller requests denied and lease revoked, unowned running terminals disconnected (terminal/storage/live/recovery.ts) | Re-earns observers and the controller lease under a new generation. The terminal stays running; its output replays from the last acknowledged byte offset. | Disconnected, as today. | The same terminal id keeps its pid and start time; output has no hole, duplicate or gap. |
Turns: in-flight turns become outcome-unknown (session/turn/storage/lifecycle.ts) | Keeps the turn. It completes through the adopted channel. No outcome-unknown, and no relaunch or retry of the turn while the adopted owner lives (today's crash recovery relaunches the runtime and resumes the vendor thread; D-196 suspends that only while the owner answers). | outcome-unknown, as today. | One long turn spans the restart and settles once; no second run starts. |
Permit fence: the recorded tree must be gone, else cleanup_unknown (external executors) | Keeps the occupied permit. A populated tree whose helper answers with the recorded identity is adopted, not a survivor (D-098 is amended). | Fenced cleanup_unknown, as today. | The vendor authorization is not fenced; a mismatched helper still fences. |
Session credentials: every active row revoked at start (revokeAllSessionCredentials) | Keeps the row, bound to the helper id and start time (see below). | Revoked, node_restarted. | The surviving worker's next authenticated call succeeds; a lost owner's credential fails. |
Approvals: every pending row denied (cancelAbandoned) | Keeps the row pending with its original expiry (see below). | Denied, as today. | Answering the original approval after the restart releases the worker. |
Invocation attachments: every active row revoked (revokeActiveInvocationSessionAttachments) | Re-earns: the Session's attachment is re-validated against the adopted helper, and revoked if that fails. | Revoked, as today. | The attachment of an adopted Session authenticates after restart. |
Port leases (portLeases.reconcileAtStart) | Keeps a lease whose owner is adopted. | Released, as today. | An adopted Session still holds its ports. |
Voice Pi relays and execution operations (recoverVoicePiRelays, recoverExecution*) | Not process-owned by a helper. Unchanged: ZU0 does not extend adoption to them. | As today. | Covered by their existing tests. |
Credentials: keep, not re-issue
The gap analysis proposed re-issuing a credential for an adopted process.
That cannot work: a worker's environment is fixed when it launches
(SESSION_CREDENTIAL_VARIABLE and SESSION_NODE_ORIGIN_VARIABLE, set in
session-credential/service.ts), and issuing a new credential revokes the
previous one. A replacement the worker never receives would lock it out.
So an adopted Session keeps its credential row:
- the row stays
active, bound to the helper id and start time; - it is revoked at once if adoption fails, the lease ends or the helper exits;
- every other active row is still revoked at start.
Two further requirements follow, and ZU1a chooses the mechanism:
- A stable or recoverable origin. The worker holds the old Node origin, and each start picks a new port. An adopted Session needs an endpoint that survives the restart (for example a per-Session private socket path recorded on the Session) or a way to re-read the Node's descriptor.
- Detach is not scope closure. A credential is revoked today when its launch scope closes, and authentication also checks that the Session is active. For an adopted Session the credential's owner moves from the launch scope to the helper lease, and runtime admission is restored before the Node accepts requests.
The suite checks the outcome: after the restart the worker's next authenticated call succeeds, and the old origin is unreachable.
Approvals: keep the row and the delivery
A pending approval row is not enough. The waiter that delivers the answer to the worker lives in the Node's memory, and a restart starts with none, so a stored answer would reach no one. For an adopted owner the new Node:
- keeps the row pending with its original expiry;
- rebuilds the waiter from the durable row and the vendor request id;
- delivers a decision recorded before the restart, and any made after it;
- records an update detach as its own reason, never as a cancellation.
A lost owner's approvals are denied with today's reason.
Claude Code SDK Sessions
They do not stay attached. An update waits until the Session is between turns (30 minutes by default; after that the machine is skipped and retried later, and the Session keeps running), recycles the idle process and resumes it by session ID. No turn is cut. The suite covers the finite turn, the resume, and a turn that never settles.
What the first ZU release cannot do
It cannot adopt or roll back Sessions started before it: their helpers hold a control pipe, not a socket. The release notes say so.
Open items for ZU1a
- The origin mechanism above.
- The helper control protocol, versioned as its own axis.
- The lease length and the ring-buffer size, set from measurements.
- Spike evidence: spikes.
Imported from Maia OS docs at 3dc3e07