Skip to content

id: design: native-spill write lease protocol for hot worlds (from #2059) #2819

Open

Description

@dezren39

From the #2059 investigation (hot-world sharding)

The landed investigation doc for #2059 (doc/2026-10-30T19-35-00Z_investigation_hot_world_sharding/, §3 rung 3) designs native-node spill for an indivisible hot world: a native id node runs the world's program turns while the hosted Durable Object keeps the journal of record. The doc is explicit that the journaled single-writer lease is the whole safety design — split-brain (DO and native node both accepting writes) is the catastrophic failure. The lease protocol itself was not designed. This issue designs it, so the #2046 synthesis adopts a designed protocol instead of a placeholder.

Design to produce

  • Lease object: grant/revoke/handoff as journaled entries — who grants (DO? coordinator? operator?), lease term and renewal, and how the grant is recorded so both sides and any auditor can prove who held the write lease at any journal position.
  • Fencing: how a partitioned or crashed ex-holder is fenced out (monotonic lease epoch checked on journal append is the obvious shape — verify or replace), and what happens to writes an ex-holder attempts after losing the lease.
  • Handoff both ways: DO → native at spill start, native → DO at de-escalation (investigation §2: spill ends when the warning band clears for ~1 hour), including in-flight command treatment at the boundary.
  • Failure walkthroughs: native node crashes holding the lease; DO and node partitioned while both alive; lease expires mid-turn; journal mirror lag at handoff.

Acceptance criteria

  • Design recorded in a doc under doc/ and linked from this issue
  • Lease grant/revoke/handoff specified precisely enough to implement against pkgs/id/src/world/'s journaled-entry patterns
  • Split-brain impossibility argued from the protocol mechanics, not asserted
  • Anything buildable that falls out is filed as its own issue(s)

Program: ID platform expansion, epic #2056. Parent investigation: #2059. Consumer: #2046 (serverless edition v1 synthesis).

Activity

  1. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Oct 30, 2026
  2. added
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    on Oct 31, 2026
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Intake review

    Related: parent #2059; sibling #2818.

    Notes: design-stage issue spun out of #2059; body already carries an acceptance section, so none was appended. No merged PR or commit found this run that implements this design — verified still live.

    Risks: design drift if the sibling design lands first and changes shared assumptions (hot-world sharding seams) — the two designs should be read together before either is implemented.

    Suggested approach: keep as design; when picked up, the design output should land as a linked implementation issue per the standing "a design is not finished work" rule.

    🤖 Agent note · filed

  4. added
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Oct 31, 2026
  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: picked up

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: triaged — design record (queue task 5745)

    Design record: native-spill write lease protocol (#2059 rung 3)

    Context. The #2059 investigation (doc/2026-10-30T19-35-00Z_investigation_hot_world_sharding, §3 rung 3) moves a hot indivisible world's execution to a native id node while the hosted Durable Object keeps the journal of record — "execution moved, journal and identity did not." Consumer: the #2046 synthesis. Sibling #2818 (region-seam handoff format) shares only the journaled-entry pattern; it shuttles journal segments between shards and does not interact with the lease.

    The decision

    The lease lives in the journal itself, the DO is the only grantor, and fencing is an epoch check on journal append. No side channel, no separate lease service: the journal is already the single ordered record both sides and any auditor read (pkgs/id: one JournalEntry per line in journal.jsonl, fsynced per append — world_store.rs:6), so lease state derived from it can never disagree with the history it protects.

    The protocol

    • Lease object = journaled entries, appended like every other world entry: LeaseGranted { holder, epoch, term, granted_at_position }, LeaseRenewed { epoch, term }, LeaseRevoked { epoch, reason }, LeaseHandedOff { from, to, epoch }. Epoch is monotonic: every grant/handoff increments it; it never resets across the journal's life. Holder identity is the node's world identity (the same key that signs its journal-visible actions).
    • Who grants: the DO — it owns the journal of record, so it is the only party whose grant is meaningful. A native node requests spill (with the id: investigate: hot-world sharding — when one world outgrows one Durable Object #2059 trigger evidence); the DO grants by appending LeaseGranted. An operator can force LeaseRevoked through the DO, never by editing the native node.
    • Fencing: a writer appends only presenting the current epoch. The append path checks the presented epoch against the latest lease entry in the journal: stale epoch → the append is rejected and the attempt itself journaled as an audit entry, never applied. This verifies the shape the issue proposed (monotonic epoch on journal append) rather than replacing it — with one strengthening: rejection is journaled, so a partitioned ex-holder's attempts are provable after the fact, not silent.
    • Handoff DO → native (spill start): DO appends LeaseHandedOff { epoch+1 } as its last act as writer, then stops accepting writes. The native node begins appending only after it has replayed the journal through the handoff position. Handoff native → DO (de-escalation, warning band clear ~1 h per id: investigate: hot-world sharding — when one world outgrows one Durable Object #2059 §2**):** symmetric — the node appends its final entries, requests handback, the DO appends LeaseHandedOff { epoch+1 } and resumes.
    • In-flight commands at the boundary: a command the old holder received but did not journal before losing the lease is refused with a retryable not-holder error — it is never executed by the old holder after the boundary and never silently re-executed by the new one; the client retries against the current holder.

    Split-brain impossibility (argued from the mechanics)

    Split-brain requires two writers appending accepted entries concurrently. An append is accepted only if (1) it lands in the one journal and (2) its epoch equals the journal's latest lease epoch. The journal has a single append order (one JSONL file, fsynced per append, on the DO side of record). A handoff changes the current epoch exactly once, at one journal position. Before that position the old holder's epoch is current and the new holder's is not yet granted; after it, the reverse. There is no position at which two epochs are both current, and no append path that skips the journal — so two accepted writers at once would require the journal itself to fork, which is the one thing the DO-of-record construction excludes. A partitioned ex-holder can attempt appends; each is rejected-and-journaled (or cannot reach the journal at all), which is an availability event, not a consistency one.

    Failure walkthroughs

    • Native node crashes holding the lease: the lease term expires; the DO appends LeaseRevoked { reason: expiry } then LeaseGranted { holder: DO, epoch+1 } and resumes writing. The dead node's unjournaled work is lost by definition (it was never in the record); its journaled work stands.
    • DO and node partitioned, both alive: the node cannot reach the journal of record, so it cannot append at all; if the deployment ever gives the node a journal mirror, its appends carry the pre-partition epoch and are rejected-and-journaled on heal. The DO, after term expiry, revokes and resumes as above. Exactly one writer ever lands an entry.
    • Lease expires mid-turn: a turn is not an append unit — individual appends are. Appends before expiry land; the first append after expiry fails the epoch check (the DO's revocation has advanced the epoch), the turn aborts, and the client sees the retryable not-holder error. No partial turn is journaled as a unit unless the program itself journaled it in pieces, which is today's semantics too.
    • Journal mirror lag at handoff: the new holder must replay through the handoff position before its first append; its first append carries the handoff epoch and a prev-position at or past the handoff entry. If its mirror is behind, it waits — lag delays the handoff, it cannot corrupt it, because the epoch check is evaluated against the journal of record, not the mirror.

    Invariants

    • Epoch never decreases and is never reused; every grant/handoff/revoke is a journal entry (auditor can prove the holder at any journal position).
    • The DO is the only grantor; a native node can hold the lease but can never grant, renew past its term, or hand it to a third party.
    • Rejected appends are journaled as attempts — fencing is evidence, not silence.
    • Spill changes execution only: journal, identity, membership, and capability roots do not move (per id: investigate: hot-world sharding — when one world outgrows one Durable Object #2059 §4).

    Phased implementation packets (enqueued, in order)

    Phase Issue Queue task Dedupe key
    1 — design doc + entry schemas #3803 task 5795 repo-health:v1:fix:developing-today/code:issue:3803
    2 — lease entries + epoch fencing #3804 task 5796 repo-health:v1:fix:developing-today/code:issue:3804
    3 — handoff both ways + failure tests #3805 task 5797 repo-health:v1:fix:developing-today/code:issue:3805

    Question for Drew

    None that blocks: grantor authority (DO-only) and the default lease term are the two judgement calls; the record fixes DO-only as a safety property (a second grantor reintroduces the split-brain the protocol exists to exclude), and the term is an operational default for phase 1 to set (recommended: 30 s term, renewal at 10 s — long enough to ride out a GC/network stall, short enough that a crashed holder stalls writes for under a minute). Flagging them here rather than stalling on them.

    🤖 Agent note · triage task 5745 · lane B · 2026-11-01

  7. 6 remaining items

  8. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  9. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: reviewed (coverage loop-back)
    Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 6822 (triage, queued, priority 50) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
    Found via: coverage loop-back 2026-11-03

  10. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: picked up

  12. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: triaged — design gate dispositioned

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-inskind/designA design proposal (design-architect): multi-step, phased, decision-bearingmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions