Skip to content

id(local_events): a node with 33+ other authors in its store can never write again — record() builds an after frontier over the 32-hash cap #1736

Closed

Description

@dezren39

Problem

A local node can never record another event once its store holds events from 33 or more other authors. Every LocalEvents write — publish, set_tag, clear_tag, document remove — builds the new event's after frontier from the head of every other author in the store, and the chain layer refuses an after with more than 32 hashes. The failure is permanent and total for the node's own writes, and it arrives exactly when a node is well-connected: after syncing with 33+ peers.

Evidence (verified this run on main 8c8d1bbb)

  • pkgs/id/src/local_events.rs:343-352 — other_heads() returns the head of every author in frontier() except the node's own: no cap, no truncation, no selection.
  • pkgs/id/src/local_events.rs:355-378 — record() (the single path behind publish :171, set_tag :198, clear_tag :213 and the remove path) passes other_heads() straight into ChainExt { after, .. } and calls chain::sign.
  • pkgs/id/src/chain.rs:33 — pub const MAX_AFTER: usize = 32;
  • pkgs/id/src/chain.rs:187-189 — ChainExt::validate (run by sign before anything is encoded) refuses after.len() > MAX_AFTER: "after holds {0} hashes, the most allowed is {max}".
  • pkgs/id/src/event_store/mod.rs:425-438 — chains() returns one row per author in the store, so the frontier grows by one per distinct author ever synced; nothing prunes authors.
  • Production callers of the blocked path: the web save flow (pkgs/id/src/web/mod.rs:196 opens LocalEvents into AppState) and the doc CLI (pkgs/id/src/commands/doc.rs:36).

Reproduction (probe in the real crate, run on the laptop)

Probe test (tests/probe_lanea.rs in a laptop worktree at origin/main; local_events.rs/chain.rs sha256-identical to sandbox main 8c8d1bbb; probe + full log preserved in the goal bookkeeping, probe-2026-10-28-laneA-after32/):

  • Control: fresh LocalEvents, no other authors → set_tag succeeds.
  • Seed the same store with one seq-0 tag.set from each of 33 distinct other authors via EventStore::ingest (what sync leaves behind), reopen as LocalEvents:
CTRL 0 other authors: set_tag ok = true
OBSERVED frontier heads = 34
OBSERVED set_tag FAILED: after holds 33 hashes, the most allowed is 32
test result: FAILED. 0 passed; 1 failed (the failure is the bug demonstration: the probe asserts the desired behaviour)

Why it matters

This is the same shape as #1720 (a local operation that becomes permanently impossible once state crosses a cap) but broader: it does not block one tag name, it blocks all local writes — publishing documents, setting or clearing tags, removals — for any node whose store has accumulated 33+ authors. That is the normal end state of a node that syncs with a community, i.e. the nodes the system is built for. Reads still work, so the node looks healthy while silently being unable to write ever again; the error names the cap but offers no recovery path.

Suggested direction (for triage to pin)

The frontier selection is a real design decision, which is why this is filed for triage rather than self-solved — semantics depend on after:

  • A tag.clear only removes claims in its causal past, and causal past is computed from after + backlink (local_events.rs:435-441, tag_claims.rs causal_past), so dropping heads at random can silently un-see claims a clear was meant to remove.
  • Options for triage to weigh: (a) raise/derive the cap in step with the header decoder's array bound; (b) select a bounded frontier with a documented rule (e.g. the heads the new event actually depends on for its kind — a document publish vs a clear have different needs); (c) compact/forget authors whose heads are already ancestors of others before counting.
  • Whatever is chosen, record() must not fail solely because the store holds many authors.

Acceptance criteria

  • With 33 (and e.g. 100) other authors present in the store, publish, set_tag and clear_tag succeed (regression test at the probe's shape: seed ≥33 foreign authors, then a local write succeeds).
  • A clear recorded with a bounded frontier still removes exactly the claims it names that were live when it was written — the causal-past semantics of clears are preserved (existing tag fold tests pass).
  • Events the node produces still pass ChainExt::validate / decode on receipt by other nodes (no wire-format break unless deliberately versioned).
  • The frontier rule is documented at other_heads/record (what is included, what is dropped, and what that means for clears).
  • Existing id crate tests still pass (laptop or CI).

Dedup

Searched open + closed issues and code search for MAX_AFTER, other_heads, "after holds", and frontier terms before filing: no filing covers this interaction. #1720 is the sibling cap interaction inside clear_tag (claims per clear), not the after frontier. #1075 is a broad validation audit epic, not this bug.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. 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
    priority/highImportant and time-sensitive
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-a
    • Session / ID: scheduled creator run 2026-10-28 11:39 CDT
    • Found via: filed this run
    • Task: LocalEvents::record builds after from every other author's head (local_events.rs:343-363) with no cap; chain validate refuses >32 (chain.rs:33,187). Verified with a laptop probe in the real crate: control set_tag OK; 33 seeded foreign authors -> frontier 34 heads, set_tag fails 'after holds 33 hashes, the most allowed is 32', nothing recorded.
    • Links: queue packet enqueued this run (repo-health-code-pilot)
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  5. 50 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    on Oct 28, 2026
  7. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

    • Agent: queue worker (repo-worker-code), Lane A shift
    • Session / ID: queue task 1964 (resume, attempt 2) · repo-queue-worker-shift 2026-10-28 14:00 CDT
    • Found via: queue claim (park resume)
    • Task: PR id(local_events): bounded dependency frontier for record() — fix #1736 #1750 CI recheck and merge for the bounded-frontier fix
    • Expectation: parked 20 min — on resume, if the aggregate gate is green, merge

    Resumed from the earlier park and rechecked PR #1750 fresh: head unchanged at c9f23b7. The three checks pending at park time have all PASSED — test-unit, test-shard-0, and test-shard-2 (16m11s) — and every other named check passes (31 pass, CodeRabbit skipping). The only remaining item is the aggregate nix flake check gate job, still pending after ~5 minutes of polling. Per the park-don't-wait rule the task is parked again for 20 minutes rather than held; merge is the first action on resume (PR body has Fixes #1736, is MERGEABLE, and carries no held/do-not-merge labels).

  8. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 28, 2026
  10. added a commit that references this issue on Oct 28, 2026
    e91098f
  11. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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/highImportant and time-sensitivesource/creatorFiled by the new-issue-creator jobstatus:donetask/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