Skip to content

fix(sync): converge public SWM catch-up across bounded repeat passes (#2050) - #2076

Open
Jurij89 wants to merge 32 commits into
testnet-canaryfrom
fix/2050-public-swm-continuation
Open

fix(sync): converge public SWM catch-up across bounded repeat passes (#2050)#2076
Jurij89 wants to merge 32 commits into
testnet-canaryfrom
fix/2050-public-swm-continuation

Conversation

@Jurij89

@Jurij89 Jurij89 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Public SWM catch-up now repeats, bounded and progress-aware. A receiver fetches SWM content as immutable content-addressed snapshots, one KA at a time, inside a single 120 s deadline that also covers the metadata and aggregate-data phases. When it expired mid-list, syncPublicSnapshotsForMeta abandoned every remaining snapshot, and the identity of the missing set — fully computable in memory at that instant — was discarded one stack frame later. Each peer got one round, the walk ran once, and the job terminated unreachable with a partial graph and no resume path. Another pass now runs while the job budget allows, coverage is still advancing, and at least one peer demonstrably holds descriptors we lack.
  • This is not a new mechanism. The repo already runs a bounded round loop on this lane for post-approval curator sync (MAX_POST_APPROVAL_CURATOR_SYNC_ROUNDS). SWM catch-up can leave a partial unrecovered tail after phase timeouts under backpressure #2050 is that loop missing from the general foreground catch-up. Admission is acquired and released per peer per pass by the existing code, so scheduler fairness is preserved by construction — no coordinator, no manifest freeze, no new admission primitive, no change to ordered-sync.ts or the single-flight keys.
  • A second defect, not in the issue: head rows were deleted and never rewritten. replaceHeadMetadata is delete-only, and the compensating bulk storeInsert(processed.verifiedMeta) sits below the continue on the failure branch. A partial round therefore deleted head rows for the KAs it had just materialized: content present, heads absent, invisible to every head reader, and permanent — the next round sees the content and skips. Now compensated immediately after every call that deletes a head, inside the existing write lock.
  • A third defect, caught by this PR's own review, in this PR's own fix. snapshotsResolved counted refs fetched, not KAs materialized — and that field is read by three consumers: the terminal message, the capability gate, and the coverage high-water mark. A round that cached all N refs and failed every materialization reported N/N, so the gate computed N < N → false and dropped the peer while the mark saturated. The walk stopped having written zero KAs, silently disabling the retry loop in exactly the failure class the loop exists for. It presented as a reporting defect and was classified as one. missingCount is now derived from the resolved count, so resolved + missing === total holds by construction.
  • Terminal reporting now names the shortfall and why the loop stopped. Previously a job could end unreachable saying nothing about shared memory at all. Stop reasons render through an exhaustive Record over a closed union, so a new reason fails the build rather than rendering nothing.

Deferred deliberately — please read before approving

  • Metadata and aggregate-data replay per pass. Each pass re-fetches the manifest per capable peer. This is the accepted cost of the chosen design and is now measurable: replayPhaseBytesReceived / snapshotPhaseBytesReceived split the previously-merged bytesReceived scalar. → Frozen manifest — remove metadata and aggregate-data replay per pass #2078
  • The paired Base Sepolia acceptance runs have NOT been run. Methodology is prepared: rows normalized per job and per materialized KA (never per admission — extra passes create admissions by construction), queue-wait labelled admitted-only because syncSchedulerQueueWaitMs is recorded only after start() succeeds, and validity gated on comparable rejection counts. In a change that increases admissions, an unlabelled p95 would be unfalsifiable in the flattering direction.
  • In-agent runCatchupOverPeers parity is not implemented. All three live callers are already bounded retry loops, so naive parity would nest a loop inside loops that already retry — a defect, not a missing half. That path still propagates every SWM catch-up can leave a partial unrecovered tail after phase timeouts under backpressure #2050 diagnostic; it does not repeat the walk.
  • snapshotsTotal > 0 in the capability gate is inert — a defensive restatement its neighbour already enforces at 0 < 0. Documented in code as unpinnable-by-construction rather than patched around with an impossible fixture.

Related

Diagrams

Flow 1 — the snapshot walk when the deadline expires mid-list

Before — one pass per peer; the tail is abandoned and the missing set is discarded:

sequenceDiagram
    participant Job as Catch-up job
    participant Walk as syncPublicSnapshotsForMeta
    participant Peer
    participant Store as Triple store
    Job->>Walk: one pass (120s deadline covers meta + data + ALL snapshots)
    Walk->>Peer: fetch manifest (250 snapshots)
    loop until the deadline expires
        Walk->>Peer: fetch snapshot[i]
        Walk->>Store: materialize KA[i]
    end
    Note over Walk: deadline expires at i=178
    Walk-->>Job: return (72 snapshots abandoned, identity DISCARDED)
    Job-->>Job: terminal "unreachable", partial graph, no resume path
Loading

After — the walk repeats while coverage advances and a peer still holds what we lack:

sequenceDiagram
    participant Job as Catch-up job
    participant Walk as syncPublicSnapshotsForMeta
    participant Peer
    participant Store as Triple store
    Job->>Walk: pass 1
    Walk->>Peer: fetch manifest (250 snapshots)
    Walk->>Store: materialize 178 KAs
    Walk-->>Job: coverage {resolved 178, total 250, missing 72}
    Job->>Job: shouldRunAnotherCatchupPass -> continue
    Job->>Walk: pass 2 (shared-memory plane only)
    Walk->>Peer: re-fetch manifest (accepted replay cost)
    Walk->>Store: materialize remaining 72 KAs
    Walk-->>Job: coverage {resolved 250, total 250, missing 0}
    Job->>Job: no capable peers left -> stop
    Job-->>Job: terminal "done", complete graph
Loading

Flow 2 — head metadata on a partial round (the defect not in the issue)

Before — the head is deleted and its rewrite is unreachable:

sequenceDiagram
    participant Walk as syncPublicSnapshotsForMeta
    participant Mat as snapshotMaterializer
    participant Store as Triple store
    Walk->>Mat: replaceHeadMetadata(KA)
    Mat->>Store: deleteByPattern(head + operations)
    Note over Mat,Store: delete-only, no insert
    Walk->>Store: write assertion graph (content)
    Note over Walk: snapshot phase incomplete -> continue
    Walk--xStore: storeInsert(verifiedMeta) NEVER REACHED (below the continue)
    Note over Store: content present, heads ABSENT<br/>invisible to every head reader, and permanent
Loading

After — each KA's verified metadata is rewritten immediately, inside the same lock:

sequenceDiagram
    participant Walk as syncPublicSnapshotsForMeta
    participant Mat as snapshotMaterializer
    participant Store as Triple store
    Walk->>Mat: replaceHeadMetadata(KA)
    Mat->>Store: deleteByPattern(head + operations)
    Walk->>Store: insertVerifiedDescriptorMeta(KA), same write lock
    Note over Store: head + operations restored per KA
    Walk->>Store: write assertion graph (content)
    Note over Walk: snapshot phase incomplete, continue
    Note over Store: content AND heads present<br/>the round is durable
Loading

Files changed

File What
packages/agent/src/sync/requester/shared-memory-sync.ts The walk. Per-peer coverage record; snapshotsResolved counts materialized refs with missingCount derived from it; progress carried out through a throw on a non-enumerable, validated-on-read payload; per-KA verified-meta insert at both head-deleting call sites, with a perKaInsertedMetaKeys ledger so insertedMetaTriples stays byte-identical on a usable round; bytesReceived split into replay vs snapshot phases
packages/agent/src/sync/catchup-pass-policy.ts New. shouldRunAnotherCatchupPass over a closed CatchupPassDecisionReason union, plus budget/max-pass resolvers. Lives in packages/agent because packages/agent has no dependency on packages/cli
packages/agent/src/dkg-agent-types.ts SwmSnapshotCoverage, continuation diagnostics, the byte-split fields
packages/agent/src/dkg-agent-lifecycle.ts Threads the new diagnostics through the single runSharedMemorySync caller
packages/agent/src/index.ts Exports the pass policy and coverage types for the CLI
packages/agent/src/sync/catchup-policy.ts One-line type touch
packages/cli/src/catchup-runner-worker-impl.ts The bounded pass loop; capablePeersForNextPass; highestResolvedCoverage; per-pass structured log line; retention of a peer's coverage across a failed round (delete only on a clean round that reported none)
packages/cli/src/context-graph-readiness.ts swmShortfallClause; stop reasons rendered through an exhaustive Record
packages/cli/src/catchup-runner.ts Passes the continuation config through to the worker
packages/agent/test/sync-requester-progress.test.ts Coverage-record rows, the throw-path row (T14), the reduction/selection rows
packages/agent/test/swm-snapshot-materializer.test.ts T9/T9b against a real OxigraphStore — partial-round heads, and the already-materialized repair for an absent or older-version head
packages/agent/test/swm-descriptor-fixtures.ts New. Shared descriptor-shaped SWM share builders, so a fourth hand-rolled fixture is not needed
packages/agent/test/catchup-pass-policy.test.ts New. Stop-reason precedence and budget/max-pass resolution
packages/agent/test/swm-public-snapshot-materialization.test.ts Materialization-failure accounting
packages/cli/test/catchup-runner-worker-impl.test.ts The composed continuation chain end to end; capability-gate rows; coverage-retention pair; per-pass log line
packages/cli/test/context-graph-catchup-readiness.test.ts T16 — all ten terminal error strings, previously with zero coverage — and T16b, the shortfall clause
packages/agent/vitest.unit.config.ts Adds the new test files to the include allow-list (it is an allow-list; a file not added is silently uncollected)

Test plan

  • packages/agent and packages/cli unit lanes, as "baseline + exactly N named new tests" rather than "identical to baseline"

  • Attribution guarantee: 20/20 files any chunk touches are green at base (31be166b8), enumerated by name — 9 CLI, 11 agent. Three agent files the full run never reached were run separately at the same commit in the same pristine worktree (3 passed | 35 passed) rather than assumed. Red in any of those 20 now is a real regression.

  • Mutation set, serial, kill sets predicted in advance. 5 killed, 1 documented survival:

    Mutant Result
    coverage high-water <=< killed (3 rows)
    loop stops after the first pass killed — the seam row, on the pass count
    manifestCompletetrue killed (truncated-peer row)
    snapshotsTotal > 0>= 0 survives — the clause is redundant, unpinnable by construction, documented in code
    unconditional delete of peer coverage killed (RETAINS row)
    unconditional retain killed (FORGETS row) — disjoint subsets, so the pair tests the predicate
    materializedRefsForCg0 on the throw path killed T14 (2 → 0); the previous T14 asserted 0 and would have passed
    logPassLine emits nothing killed exactly one row (expected [] to have a length of 1)

    All kills are assertion deaths, not timeouts — 10–21 ms against vitest's 5000 ms minimum, checked because a test that times out under load reads as a killed mutant.

  • Removing the widened already-materialized repair survived 26/26 rows — not thin coverage, none. Reachability, not assertion strength: both existing rows go through the materialize path while that exit short-circuits earlier. Closed by T9/T9b.

  • Paired Base Sepolia runs — NOT DONE. Same receiver snapshot and fixture, baseline commit then candidate; exact 250 KA / 30,900 triple parity and terminal done; scheduler metrics normalized per job and per materialized KA; replay reported separately from useful bytes.

Known-red at base, not caused by this PR

Neither lane is green at base on Windows, and this PR does not claim otherwise. There is no whole-lane agent total and none is invented here — the deepest run reached 72 of ~151 files with 52 failures confined to 4 RFC-64 files (native-gate1.integration 29, dkg-agent-native-wiring.integration 16, current-head-discovery-v1 4, transport-v1 3). No chunk touches any of them.

That red is deterministic, not flaky and not contention: gate1 fails 29/29, measured three times — 904 s contended, 941 s alone on a verified-clear box, 965 s. The chain is EBUSY: resource busy or locked, unlink …inventory-v1.lease.sqlite3Hook timed out in 10000ms → every test timing out at its 30 s limit awaiting setupLiveReceiver(). A teardown file-handle failure, not an assertion, so the four files should be subtracted by name rather than re-measured.

This does not mean the branch lost its CI signal. Those files are in the local test:unit allow-list, not in rfc64-inventory-windows.yml, and #2053's SQLite lifecycle (Windows) passed on this same base.

Jurij89 and others added 17 commits August 4, 2026 16:34
Chunks 1 and 4 of the #2050 plan, plus the test rows landed so far.

Chunk 1 - per-peer SWM snapshot coverage:
- SwmSnapshotCoverage carried as ONE coherent record, selected whole from a
  single peer. Ranked authority -> manifestComplete -> largest snapshotsTotal
  -> most resolved. Ranking by FRACTION returns 200/200 over 178/250 and so
  reports "0 outstanding" on a job 72 assets short - self-consistent and
  undetectable downstream, which is worse than the synthetic pair the record
  shape exists to prevent.
- replayPhaseBytesReceived / snapshotPhaseBytesReceived split out of the merged
  bytesReceived scalar, so the accepted cost of repeating the walk is
  measurable in bytes rather than only in triples.

Chunk 4 - the bounded repeat:
- shouldRunAnotherCatchupPass: pure, injectable clock, documented stop-reason
  precedence (coverage-stalled outranks budget-exhausted, because a stalled run
  that also ran out of time must not read as "raise the budget").
- Capability gate reads coverage ONLY. A yielding peer carries failedPeers > 0
  and is exactly the peer the next pass exists to revisit, so gating on
  "no failures" would strand the work the loop was built for.
- Coverage is retained when a round fails and deleted only after a clean round
  reports none. A throw, a denial or a backpressure-deferred plane all return
  no coverage without being evidence of absence - and deferral is the r26 shape
  specifically, with sync-global at 2/2 for 99.7% of that job.
- Distinct-peer accounting via Sets; peersNotAttempted could otherwise go
  negative once passes repeat.
- Continuation passes are shared-memory only, so repeats never re-pull durable.

Tests:
- All ten terminal readiness strings pinned (previously zero coverage).
- T9 reproduces G7 against a real OxigraphStore and FAILS on this commit by
  design: assertion graphs are written while head rows are absent. It goes
  green with Chunk 3's per-KA metadata insert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Chunk 2. Three changes that let a repeated peer walk make progress
instead of restarting into the same wall.

Skip-and-continue on a short prefix. Ref order is byte-identical every
pass (Map insertion order), so returning on an unserveable ref pinned
every future pass at that index and drove coverage to a fixed point.
The ref is now recorded and the walk continues; the KA stays uncached
and unapplied, and is retried from offset zero next pass.

A non-blocking yield before each KA, checked before the fetch. A cache
"hit" is O(KA size) and a miss is a round trip, so the clock is read
before either. Checking before the fetch also means no SyncPageResult
exists on that path, so timedOutPhases structurally cannot move: a
local budget decision can never be reported as a peer timeout and put
a healthy responder into backoff. It does set failedPhases - the round
genuinely did not complete the plane, and without that a yielding round
classifies as clean and reports the graph done with KAs still missing.

Bounded missing state: an exact missingCount plus a missingSample
capped at 10, since a public peer controls manifest size.

completed is now derived (missingCount === 0) rather than hardcoded.
All three give-up paths route through one noteMissing helper, so a
round can no longer fall out of the loop claiming success while having
abandoned work - which is exactly what skip-and-continue would have
done against the old hardcoded true.

swm-recovery.test.ts passes with no edits (14 tests), including the
single-snapshot short-prefix case that pins completed: false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
…2050)

Chunk 5. The incomplete-progress terminal could not say WHAT was
missing, even though the answer was computable in memory at the moment
the round gave up. It now appends a clause naming the counts, the peer
they came from, the pass count, and a bounded sample of the
outstanding Knowledge Assets.

Every figure comes from ONE SwmSnapshotCoverage record, so the
sentence cannot describe a graph state no peer reported and the named
KAs always belong to the manifest the counts came from.

The base sentence is byte-identical; the clause is appended. It is
omitted entirely - exactly '' - when there is no coverage record, and
when the selected manifest was fully resolved: in that case the
shortfall lies on another plane and "0 outstanding" beside an
unreachable verdict would misdirect the reader.

An incomplete manifest is reported as a lower bound rather than a
total. Wording is scoped to "Shared memory" throughout, since
continuation passes repeat the shared-memory walk only and must not
imply the durable plane was retried.

T16 pins that sentence as a PREFIX so the append does not break it -
which means T16 is satisfied by the prefix followed by anything at
all, including nothing. T16b added here is the row that can see the
append: whole-string equality plus the cap, the truncation marker, the
pass-count singular/plural, and both omission cases. Mutating the
clause to vanish kills 7 of its rows while T16's prefix pin stays
green, which is the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
…g exit (#2050)

`replaceHeadMetadata` is delete-only, and the compensating
`storeInsert(processed.verifiedMeta)` sits below the `continue` on the
incomplete branch. A round that ran out of clock mid-list therefore deleted
the head rows of the KAs it had just materialized and never rewrote them:
content present, heads absent, invisible to every head reader — and
permanent, because the next round sees the content and skips.

Each KA's verified metadata is now written inside its own write lock,
immediately after the delete, at all three exits: the union-insert repair,
the replace path, and the already-materialized return. That last one is
widened from "no head at all" to "the head does not certify THIS
descriptor's version", so a head left behind at an older version by a pass
that stopped between the replace and the swap is repaired too. Both states
are unreachable by any content-based check and never self-heal through
partial rounds, which are the only rounds this scenario gets.

`insertedMetaTriples` keeps its exact meaning: on a usable round it stays
`processed.verifiedMeta.length`, byte-identical to before, because the bulk
insert now counts the full length minus the rows the per-KA path already
wrote. On a partial round it becomes non-zero, which inverts the field's
diagnostic sense — it was the G7 symptom, it is now the repair signal.
The ledger subtracts by size rather than re-filtering so the identity holds
even if a meta response carries a duplicate quad.

Also captures materialization counters before a throw. A snapshot-phase
transport failure unwinds past every post-call merge, so a round that
materialized N KAs and then threw reported zero inserted triples and read
to the pass loop as non-advancing progress. The counters are now added per
KA, inside the lock, once the KA is durably committed.

Adds `continuationStopReason` to the shared-memory diagnostics, typed as the
pass policy's closed union so a new stop reason cannot reach the terminal
message unnoticed, and requires `manifestComplete` in the capability gate: a
truncated-manifest round caches blobs and advances `snapshotsResolved` while
materializing nothing, which would otherwise satisfy the coverage-advance
gate forever. A peer that yielded mid-snapshot-list completed its meta phase
first, so it still qualifies.

Refs #2050.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
…2050)

The shortfall clause reported how much was missing but not why the
loop gave up. "72 outstanding because a further pass stopped making
progress" and "72 outstanding because the time budget was exhausted"
call for opposite operator actions, and the message could not tell
them apart.

Renders continuationStopReason through an exhaustive Record over the
closed CatchupPassDecisionReason union, so adding a reason fails the
build here rather than silently rendering nothing. Omitted when the
loop never ran - shared memory not requested - and for 'continue',
which is not a stop.

coverage-stalled deliberately outranks budget-exhausted in the policy,
so a run that stalled AND expired reports the stall. Its wording
therefore never mentions time: "ran out of time" would send an
operator to raise a budget that buys nothing, which is the precise
misdirection that precedence exists to prevent. A test asserts the
text matches no /budget|time|expired/, and mutating it to blame the
clock kills exactly that row.

Also states both facts an incomplete manifest carries: the denominator
is a lower bound AND that peer was not retried, since the capability
gate requires a complete manifest. A truncated round advances
snapshotsResolved against a truncated denominator while materializing
nothing, so "the count understates it" and "we stopped asking" are
different things for the reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
A catch-up that will not converge is currently only diagnosable by
reconstructing it from counters, and the two states an operator most needs
to tell apart — a walk that stopped because it finished and one that
stopped because it gave up — look identical in the numbers.

Each pass now emits one line carrying the Context Graph, the capable-peer
count, coverage before and after, elapsed time, and the typed stop reason.
Coverage is always printed with the 8-char suffix of the peer that reported
it: the record is selected whole from one round, so the counts and their
peer belong together, and printing the counts alone would invite reading
them as a fleet total. A truncated manifest says so, because its denominator
is only a lower bound.

Coverage before and after appear on the same line deliberately. A pass with
large elapsed time and unmoved coverage is the signature of a job that is
not converging, and that is invisible in either number alone.

The Worker has no logger, so the line is formatted where the coverage
records live and emitted through a new `logCatchupPass` host call. That call
can never affect the job: a catch-up that failed because a log line could
not be delivered would be strictly worse than one with a missing log line,
so the RPC rejection is swallowed. That also keeps a host which does not
implement the method from turning observability into a dependency.

Refs #2050.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Second half of "capture counters before a throw". The first half made
the DATA counters honest by adding them per KA inside the write lock;
this makes the DECISION honest.

The continuation loop's progress signal is
swmCoverage.snapshotsResolved, and that record is assembled from
syncPublicSnapshotsForMeta's return value. A snapshot-phase transport
failure throws, so the return never happened: a round that materialized
120 Knowledge Assets and failed on the 121st recorded no coverage at
all, the high-water mark did not move, and the loop reported
coverage-stalled and abandoned a peer that was converging. That is the
r26 shape and the behaviour #2050 exists to remove - and with the
counters already fixed it was worse than before, because the
diagnostics looked right while the decision was wrong.

The walk now attaches its own counts to the error and rethrows; the
outer catch folds them into a coverage record. Everything from the
failing index on is counted missing, so resolved + missing === total
holds on the failure path exactly as on the returned one.
manifestComplete is hoisted out of the try so a throwing round can
still mark a truncated denominator as a lower bound.

The payload is non-enumerable so it cannot widen a structured clone or
a log dump, and it is validated on read rather than trusted: it crosses
an unknown boundary, and a fabricated denominator would corrupt the
record that both the capability gate and the terminal message read.

No new abandonment path - the throw route calls the same abandonFrom
helper - so completed: missingCount === 0 remains the single completion
expression. No third bytesReceived site, so
replayPhaseBytesReceived + snapshotPhaseBytesReceived === bytesReceived
is untouched.

T14 pins it. Mutating the recovery away reproduces the pre-fix symptom
exactly: expected undefined to deeply equal the record.

Also removes a duplicate import pair appended with T13 - the same two
symbols are already imported at the top of the file. It survives
esbuild today, but tsc does not typecheck test files, so nothing would
have caught it under a stricter toolchain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
…ps (#2050)

`continuationStopReason` was assigned on every pass decision, so the field
transiently held `'continue'` — a value that is not a stop reason at all.
It could not escape today, because the loop's only exit is the break on a
non-continuing decision, but that made the guarantee a property of the
current control flow rather than of the type: one added exit path, such as
a cancellation break after a pass, would have published it.

Assigning only on the stopping decision makes `'continue'` structurally
unrepresentable in the field. The terminal message already absorbed it —
`SWM_STOP_REASON_TEXT.continue` is the empty string, which is falsy and
omits the clause — so this would have degraded to a missing sentence rather
than malformed text. That defensive entry is now provably dead rather than
load-bearing, which is the better state for it to be in.

Refs #2050.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
…erified (#2050)

Two seam defects that had to be fixed together, because fixing the
first alone unmasks the second.

FINDING 1 - the terminal message went silent on shared memory in
exactly the failure class #2050 is about. swmShortfallClause gated on
missingCount, which measures FETCH completeness only; it is produced
entirely by noteMissing inside the snapshot walk. Materialization
failures are a separate counter and never touch it, while usability
reads both. So a round where every ref fetched cleanly and some KAs
could not be WRITTEN - a store error inside the KA write lock, the G7
failure class, likeliest under the same store pressure that produces
incomplete rounds - produced missingCount === 0 and an empty clause.
The operator got the base sentence and nothing at all about shared
memory in the one case where shared memory was what failed.

FINDING 2 - snapshotsResolved counts refs present and digest-valid in
the blob CACHE, not Knowledge Assets written, and the clause rendered
it as "snapshots verified". Finding 1 was masking it: with the clause
suppressed, the misleading sentence never printed. Fixing the gate
alone would have started reporting "250/250 snapshots verified" on a
graph where every write failed - wrong in the flattering direction and
undetectable by the reader. Now "fetched", with writes reported
separately.

The two shortfalls are separate clauses, not one conflated number,
because they send an operator to different places: not retrieved is a
network and peer-set problem, retrieved but not written is a store
problem.

materializationFailures now travels ON the coverage record. Both the
success path and the throw path build that record through ONE shared
recordSnapshotCoverage helper, so it can never be reassembled in a
catch block from scalars that did not travel together - the failure
mode the whole-record contract exists to prevent. The counter is
mirrored outside the try so a later throw cannot lose it, and is
defaulted rather than trusted on read, since the record crosses a
worker RPC boundary.

Mutating the gate back to retrieval-only kills exactly the two rows
that describe the defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
The most consequential defect in this PR, and it sat inside the fix.

snapshotsResolved was walk.readySnapshots - refs retrieved and
digest-valid in the blob cache. The walk increments that
unconditionally after the materialization hook, and the hook swallows
its own failures into materializationFailures without rethrowing. So a
round where all 250 refs are cached and EVERY materialization fails
produced 250/250 with missingCount 0.

Three consumers read that number, not one:
  - the capability gate computed 250 < 250 -> false -> peer not capable
  - the coverage high-water mark advanced to maximal
  - the terminal message consulted missingCount and stayed silent

So the continuation stopped, silently, having written zero Knowledge
Assets - a round that fetched everything and materialized nothing
looked like perfect progress to the loop meant to catch exactly that.

Resolved now counts refs whose every descriptor is locally present.
missingCount is derived as total - resolved, so it covers both
never-fetched and fetched-but-unwritten without tracking either twice,
and the invariant holds by construction rather than by arithmetic
maintained in two places. Message, gate and high-water mark correct
together, and "250/250 verified on a graph with zero written" becomes
unrepresentable rather than merely avoided.

The walk's own readySnapshots is deliberately unchanged: it feeds
completed/missingCount, which are genuinely about FETCH completeness
and are consumed by the private lane. swm-recovery.test.ts passes with
zero edits, as it has all session.

A truncated-manifest round now reports resolved 0, which is correct -
nothing was written - and manifestComplete already gates that case in
the capability predicate.

Mutating resolved back to readySnapshots kills both coverage rows.

Also removes T14's invariant assertion rather than rewriting it. It
computed 2 + 1 === 3 from an object the preceding deep-equal had
already pinned, and the invariant now holds by construction, so any
version of it is a check that cannot fail. A vacuous assertion is
worse than none - it reads as coverage of a property nothing checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
T9 pins that a partial round leaves head rows for exactly the KAs it
materialized, and none for the rest, asserted over the full final quad
set against a real OxigraphStore. It failed on the pre-fix tree with
`expected [] to deeply equal [ …(2) ]` — both assertion graphs written,
zero head rows — which is the r26 residual stated as a test rather than
inferred from insert counters.

T9b covers the THIRD head-deleting exit: the already-materialized return
where the head is absent (`version === null`, so `needsRepair` is false)
or pinned at an older version. Neither T9 row reaches it — both go
through the materialize path — and a mutation removing the exit's
widening survived 26/26 before this.

Both run on the PARTIAL branch deliberately. On a complete round the
bulk `storeInsert(processed.verifiedMeta)` writes every head regardless,
so the assertions would pass with the repair removed; with the round
incomplete, the exit's own insert is the only thing that can write them.

`share()` gains `ual`/`payloadCount`, both defaulting to the previous
values, so v1/v2 and the 13 existing tests are behaviourally unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Moves `share()`/`manifest()` into `swm-descriptor-fixtures.ts` so the
throw-path row in `sync-requester-progress.test.ts` can build
DESCRIPTOR-shaped metadata instead of hand-rolling a fourth SWM fixture.

Why this matters beyond deduplication: materialization is gated on
`snapshotDescriptorsByRef.size > 0`, populated by
`parseGraphScopedSwmRecoveryDescriptors`. Metadata carrying only
`publicQuadsDigest`/`publicQuadsCount` is `collectPublicSnapshotMetadata`
shape — enough to enumerate refs, not enough to yield a descriptor. A
fixture in that shape leaves the map empty, so `snapshotsResolved` stays
0 for a reason unrelated to the code under test.

And a subtly wrong fixture does not error: `validateOperationRows` throws
on a kaUal/assertionVersion mismatch, the caller catches it and calls
`snapshotDescriptorsByRef.clear()`, silently disabling materialization
for the whole Context Graph. It stops testing rather than failing.

These builders are known-good by provenance: the partial-round row built
on them failed pre-fix with `expected [] to deeply equal [ …(2) ]`
against a real OxigraphStore, which is only reachable if the descriptors
are real.

Positional signatures are kept as thin adapters so every existing call
site is untouched. All 17 tests pass, including the 13 pre-existing ones
— re-proven by run, since a defaults-preserving refactor is exactly the
kind that looks obviously safe and isn't.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
T14 is titled "carries the resolved count out through the throw instead
of reporting zero" and asserted snapshotsResolved: 0. Its fixture wired
no snapshotMaterializer, so zero was true BY CONSTRUCTION - the row
passed while structurally unable to observe its own property.

Two opposite properties live on snapshotsResolved, and a
no-materializer fixture collapses them to the same number:

  - do not OVER-report: a round that fetches N and materializes 0 must
    not claim N/N. Covered by the coverage rows in this file, precisely
    BECAUSE they wire no materializer, so fetched != materialized.
  - do not UNDER-report: a round that materializes some and then THROWS
    must report what it wrote. Covered by nothing - and it is the whole
    reason "carry snapshot progress out through a throw" exists.

Distinct from the correct-mutant-wrong-property family: the assertion
was sound; the FIXTURE erased the distinction, so no strengthening of
the assertion could have helped. Renaming the row would have made the
title honest and left the load-bearing property at zero coverage.

Now builds DESCRIPTOR-shaped metadata via the shared fixtures, wires
the real materializer against a real OxigraphStore, and caches two of
three snapshots so the third's fetch throws after two are materialized.
Expectation becomes 2 resolved / 1 missing.

A real store and the real materializer rather than a stub: descriptor
metadata that is subtly wrong does not error. validateOperationRows
throws, runSharedMemorySync catches it and calls
snapshotDescriptorsByRef.clear(), and materialization is SILENTLY
disabled for the whole Context Graph - reproducing resolved: 0 for a
new reason, indistinguishable from a pass.

Verified by mutation, not by the passing run: forcing the throw path's
coverage record to 0 resolved kills the row with

    -   "snapshotsResolved": 2,
    +   "snapshotsResolved": 0,
    -   "missingCount": 1,
    +   "missingCount": 3,

which is the pre-fix defect exactly. The previous row asserted 0 and
would have PASSED under that mutant. 56/56 green with it reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Every link in the continuation chain was pinned individually — the coverage
record survives a throwing round, the capability gate reads coverage rather
than failure counters, the high-water mark advances, and the pure stop rule
decides correctly. Nothing executed the seam joining them: that a round
which materialized some Knowledge Assets and then failed actually causes a
second pass to run.

That gap is where both of the last two defects on this change survived. Each
piece was individually correct; a chain of correct links can still fail to
be connected.

The load-bearing assertion is the pass COUNT, not the terminal coverage
record. A row that checks only the final numbers passes whether the loop ran
once or twice, because the second pass simply overwrites the first pass's
record with the converged values. It also asserts the durable plane was
pulled exactly once, pinning continuation passes as shared-memory only.

A negative row covers the other direction: a first pass that resolved
nothing does not earn a repeat, since the high-water mark starts at 0.
Without it the positive row would pass under an implementation that always
ran a second pass.

Mutation: forcing the loop to stop after the first decision regardless of
its outcome kills the seam row on the pass count
(`expected [ peer ] to deeply equal [ peer, peer ]`) and leaves the other
35 rows green — including the negative row, which already expects one pass.
Kill set predicted before running, source verified clean before and the
mutant verified present after, then reverted to 36/36.

Refs #2050.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Nine of the ten terminal error strings in this file are pinned with
toBe. One used c.error?.startsWith(...) - and a prefix pin is satisfied
by the prefix followed by ANYTHING, including nothing. That is the
exact pattern this PR already catalogued after mutating a clause to
emit nothing and watching seven rows die while a prefix pin stayed
green.

Verified the weaker form bought nothing: replacing it with full
equality passes unchanged, 50/50. The suffix it was tolerating does not
exist in this scenario, so the row was strictly weaker for no reason.

toBe now fails if the shortfall clause is appended here, dropped, or
reworded - none of which startsWith could see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Both surviving gate clauses were unpinned, and the obvious fixture cannot
see either. A single barren peer leaves the high-water mark unmoved, so the
loop stops at `coverage-stalled` before the gate is consulted and the row
passes whether the guard exists or not — the same fixture-collapse pattern
that let a throwing-round row assert `snapshotsResolved: 0` under a title
promising a non-zero one.

Pairing a productive peer with the peer under test reaches the gate, and the
assertion becomes the MEMBERSHIP of the second pass rather than whether one
happened.

`manifestComplete` is now pinned: mutating it to `true` re-contacts a
truncated-manifest peer and the row dies with
`expected [ 'peer-truncated-3333', …(3) ] to have a length of 1 but got 4`.

`snapshotsTotal > 0` is NOT pinned, and the row does not pretend otherwise.
Mutating it to `>= 0` leaves all 38 rows green, because the clause is
redundant: a barren peer reports `0/0`, and `snapshotsResolved <
snapshotsTotal` is already false there. For the clause to change any
outcome, `resolved < total` would have to hold with `total <= 0` — that is,
a negative resolved count, which no path produces. It is a defensive
restatement of a condition its neighbour already enforces, kept for
readability rather than effect, and recorded as such instead of being
reported as covered.

Refs #2050.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
…mutants

The retain-on-failure rule had no coverage, and finding an observable for it
meant ruling two out. After a throwing pass the stop reason cannot tell
retention from forgetting: retained leaves coverage equal to the high-water
mark, forgotten makes the max over an empty map 0, and both are <= the mark,
so both report `coverage-stalled`. The terminal message cannot either — it
renders the reduced `diagnostics.sharedMemory.swmCoverage`, which
`accumulate` builds independently of `lastCoverageByPeer`, so forgetting a
peer there is invisible to it.

A second, still-productive peer keeps the mark advancing so a further pass
runs at all, and membership of that pass becomes the observable. That peer's
rate is load-bearing: at +1 per pass the peer under test, retained at 2,
holds the max flat and the loop stops before the pass the row depends on.

Both mutants die, on disjoint rows, which is what shows the pair tests the
predicate rather than merely that something changed:

  unconditional delete (pre-fix)  -> kills RETAINS, leaves FORGETS green
  unconditional retain            -> kills FORGETS, leaves RETAINS green

The FORGETS row needed a second fix to earn that. With a default clean round
it passed under unconditional retain, because a clean DATA-BEARING round
proves the plane and the loop stops at `plane-proven` before the capability
gate is consulted — a negative control that could not see the guard it
existed to protect. It now returns a clean round carrying no data.

Also corrects the gate's own comment, which claimed `snapshotsTotal > 0`
excludes barren peers. It does not: `snapshotsResolved < snapshotsTotal`
already fails them at 0 < 0. The clause has no reachable effect and no
honest fixture can kill it, so the comment now says so — in the code, where
the next mutation window will read it, rather than in a commit body.

Refs #2050.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
@Jurij89

Jurij89 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up issues filed

Deliberately out of scope here; each is referenced in the PR's What is NOT fixed section.

Not yet run: the paired Base Sepolia baseline/candidate runs. Methodology is prepared — rows normalized per job and per materialized KA (never per admission, since extra passes create admissions by construction), queue-wait labelled admitted-only because syncSchedulerQueueWaitMs is only recorded after start() succeeds, and validity gated on comparable rejection counts.

Comment thread packages/cli/src/catchup-runner-worker-impl.ts Outdated
Comment thread packages/agent/src/sync/requester/shared-memory-sync.ts
Comment thread packages/agent/src/sync/requester/shared-memory-sync.ts
Comment thread packages/agent/src/sync/requester/shared-memory-sync.ts
Comment thread packages/agent/src/dkg-agent-types.ts Outdated
Comment thread packages/agent/src/dkg-agent-types.ts
Comment thread packages/cli/test/context-graph-catchup-readiness.test.ts
Comment thread packages/agent/src/sync/requester/shared-memory-sync.ts
Comment thread packages/agent/src/sync/requester/shared-memory-sync.ts
Comment thread packages/agent/src/dkg-agent-lifecycle.ts
Jurij89 and others added 2 commits August 4, 2026 20:36
The per-pass line was the one #2050 surface with no coverage. It is the
only PER-PASS observability an operator gets - the terminal record
reports the FINAL state, so without it a job that converged in four
passes is indistinguishable from one that converged in one.

It needs a row rather than a reader because it fails silently by
design: the line travels as a fire-and-forget RPC whose rejection is
deliberately swallowed so observability can never fail a catch-up. A
line that stops being emitted therefore produces no error anywhere.

Asserts the TRANSITION, not the endpoint. A line reporting only the
final 3 could not distinguish a pass that advanced coverage from one
that ran and achieved nothing, which is the single fact the line exists
to convey.

Verified by mutation: stubbing logPassLine to emit nothing kills this
row with "expected [] to have a length of 1", in 21ms - an assertion
death, not a timeout. Exactly ONE test died, which is the evidence it
covers something no other row does. 41/41 green with it reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Comment-only. Found by re-reading T16b to confirm its assertions match
its name - they do (full-equality toBe on the clause, plus the pass-count
wording). Its HEADER did not: it justified T16b's existence by saying
T16 pins that message as a prefix, which stopped being true when the
preceding commit strengthened T16 to toBe.

Self-inflicted staleness of exactly the kind this PR kept finding: the
fact was corrected in one place and the prose explaining WHY a sibling
exists still encoded the retired version.

The justification is restated on the durable ground rather than the
one that moved: T16's fixture never appends a shortfall, so no
assertion of any strength in it can observe this clause. Strength and
reachability are different properties, and it is reachability that
makes these rows necessary - which stays true however T16 is later
asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
Comment thread packages/agent/src/sync/requester/shared-memory-sync.ts
Comment thread packages/agent/src/index.ts
Review on #2076 raised three defects in the fix itself. All three were
validated against the code by independent readers and adversarially
challenged before being accepted; the severity arguments are recorded on
the threads.

1. The progress gate and the capability gate read different peer sets.
   `capablePeersForNextPass` retries only peers with `resolved < total`,
   but `highestResolvedCoverage` maxed over EVERY retained record. A peer
   at 400/400 pinned the reading, and a capable peer converging against a
   120-ref manifest could never clear that bar, so it got exactly one
   continuation pass however well it was doing — and the stop was reported
   as `coverage-stalled`, whose text says more passes would not help.

   Replaced with `totalPeerProgress`, a sum over a monotone per-peer
   high-water ledger. Filtering the max to the capable set was rejected: it
   inverts the bug, because the capable set shrinks as peers finish.

2. The plane-proven stop was consulted before the capability gate and read
   an aggregate over every peer, so a member serving its own SWM rows and
   no snapshot refs — the ordinary shape in a multi-member public CG —
   ended the walk while another peer still had a complete manifest with
   refs outstanding. The loop contributed zero passes in exactly the
   topology it was written for. Now gated on the capable set being empty.

3. `missingCount` was redefined as a materialization shortfall while the
   type doc and terminal formatter still called it retrieval, so a ref that
   fetched and digest-verified but failed to write was named "not
   retrieved" — sending an operator to the network when the fault was the
   store. Note `materializationFailures > 0` with `missingCount === 0` is
   now unrepresentable, and a fixture pinned exactly that pair.

   Relabelled to "materialized"/"not materialized", corrected the type
   docs, made the write count a cause indicator rather than a second
   disjoint axis, deduped the ref sample, and rebuilt the T16c fixtures on
   records the producer can actually emit.

Each new row was mutation-tested: reverting the fix kills it on an
assertion, not a timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Branimir Rakic and others added 2 commits August 5, 2026 00:53
Review on #2076 found three behaviours this PR introduced or changed that
no test drove. Each row below was mutation-tested: reverting the behaviour
it pins kills it on an assertion.

1. Skip-and-continue. The short-prefix branch of `syncPublicSnapshotsForMeta`
   became `noteMissing + continue`, so one unserveable ref no longer pins the
   manifest — but every existing short-prefix row declares exactly ONE ref,
   where "skip this ref" and "abandon the manifest" are indistinguishable.
   Two refs now: a cleanly-closed short prefix followed by a valid snapshot,
   asserting the second is still requested, verified and cached while the
   prefix is not. Under the `continue` -> `break` mutation the pre-existing
   sibling row still passes and only this one fails, which is the blind spot
   stated as evidence.

2. Deadline yield. A voluntary local round-budget yield must not be charged
   to the peer as a timeout. Driven deterministically with fake timers, the
   first ref resolving from cache and the ready-hook advancing the clock:
   pins `yieldedAtDeadline`, pre-deadline progress surviving, the abandoned
   tail counted rather than dropped, and `timedOutPhases: 0` so nothing here
   can reach `backoffWorthyFailure`.

3. Inline lifecycle seam. Deleting the SWM coverage forwarding in
   `runCatchupOverPeers` left every suite green. Two peers, not one: with a
   single peer `+=` is indistinguishable from `=` and last-writer-wins is
   indistinguishable from selected-whole. The larger short manifest must win
   whole, so a field-wise reduction synthesizing 200/250 fails the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two leftovers from replacing the fleet-wide max, both in code the same
change touched.

A test comment still named `highestResolvedCoverage` — a function that no
longer exists — and justified a fixture's wide per-pass margin as
load-bearing against a hazard the fix removed. The margin was a dodge
around a real defect; now that the gate sums each peer's own high-water it
is belt-and-braces, and the comment says so rather than pointing at a
deleted symbol.

The per-pass log line printed the new cross-peer sum under the old
"coverage" wording, directly beside `describeCoverage`, which renders ONE
whole record from ONE peer — and whose own doc warns that counts printed
without their peer invite being read as a fleet total. That is the same
defect class as the `missingCount` mislabel this review round already
fixed: a number whose rendering overstates what it measures. Now labelled
"progress ... summed across peers".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/agent/test/sync-requester-progress.test.ts
…an emit

Review found `SwmSnapshotCoverage` fixtures annotated with the type but
omitting the required `materializationFailures`. Correct, and it is the
same class as the T16c fixture fixed earlier this round: a fixture that
does not represent producer output cannot catch the producer drifting away
from it.

Four base literals were short — `partialOfLarge`, `completeSmaller` and
`truncatedButLarger` in the agent suite, `r26` in the CLI suite. The three
spread-derived records inherit the field once the bases carry it. All four
already satisfied `resolved + missing === total`, so adding the counter
makes them emittable rather than merely well-typed.

Worth recording WHY the annotation did not catch this, because it defeats
the suggested remedy too: no tsconfig in this repo typechecks `.test.ts`
at all. Both package configs are `include: ["src"]`, and
`packages/agent/tsconfig.type-tests.json` covers only
`test/**/*.typecheck.ts`. Measured rather than inferred — appending
`const x: number = 'definitely not a number';` to this very file is caught
by neither config and the suite still runs 56/56 green.

So a typed fixture builder would enforce nothing either. The durable fix is
to typecheck test sources in CI, which is a repo-wide change with its own
baseline and belongs in its own PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/cli/src/context-graph-readiness.ts
Branimir Rakic and others added 2 commits August 5, 2026 01:38
…operator

Review flagged that the shortfall clause caps how MANY identifiers it names
but not what any one of them contains. Confirmed at the source: a ref is a
`dkg:publicSnapshotRef` literal chosen by a remote peer, and the parser
applies only `.trim()` before it reaches `missingSample` and then the
terminal error, which surfaces through the API and the node UI.

Two distinct exposures, so two bounds.

Renderer — `sanitizeSnapshotRef` folds every C0/C1 control character to
U+FFFD and bounds the rendered length with a visible marker. Folding rather
than deleting is deliberate: deleting would let a crafted "a\nb" collapse
into the real ref "ab" and impersonate it. This is the last point before an
operator sees the string, so it holds whichever producer path filled the
sample. Mapping after the slice keeps the "(+N more)" arithmetic on the
sample COUNT, which sanitising cannot change.

Producer — `boundSampledRef` caps each ref as it enters either sample.
Capping the sample size bounds how many refs are kept, not how large each
is; ten megabyte literals would still cross the worker RPC and sit in the
diagnostics record, which the renderer cannot prevent from the far side.
Oversized peer literals are a phenomenon this codebase has already met on
the sync path.

Two rows added, both mutation-tested: bypassing the sanitizer fails them on
assertions in 4ms and 1ms — one on a forged newline surviving into the
message, one on the overlong literal being rendered whole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ust lost

Found by an adversarial pass over this review round's own fixes, which were
unreviewed code.

47bb6a9 narrowed the plane-proven stop so an unrelated peer's clean round
could not end the walk while a capable peer still had refs outstanding, and
left the structurally identical ungated stop one clause below it.

At the pass-1 boundary `coverageHighWaterMark` is 0 by initialization, so
`lastPassCoverage <= coverageHighWaterMark` collapses to "the whole walk
materialized zero" — a state a CAPABLE peer routinely reports: a store
fault that failed every write, or a round whose deadline was spent by the
metadata and aggregate phases so the snapshot walk yielded at index 0. Such
a peer emits `{resolved: 0, total: 250, manifestComplete: true}`, which is
the peer stating it holds 250 refs we lack. It earned ZERO repeats while
the terminal message told the operator more passes would not help — and on
a warm cache those repeats would have cost no network bytes at all.

Suppressed only at that boundary, only while the capable set is non-empty.
This does not re-admit the barren-retry the 0-init guards against: a barren
peer emits no coverage record at all, and a truncated-manifest peer is
excluded by `capablePeersForNextPass`. A non-empty capable set on pass 1 is
a peer's own statement that it holds what we lack, not an absence of
failure. Later passes stall normally.

Mutation-tested at both levels — disabling the suppression fails the unit
row and the worker-loop row on assertions in 4ms each.

Note for whoever runs these next: the CLI worker loads the policy through
the agent's BUILT dist, so the worker row was a FALSE GREEN until
`node scripts/build.mjs` ran. Rebuild before trusting any CLI-side result
after an agent-side edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/cli/src/catchup-runner-worker-impl.ts Outdated
…ing forever

Second defect found by the adversarial pass over this round's own fixes.

`snapshotsTotal` counts refs in the peer's MANIFEST; `snapshotsResolved`
counts refs we MATERIALIZED. The two are read by different producers, and a
ref can exist for the first and be invisible to the second:
`collectPublicSnapshotMetadata` needs only a digest and a count, while
`parseGraphScopedSwmRecoveryDescriptors` visits only operation subjects a
head row names. Re-sharing a Knowledge Asset leaves the superseded
share-operation row behind while the head names the current operation — so
that ref stays in the manifest with no descriptor.

Such a ref could never enter `materializedRefs`, so
`snapshotsResolved < snapshotsTotal` held FOREVER. `capablePeersForNextPass`
kept calling the peer capable, and every future catch-up job spent its whole
pass budget re-walking a graph that was already complete — at O(KA size) per
cached ref, since a cache hit is a full blob read, a digest, a COUNT and a
CONSTRUCT. Left alone this would have been the more expensive of the two
defects, because it recurs per job rather than once.

The early return conflated two states. Missing WIRING means nothing CAN be
written and the ref stays unresolved; no DESCRIPTOR under a complete
manifest means there is nothing to write, which is resolved by vacuity. Now
split, and gated on `manifestComplete` — a truncated meta phase parses no
descriptors at all, so "no descriptor" there means "not known yet".

The regression row shares one KA twice and keeps only the superseded
operation rows, which is the production shape rather than a synthetic one;
both payloads are pre-cached so neither ref touches the transport. Reverting
the fix fails it on `snapshotsResolved 2 -> 1`, and only that row fails out
of 57 — so nothing previously covered this.

Known gap, stated rather than implied: when NO ref in a manifest has a
descriptor, `onSnapshotReady` is never wired and this path is not reached,
so that shape still reports zero resolved. It needs its own row and its own
fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/cli/src/catchup-runner-worker-impl.ts Outdated
… just the parser

Review noted that `DKG_SWM_CATCHUP_PASS_BUDGET_MS=0` is documented as the
operator kill switch but only the pure parser was tested. Verified: the only
test naming those env vars was the agent-side parser suite.

The gap is specific and the failure it hides is expensive. Both constants
resolve ONCE AT MODULE LOAD in catchup-pass-policy.ts and are then imported
by the worker, so a typo'd env name, a broken export, or the worker reading
a literal default would pass every existing test — and this is the lever an
operator reaches for during an incident, when nobody is in a position to
discover it does nothing.

Two files rather than one, because `max-passes-reached` is evaluated before
`budget-exhausted`: a single file holding both switches would mask the
budget row.

Each proves the switch behaviourally — one `syncSharedMemory` call and a
stop — not merely that a constant parsed. The anti-vacuity control is
explicit: the fixture is copied verbatim from the existing row that asserts
the SAME peer is contacted TWICE, so the environment variable is the only
difference between a fixture that continues and one that stops.

Mutation: renaming the env var the policy reads, then rebuilding, fails the
behavioural assertion in both files. Reproduced independently after the
delegated run.

Env is set via `vi.hoisted` with an `afterAll` restore, following the
existing `catchup-runner-worker-killswitch.test.ts` precedent, and the
restore was verified with a temporary leak probe — an unrestored env write
here would silently run later files under the kill switch and green them for
the wrong reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/cli/test/catchup-runner-worker-pass-budget-killswitch.test.ts Outdated
Comment thread packages/agent/src/sync/catchup-pass-policy.ts
The "known gap" recorded in e64f10a is not exotic. It is what the primary
shared-memory write path produces, and it is a third round of the same
denominator/numerator mismatch.

The manifest and the descriptors come from different readers.
`collectPublicSnapshotMetadata` accepts any subject carrying
`dkg:publicQuadsDigest` + `dkg:publicQuadsCount`;
`parseGraphScopedSwmRecoveryDescriptors` anchors only on `#dkg-swm-head`
subjects. An entity-level share writes its public slice under a
`urn:dkg:public-stage:...` subject with no head row, so a Context Graph
written entirely by that path yields manifest refs and NO descriptors at
all. The hook was gated on `snapshotDescriptorsByRef.size > 0`, which reads
as an optimization and meant `materializeReadySnapshot` never ran — so the
vacuity branch added in e64f10a was unreachable in precisely the shape it
was written for.

Such a graph reported `0/N` snapshots for ever. `capablePeersForNextPass`
nominated a peer that owed us nothing on every pass of every catch-up job.

7403331 made it worse rather than better: before it, pass 1 stopped
immediately at `coverage-stalled`. After it, that false capable signal is
taken as positive evidence and buys a full extra pass — complete metadata
and aggregate replay over the wire, plus an O(KA size) blob read and digest
per cached ref — to resolve zero again. The two fixes were sound
individually and wrong together, which is only visible with both applied.

Reproduced against the built dist before and after: `0/1 capable=true
continue` becomes `1/1 capable=false plane-proven`.

The hook still early-returns when the materializer or the store is absent,
so the distinction that matters is preserved and now load-bearing: missing
WIRING means nothing can be written and the ref stays unresolved; no
DESCRIPTOR under a complete manifest means there is nothing to write and it
is resolved. Under the mutation restoring the old guard, exactly one row
fails out of 58 — the two rows pinning the other side of that distinction
stay green, which is the positive proof it was not weakened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/agent/src/sync/catchup-pass-policy.ts Outdated
Branimir Rakic and others added 2 commits August 5, 2026 03:31
a04b01b wired the snapshot-ready hook unconditionally so the vacuity rule
could be reached. It also made the parse-failure path reachable by that rule,
and that combination reported a graph fully materialized while writing
nothing.

The parse runs INSIDE a block whose entry condition is
`wsMetaResult.completed`, so on the failure path `manifestComplete` is
guaranteed true exactly where the descriptor map is empty for the wrong
reason. Manifest complete AND descriptors never parsed is a third state,
distinct from a truncated meta phase and from a genuine entity-share
manifest with no head rows — and `manifestComplete` cannot express it.

Consequences, all silent: coverage read N/N with zero assertion graphs
written; `materializationFailures` stayed 0 because nothing was attempted,
so the phase read usable and the bulk `storeInsert(processed.verifiedMeta)`
landed head rows certifying assertion graphs that hold nothing — after which
`isGraphAssetMaterialized` skips those KAs for good, the same permanent
invisibility the G7 repair exists to prevent. The peer was then dropped as
satisfied, the loop stopped `plane-proven`, and the shortfall clause
rendered nothing at all.

Both triggers are production-reachable rather than synthetic: union-insert
head residue — the state this branch's own materializer names and repairs,
so any peer running the pre-fix code serves it — and a head written at a
newer `contentScopeVersion`. Because the parser builds its whole array
before returning, ONE malformed head discards the descriptors of every valid
Knowledge Asset alongside it.

`descriptorsAuthoritative` now gates both the vacuity branch and
`snapshotPhaseUsable`. Measured before and after on both shapes: `2/2` and
`3/3` with `failedPhases: 0` become `0/2` and `0/3` with `failedPhases: 1`.

Two mutations, each landing on its own assertion: dropping the flag from the
catch returns full coverage; dropping it from `snapshotPhaseUsable` returns
`failedPhases: 0`. Under both, exactly one row fails out of 59 — the three
rows pinning the other states stay green, so the four-way distinction is
complete rather than incidentally covered.

Known diagnostics residual, not fixed here: on this path `missingCount` is
non-zero while `missingSample` is empty, because the sample is populated
only from the materialization-failure branch and nothing was attempted. The
operator gets a count with no named refs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…napshots

Partial correction to e50e9e0, which over-reached.

With an empty manifest there is no snapshot the metadata could wrongly
certify, so `descriptorsAuthoritative` protects nothing there and only
discards the round's verified metadata. `recordSnapshotCoverage` also
returns early on an empty manifest, so no coverage record exists and the
shortfall clause renders nothing — leaving `unreachable` with no statement
of what is missing.

A Context Graph whose public slices are all graph-backed
(`dkg:publicSnapshotGraph`, written when the publisher has no snapshot
store) declares zero refs and hit exactly that. Measured: 0 of 8 verified
meta rows written with `failedPhases: 1` becomes 8 of 8 with
`failedPhases: 0`.

REMAINING AND NOT FIXED HERE — see the PR discussion. `descriptorsAuthoritative`
is still Context-Graph-wide while the decisions it gates are per-ref. An
entity-level share writes a `urn:dkg:public-stage:` slice that has NO head
row, so it can never have a descriptor even on a perfectly clean parse; one
unrelated malformed head therefore still withholds the whole graph's
verified metadata, including the `WorkspaceOperation` and `dkg:rootEntity`
rows that bind data which DID land. Measured: 3 entity shares alone give
`3/3 resolved, 21 meta triples, failedPhases 0`; the same 3 plus one
poisoned head give `0/4, 0 meta triples, failedPhases 1`, permanently,
because the parse is deterministic over the peer's served metadata.

Fixing that properly means deciding authority per REF, which needs the
source subject that `collectPublicSnapshotMetadata` computes and discards —
a change to the exported `PublicSnapshotMetadata` shape. Deliberately not
attempted in the same pass as the correction above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/cli/src/catchup-runner.ts
Comment thread packages/agent/src/dkg-agent-lifecycle.ts
Comment thread packages/agent/test/sync-requester-progress.test.ts
Two defects introduced by making the SWM walk repeatable. Neither is
visible in the terminal record - both live in the scalars the daemon
route branches on - and neither was covered.

1. deferredBackpressure is a JOB-LEVEL total, and accumulate() runs for
   continuation rounds too. routes/context-graph.ts short-circuits on
   `deferredBackpressure > 0 && !denied` BEFORE classification, and that
   else-branch is the only path to classifyContextGraphCatchupReadiness,
   the readiness write, markContextGraphSubscriptionState and
   PROJECT_SYNCED. Its premise - "an incomplete round has no readiness
   to inspect" - is true when the round was cut short and FALSE when
   pass 1 completed and only a best-effort extra pass was refused
   capacity.

   So a job that fully succeeded could be demoted to `deferred` and have
   its readiness discarded because an OPTIONAL pass hit local pressure -
   on exactly the workload #2050 targets, since a large public graph
   under store pressure is precisely when admission defers. The pass
   budget's own rationale anticipates this state ("sized so at least two
   extra passes fit even when a peer's plane is deferred"), so it is
   designed for, not an edge case.

   The diagnostic still counts every deferral; only the job-level scalar
   is gated, so observability is unchanged.

2. deniedPeers was a scalar `+= 1` per round, sitting four lines below
   the comment explaining why peersTried/peersResponded/peersSucceeded
   became Sets: once the walk repeats, a peer that denies on every pass
   is counted once per pass. The agent driver already models this as a
   set (accessDeniedPeers), so the two drivers disagreed on a field they
   both return - the drift the review predicted, already realized.

Verified by mutation, on disjoint rows, both assertion deaths:
  - neutralize the gate to `if (true)` -> continuation row dies
    "expected 1 to be +0"; the MANDATORY-pass control stays green.
  - revert deniedPeers to the counter -> denial row dies
    "expected 2 to be 1" in 9ms.
Each mutant kills exactly one row, and the mandatory-pass control is
what stops the first row passing under a gate on the wrong thing.

Test note: a locally deferred plane is RETRIED and only surfaces once
CATCHUP_BACKPRESSURE_MAX_WAIT_MS (180s) expires, so the rows stub
DKG_CATCHUP_BACKPRESSURE_MAX_WAIT_MS=0. Left at the default they die on
the vitest timeout, which would read as a failing assertion while
proving nothing.

Added to the vitest include allow-list; a file not listed is silently
uncollected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PC4iJT3UBkNC2iygHpVyd
@Jurij89

Jurij89 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Review round: both open threads addressed, plus two defects they surfaced

Both remaining 🟡 comments were valid. Both are deferred to tracked issues — but auditing them found two real defects, now fixed in be74b477b.

Fixed here (not raised by either comment)

1. A deferred continuation pass could demote an already-successful job. deferredBackpressure is a job-level total and accumulate runs for continuation rounds, while routes/context-graph.ts short-circuits on deferredBackpressure > 0 before classification — and that branch is the only path to classifyContextGraphCatchupReadiness, the readiness write, markContextGraphSubscriptionState and PROJECT_SYNCED.

The route's premise — "an incomplete round has no readiness to inspect" — is true when the round was cut short and false when pass 1 completed and only a best-effort extra pass was refused capacity. So the fix could stop a subscribe settling on exactly the workload it exists to fix: a large public graph under store pressure is precisely when admission defers. The pass budget's own rationale anticipates the state ("sized so at least two extra passes fit even when a peer's plane is deferred").

Only the job-level scalar is gated; the diagnostic still counts every deferral, so observability is unchanged.

2. deniedPeers counted peer-passes, not peers — the drift #2105's comment predicted, already realized. It was a scalar += 1 sitting four lines below the comment explaining why peersTried/peersResponded/peersSucceeded had become Sets, and the agent driver already modelled the same quantity as a Set. The two drivers disagreed on a field they both return.

Evidence

Both proved by mutation, on disjoint rows, both assertion deaths (not timeouts):

Mutation Result
gate → if (true) continuation row dies expected 1 to be +0; the mandatory-pass control stays green
deniedPeers → counter denial row dies expected 2 to be 1 (9 ms)

The mandatory-pass control is what stops the first row passing under a gate on the wrong thing.

Test note: a locally deferred plane is retried and only surfaces once CATCHUP_BACKPRESSURE_MAX_WAIT_MS (180 s) expires, so the rows stub it to 0. At the default they die on the vitest timeout — which would read as a failing assertion while proving nothing.

Deferred

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants