Repository navigation
Description
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 infrontier()except the node's own: no cap, no truncation, no selection.pkgs/id/src/local_events.rs:355-378—record()(the single path behindpublish:171,set_tag:198,clear_tag:213 and the remove path) passesother_heads()straight intoChainExt { after, .. }and callschain::sign.pkgs/id/src/chain.rs:33—pub const MAX_AFTER: usize = 32;pkgs/id/src/chain.rs:187-189—ChainExt::validate(run bysignbefore anything is encoded) refusesafter.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:196opensLocalEventsintoAppState) 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_tagsucceeds. - Seed the same store with one seq-0
tag.setfrom each of 33 distinct other authors viaEventStore::ingest(what sync leaves behind), reopen asLocalEvents:
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.clearonly removes claims in its causal past, and causal past is computed fromafter+ backlink (local_events.rs:435-441,tag_claims.rscausal_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_tagandclear_tagsucceed (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
developing-today commentedon Oct 28, 2026
CODE-700
dezren39 commentedon Oct 28, 2026
🤖 Agent note · filed
50 remaining items
dezren39 commentedon Oct 28, 2026
🤖 Agent note · parked
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 checkgate 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 hasFixes #1736, is MERGEABLE, and carries no held/do-not-merge labels).dezren39 commentedon Oct 28, 2026
🤖 Agent note · picked up
queue/1964-1736-bounded-frontierMerge pull request #1750 from developing-today/queue/1964-1736-bounde…
dezren39 commentedon Oct 28, 2026
🤖 Agent note · done
e91098f1193ac50ccd049d0876605ea27d8673c8; this issue auto-closed by the merge