fix(sync): remove 64k _meta ceiling + materialize catch-up snapshots (fifa-class CGs) - #1880
Conversation
…es-control-plane fix(publisher): honor publisher.maxRetries for API- and agent-admitted lift jobs (#1836)
…eiling (#1847) A CG whose SWM `_meta` reached SYNC_RESPONDER_SNAPSHOT_BUILD_MAX_ROWS (64,000) raw rows became permanently unsyncable on the meta lane: `readSwmMetaPage` passed `params.cutoffIso == null` POSITIONALLY as `fallbackOnPerSnapshotBudget`, so TTL-filtered sessions (the normal modern path) got a bounded refusal with no fallback, and the bounded snapshot applied its row/byte budget to the RAW graph BEFORE the TTL filter, so the refusal fired even when the fresh subset was tiny. Live on mainnet: 10/15 cores refusing 78-202x/day; the fifa-world-cup-2026 CG (4,600 data quads, 64,001+ meta rows) never converges (`data=0 sharedMemory=0` forever while query-remote works). THE TRAP: a naive flag-flip is NOT a fix. The fallback was disabled deliberately because the TTL-filtered paged query was `SELECT DISTINCT ?g ?s ?p ?o` + a six-predicate UNION join + global `ORDER BY ?g ?s ?p ?o` re-evaluated with a growing OFFSET per page over a mutable graph family — the #1597 listGraphs-storm class that can pin cores and gigabytes on large stores. Re-enabling the flag alone would trade a bounded refusal for a store-melter. The fix mirrors how the SWM DATA lane solved the same problem (buildFreshSwmDataGraphPlan): * buildFreshSwmMetaPlan: two small-result discovery queries per meta graph (fresh subjects by publishedAt; graph-scoped heads via the fresh-WorkspaceOperation tuple join) plus chunked VALUES row counts. The session plan caches only graph/subject/count scalars. * readBoundedFreshSwmMetaSnapshot: the snapshot now materializes only the ADMITTED rows, so the per-snapshot budget binds on what is actually served — the fifa class (64k history, small fresh subset) takes the ordinary memoized-snapshot path. Final admission still runs through filterSwmMetaSnapshotRows, the canonical in-process filter. * readFreshSwmMetaRowsPageFromPlan: if even the ADMITTED set exceeds the budget, the session degrades to whole-subject window pages walked over the plan's prefix sums — VALUES-anchored reads with NO store-side ORDER BY and NO OFFSET (rows are sorted in-process; plan subject order is compareCodePoint, identical to compareRows on ?s). Chunk row counts are verified against the plan so a mutated subject fails the session (requester restarts) instead of skipping/duplicating rows, and a seal/head row-group is always read atomically within one chunk query (never torn the way #1788 durable batching tears groups). * The store-melting TTL query is DELETED (not gated), the dead readSwmMetaRows helper is removed, and only then is fallbackOnPerSnapshotBudget enabled for TTL sessions. * Legacy cutoff-less sessions keep the existing raw-snapshot + unfiltered store-paged compatibility path, byte-for-byte. Remaining bounded refusal: a single SUBJECT above the hard 64,000-row build cap (a coherent row-group that cannot fit any budget) — pinned by test as the only refusal left, and impossible to hit through organic operation history. Mutation-tested (each reverted before commit; each killed exactly the right tests): * M1 reintroduce `params.cutoffIso == null` positional arg -> the oversized-fresh-set, paged-equivalence, plan-mutation and paged requester-reassembly tests fail (4/8); the fifa-shape test still passes, proving budget-on-filtered-set independently fixes that class. * M2 disable the chunk row-count verification -> exactly the plan-mutation session test fails. * M3 remove the single-subject cap -> exactly the pathological-subject refusal test fails. * M4 off-by-one in the window slice -> all four paged-lane correctness tests fail. Evidence (in-memory Oxigraph, default production budgets): * fifa shape: 64,026 raw rows, 26 fresh -> served in 4 pages, 22ms. * intrinsically oversized fresh set: 65,000 admitted rows -> served completely in 14 bounded pages, 774ms, no refusal. * requester fetchSyncPages reassembly across 4-row pages: every op and seal/head row-group complete, dkg:assertionVersion never stripped. sparql-scale-lint: 0 new blocking findings (the rewritten legacy query carries R2/R3 pragmas; the new plan queries are lint-clean by shape). Fixes #1847 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss pages (#1868 review) Four review findings on the #1847 lane, each with a dedicated regression: - Discovery is now bounded by construction: LIMIT-capped subject discovery (FRESH_SWM_META_PLAN_MAX_SUBJECTS) + fixed response byte caps on every plan query, with typed per-snapshot refusals; retained plans carry a scalar byte estimate charged to the process-wide responder snapshot budget as control-plane entries (LRU-evictable, globally rejected under pressure). - StoreResponseTooLargeError during TTL snapshot materialization converts to the per-snapshot snapshot_bytes budget error so the phase degrades to plan paging instead of failing outright. - Whole-subject window reads verify PER-SUBJECT counts against the plan and bind a content digest on first read, verified on every reread: same-count replacements and compensating cross-subject mutations fail the session instead of tearing or misaligning row-groups at page seams. - readResponderRowsPage optional behavior is a named options object; the fallback policy can no longer be passed positionally (the original defect). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1868 review) Partial take on the plan-orchestration collapse suggested in review: the exact-graph and TTL SWM meta lanes now share one createSessionPlanGetter owning refresh consumption, offset>0 require-existing, and expiry translation — the lifecycle most likely to drift between lanes. The full collapse (snapshot + fallback wiring) is deferred; those parts differ by lane for reviewed reasons and are slated for the graph-plan module split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etadata Two review findings on the catch-up materialization path, plus the extraction they both wanted a home for: 1. Count-only materialized-check could skip a newer snapshot. All assertion versions of a graph-scoped KA share ONE graph URI, so an older version with the same quad count read as "already materialized" and the verified newer snapshot was never written, while its metadata could still land — content and head permanently inconsistent. The guard now requires count AND publicQuadsDigest equality: the CONSTRUCT read-back only runs when the count already matches (bounded by exactly the snapshot size we would otherwise write, exact per-KA IRI scope), and the digest-over-roundtrip comparison is the same check resolveWorkspaceOperation already relies on for stored snapshot graphs. 2. Materialization left stale head metadata behind. The sync lane's meta insert is append/union-style, so materializing v2 on top of a v1 head stacked both versions' assertionVersion/shareOperationId rows on one subject — resolveKnowledgeAssetWorkspaceHead reads with LIMIT 1 and could resolve a stale or mixed head. After a successful graph replace (graph FIRST, so a crash never leaves a head without content) the new replaceHeadMetadata deletes the head subject and every operation subject it references — the catch-up counterpart of gossip's delete-then-insert (storeKnowledgeAssetWorkspaceHead) and recovery's replaceMetaForGraphAssets, including its kaUal guard so a corrupt head row can never delete another KA's operation. The fresh verified meta then lands on a clean subject. readStoredHead (MAX-version read, unchanged semantics) now also detects union-insert residue (>1 distinct version/operation) and the skip path collapses it — otherwise a round that failed between replace and head swap would leave the ambiguity permanent, because every later round skips on matching content. Structural: the store-side policy moved out of dkg-agent-lifecycle into createSharedMemorySnapshotMaterializer (swm-snapshot-materializer.ts). The lifecycle now only wires agent-owned resources (store, the SAME lock map SharedMemoryHandler uses, list-cache invalidation); the SPARQL, parsing and replace semantics have a named, directly testable home. Every query in the module is bound to an exact per-KA IRI (head subject / operation subject / assertion graph) — no bucket scans; sparql-scale-lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… subgraph + network paths The regression tests injected isGraphAssetMaterialized, so the actual lifecycle SPARQL implementation was untested — a regression back to a marker-based (or count-only) guard would have stayed green. New swm-snapshot-materializer.test.ts drives the REAL createSharedMemorySnapshotMaterializer against a real OxigraphStore: - marker-without-content (the pre-fix broken state) => guard false - short graph => false; exact content => true (digest survives the store round-trip) - EQUAL-COUNT graph holding another version's content => false — the count-only trap - readStoredHead returns MAX over duplicate head rows and flags the union-insert residue for repair - replaceHeadMetadata deletes head + referenced operations, spares unrelated subjects and other KAs' operations (kaUal guard) - end-to-end: a node fully holding v1 (same quad count as v2) catches up to v2 — graph replaced, exactly ONE head version remains, and the LIMIT-1 production reader resolveKnowledgeAssetWorkspaceHead resolves v2; a second round is a pure no-op (no replace churn) The decision-test file gains the two missing coverage lanes: a KA under a REGISTERED subgraph materializes into its subgraph assertion graph (dropping the parser admission pass-through fails exactly that test), and a cold node fetches the snapshot via the phase='snapshot' network branch and still materializes it / still withholds meta when the replace fails after the fetch. Mutation-tested — each mutant killed by exactly the intended test(s): count-only guard, marker-based guard, MAX->MIN head read, needsRepair=false, head swap removed, skip-path repair removed, subgraph admission removed, network onSnapshotReady dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
packages/evm-module/deployments/localhost_contracts.json is a generated artifact the local build rewrites (branch names, commit hashes, timestamps). None of it is needed by the SWM materialization work; restored byte-identical to origin/main so the diff carries only the actual change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… TTL meta plan (#1868 review) FreshSwmMetaPlan is now a genuinely immutable pagination description (deep-readonly graph/subject/count scalars). The mutable per-session content-digest state that used to live on subject entries moves to a sidecar WeakMap keyed by plan instance — exactly the binding's intended lifetime: the memoized plan IS the session, a refreshed/rebuilt plan is a new object with a fresh empty binding map, and evicting or expiring the plan releases its digests with it. readFreshSwmMetaSubjectWindowRows is the only writer. Placement only: same-count replacement failure semantics are unchanged and every existing mutation test passes unmodified. Also closes the two remaining #1868 round-2 coverage asks on the plan budget thread, each proven by a killed mutant: - time-based TTL expiry (controlled clock) prunes a plan AND releases its global budget charge, distinct from the maxEntries eviction the prior test covered — a mutant that leaks the charge on expiry passes the old test and is killed only by the new one; - the plan cardinality cap binds in AGGREGATE across root and subgraph meta graphs — a mutant that resets the allowance per graph passes the single-graph cap test and is killed only by the new multi-graph test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # packages/agent/vitest.unit.config.ts
… cli conflicts
|
Review: approve, with one required cleanup and three observations. I verified this against the branch head ( What I verified holds
Required cleanupDrop Observations (non-blocking)
The test engineering here deserves explicit credit: real >64k-row stores at default production budgets, mutation-kill coverage aimed at the exact positional-boolean defect, the aggregate-across-subgraphs cardinality cap, the same-count split-subject hybrid repro from the #1868 review, and store-query spies that structurally forbid the deleted global-sort shape from ever reaching the store. This is the standard sync-lane changes should be held to. |
…ssionless TTL paging as best-effort Review cleanup (branarakic): the regenerated evm-module deployment artifact leaked into the composition — 289 lines of dev-deploy churn pointing the recorded deployment at an unrelated commit and downgrading the recorded PublishingConviction version. Restored byte-identical to the base. This repo's known churn trap; caught for the third time this cycle. Also addresses review observation 1: a doc comment on createSessionPlanGetter stating plainly that sessionless TTL paging is best-effort and the digest guard does not cover it, so the guard is not later mistaken for offset-page consistency protection it does not provide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Review addressed in 588628e: Required cleanup — done. Observation 1 — comment added on Observations 2–4 — acknowledged as follow-ups: non-canonical literal admission asymmetry (consistency footnote, requester verification gates), charging the exact-graph/fresh-data plan memos to the global budget (same class, smaller footprint), and the legacy plain-string Build clean on the new head. Thank you for reproducing the test evidence independently — clear to merge. |
What ships (step 2 — fifa-class Context Graphs)
Two stacked fixes that must ship as a pair; either alone is insufficient:
_metasnapshot ceiling (issue bug(sync): SWM meta lane permanently unsyncable once _meta reaches 64,000 rows (oversized-snapshot fallback disabled for TTL-filtered sessions) #1847, canonical PR fix(sync): serve TTL-filtered SWM meta past the 64,000-row snapshot ceiling #1868): the responder applied its 64,000-row budget to the raw meta graph before TTL filtering and passed the fallback flag positionally-wrong, so any long-lived CG (e.g.fifa-world-cup-2026: ~4,600 data quads, 64k+ meta rows) became permanently unsyncable to every peer. Replaced with a bounded session-plan lane: budget binds on served rows, no store-side global sort, count- and digest-verified pages. 10/15 mainnet cores emit these refusals today.Meta arrives + content lands. Base: current
origin/testnet-canary(includes #1876 and the #1879 merge-main; its cli conflicts hand-resolved here).Gate evidence (6-node devnet, full sync suite)
Final head: verify-fixes 8/10 (only the sanctioned #1780-409 and #1779-markdown knowns), proof 32/34 (only the two known watermark checks), and the fifa-class proof itself:
Two earlier full-green runs on the pre-absorb head (32/34 + 32/34; holdout 100/100 in 6078ms and 9164ms). Agent unit suite 1249 passing.
Known issues (documented, non-blocking)
Neighbors #1869/#1870/#1873 are compatible, not competing. Real-world acceptance test: the
fifa-world-cup-2026CG itself — a testnet node syncing its full corpus is the field confirmation.🤖 Generated with Claude Code