Skip to content

id(world-replication): a journal whose Checkpoint line exceeds 256 KiB can never be replicated — split_entry_payload refuses the single entry and send_snapshot dies before the first frame #4630

Closed

Description

@dezren39

Summary

A world whose journal contains a single entry larger than MAX_REPLICATION_PAYLOAD_BYTES (256 KiB) can never be replicated over the new /id-world-replication/1 feed (PR #4589, phase 2 of #806). send_snapshot splits the journal image with split_entry_payload, which refuses any single entry over the cap — and the one journal entry that routinely grows past it is the compaction Checkpoint: it carries the program's own state snapshot (uncapped) plus all retained events on one line. Once such a checkpoint exists, every replication stream for that world dies before the first frame, deterministically, on every reconnect; live subscribers are dropped the same way when the post-compaction Reset snapshot is sent.

Evidence (origin/main 51535c2b4)

  • pkgs/id/src/world/replication.rs:79 — MAX_REPLICATION_PAYLOAD_BYTES = 256 * 1024.
  • pkgs/id/src/world/replication.rs:164-192 (split_entry_payload): chunks are split only at entry (newline) boundaries; at :179-181 a single entry with entry_end - start > MAX returns ReplicationError::PayloadTooLarge.
  • pkgs/id/src/world_replication_net.rs:197 (send_snapshot): let chunks = split_entry_payload(&snapshot.bytes).context("split journal snapshot")?; — the error ends the stream before any frame is written. The same function serves the Reset path, so a live stream dies at the reset after compaction too.
  • pkgs/id/src/world/mod.rs:874-908 — JournalEntry::Checkpoint { .., snapshot: String, events: Vec<WorldEvent> }: the program's state serialization and the retained events ride in this one entry.
  • pkgs/id/src/world/core.rs:731-743 (checkpoint_entries): the checkpoint's events is self.events.iter().cloned().collect() — all retained events (WorldLimits::retained_events defaults to 1024, world/mod.rs:103), with no byte budget applied to the entry (the snapshot_bytes budget at world/mod.rs:80-83 bounds participant snapshot frames, not this journal entry).
  • pkgs/id/src/world/actor.rs:261-268 (compact_journal): program.snapshot() is journaled with no size check. Compaction is routine: checkpoint_every defaults to 1000 commits (world/mod.rs:106), and compact_if_due publishes the reset afterwards (world/actor.rs:1618, :1630).
  • pkgs/id/src/world_store.rs:242-245 — FileJournal::compact writes each entry as exactly one serde_json line, so the checkpoint is a single line in the journal file; replication_snapshot (:206-218) returns those exact bytes to send_snapshot.

So both growth routes land on the same line: a program whose serialized state exceeds ~256 KiB, or ~1024 retained events whose serializations sum past it. Neither is exotic for a long-lived world, and compaction is the normal state of such a world — precisely the worlds read replicas (#806) exist for.

Reproduced this run (laptop probe)

Probe ~/git/fleet/probe-2026-11-02-laneB-replcheckpoint/ on the laptop: split_entry_payload + the cap constant extracted verbatim from origin/main 51535c2b4 (snippet md5 5dda768b; source replication.rs md5 99ceb4bf), fed journal byte images in the exact on-disk shape (one Created line + one Checkpoint line):

  • PROBE1 PASS: checkpoint line with a 300,000-byte program snapshot → Err(PayloadTooLarge) — the snapshot can never be sent.
  • PROBE2 PASS (control): 100,000-byte snapshot → Ok(1 chunk), chunks reassemble to the exact journal bytes.
  • PROBE3 PASS (boundary): a single entry of exactly 262,144 bytes (incl. newline) → Ok; 262,145 bytes → Err(PayloadTooLarge).

Why it matters

The failure is silent from the replica's side (the stream just ends, with the error only in the server's debug log), total (no snapshot, no tail, no reset — the world is unreplicable, not degraded), and permanent (the checkpoint stays in the journal; retrying reproduces the identical refusal). It strikes exactly the worlds that have lived long enough to compact with substantial state — the ones whose writer most needs a surviving replica per #806's acceptance ("Replicas serve spectators while the writer is offline").

Acceptance criteria

  • A world whose compacted journal contains a Checkpoint entry larger than 256 KiB replicates end to end: a fresh subscriber receives the full snapshot and reaches the terminal cursor, and a live subscriber survives the post-compaction reset.
  • The chosen mechanism (e.g. sub-entry payload splitting with the chain/cursor contract updated, a larger single-entry allowance within the 1 MiB transport frame bound, or a bounded checkpoint) is applied consistently to the snapshot, tail, and tracker-accept paths, and the replication module docs state the resulting per-entry guarantee.
  • Regression test: a journal image whose checkpoint line exceeds MAX_REPLICATION_PAYLOAD_BYTES ( program-snapshot route and/or retained-events route) round-trips through the feed split/send path and is accepted by ReplicationTracker with a cursor that matches a recomputation from the journal bytes.
  • Existing replication tests (cargo test -p id --lib replication and the world_store replication feed tests) still pass.

Related: #806 (the replication epic this feed implements), #4592/#4593 (replication admission contract — receiver side; this issue is the sender side).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    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
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

  3. 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 2, 2026
  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: picked-up

  5. 35 remaining items

  6. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1

    Transition: picked up
    Worker: repo-worker-code (lane 7b47536b-c162-444c-be8a-3a9538cc4cb5, OpenChamber scheduled lane)
    Task: task 6557 (queue repo-health-code-pilot)
    Scope: design-only. Choose the oversized-entry replication mechanism for #4630 — fragmentation/continuation frames vs larger in-frame allowance vs bounded checkpoint — and land the design note by PR; implementation issues filed from it. The fix itself is out of scope; #4630 stays open until implementation merges.

    Fresh state check 2026-11-03T12:00Z: issue OPEN, no held / user/owned / do-not-merge labels, no competing PR and no queue/*4630* branch on origin.

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

    @dezren39
    MemberAuthor

    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 6557 (small_fix, queued, priority 30) 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

  9. added and removed
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: task 6557 · repo-fleet-monitor run 2026-11-03 08:12 CST
    • Found via: monitor reconciliation
    • Task: Claim-evaporation repair. Task 6557 was claimed and this issue labelled queue/claimed + work/assigned, but the holder's run ended without complete/park/park/block; the lease expired and the task returned to queued, unclaimed. Labels are reset to the truth: queue/queued + status/needs-triage. Last checkpoint stage recorded on the task: claimed — the next claimant resumes from that checkpoint, it does not restart from zero.
    • Expectation: a queue lane re-claims task 6557 and continues from stage claimed.
  11. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  12. added 2 commits that reference this issue on Nov 3, 2026
  13. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1

    Transition: design complete (task 6557, queue repo-health-code-pilot)
    Worker: repo-worker-code (lane 335a5d1f-bfc1-4831-99ea-850c8939a727, OpenChamber scheduled lane)

    Design delivered — #4630 mechanism chosen

    Design note merged in #4962 (merge f3d150bb5): doc/2026-11-03T14-55-00Z_design_id4630_oversized_replication_entries.md.

    Chosen: bound the checkpoint at write time (WorldCore::checkpoint_entries) by emitting bounded part entries, MAX_CHECKPOINT_PART_BYTES = 192 KiB — below the existing 256 KiB MAX_REPLICATION_PAYLOAD_BYTES. No wire change, no envelope-version bump, no ReplicationTracker::accept change, no receiver upgrade; JOURNAL_VERSION 6 for the new part kinds. The v1 invariant #4592 relies on (payloads are whole entries; the cursor is entries absorbed) is preserved exactly.

    Evaluated and recorded in the note:

    • (a) sub-entry fragmentation/continuation frames — rejected: v1→v2 wire bump (accept refuses any other envelope version) plus a permanent per-fragment receiver-buffering surface to solve a writer-controlled entry kind.
    • (b) larger in-frame allowance within the 1 MiB transport bound — deferred: cannot bound the program snapshot (unbounded at actor.rs:275), so the residual is large; and it is a receiver-side constant, so raising it silently compatibility-splits v1 receivers.
    • (c) bounded checkpoint — chosen, staged (write-time split; then legacy-journal handling).

    Per-entry guarantee is stated in the note: every writer-emitted entry ≤ 192 KiB, hence ≤ one v1 payload frame. The one residual case (a Checkpoint already stored by an older binary, not since re-compacted) has its own follow-up.

    Implementation follow-ups filed

    This issue stays OPEN — the implementation in #4966 closes it. Labels cleared to match the handoff (no in-flight queue/*, status/*, work/*).

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/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions