Skip to content

fix(agent): relieve repeated sync pressure - #2053

Merged
Jurij89 merged 16 commits into
testnet-canaryfrom
fix/2052-sync-pressure
Aug 4, 2026
Merged

fix(agent): relieve repeated sync pressure#2053
Jurij89 merged 16 commits into
testnet-canaryfrom
fix/2052-sync-pressure

Conversation

@Jurij89

@Jurij89 Jurij89 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Preserve configured numeric sync priorities while guaranteeing one bounded service turn for aged lower-priority work. The existing global scheduler remains work-conserving: no slot is reserved, active work is not preempted, and foreground catch-up can no longer be displaced indefinitely by reconnect/reconcile churn.
  • Carry a private found | clean-absent | incomplete disposition through the existing exact-request single-flight, rotate each missing VM slot/fingerprint across a bounded canonical peer roster, and enter exponential backoff only after every current candidate independently proves clean absence. Suppressed passes perform no discovery, dial, protocol probe, or decision-only store/chain work.
  • Bound exact-response accumulation before decode/materialization, page oversized curator peer registries, and cap/process-clean all new recovery, binding, negative-cache, and membership-persistence state.
  • Harden graph-scoped sync across unsubscribe, rebind, restart, and shutdown: capture binding generations before authentication, reject conflicting same-name chain slots, serialize strict subscription persistence, keep materialization atomic, and physically drain admitted authentication/persistence/materialization before backing stores close.
  • Preserve fail-closed chain authentication and legacy/system-graph repair. This PR relieves the measured pressure but intentionally does not claim complete resolution of Reconnect-triggered background sync amplification can starve foreground catch-up #2052's unchanged legacy replay.

Related

Diagrams

Global sync admission

Before:

sequenceDiagram
    participant Foreground
    participant Queue
    participant Background
    participant Worker
    Background->>Queue: Enqueue older reconnect/reconcile work
    Foreground->>Queue: Enqueue priority 2000 catch-up
    Worker-->>Queue: Release a global slot
    Queue->>Worker: Start aged background by effective priority
    Queue-->>Foreground: Remain queued or be displaced
Loading

After:

sequenceDiagram
    participant Foreground
    participant Queue
    participant Background
    participant Worker
    Background->>Queue: Enqueue lower-priority reconnect/reconcile work
    Foreground->>Queue: Enqueue priority 2000 catch-up
    Worker-->>Queue: Release a global slot
    Queue->>Worker: Start highest raw priority when no debt is owed
    Queue-->>Foreground: Foreground starts at the bounded release
    Worker-->>Queue: Release the next slot
    Queue->>Worker: Serve one retained aged debt without idling capacity
Loading

Missing VM asset recovery

Before:

sequenceDiagram
    participant Sweep
    participant Recovery
    participant Roster
    participant Peer
    participant ChainStore
    Sweep->>ChainStore: Reconcile missing ordinal
    Sweep->>Recovery: Recover missing asset
    Recovery->>Roster: Resolve and prepare peers each sweep
    Recovery->>Peer: Exact fetch
    Peer-->>Recovery: Empty or incomplete aggregate result
    Recovery->>ChainStore: Reconcile again
    Sweep->>Recovery: Retry without slot-specific absence proof
Loading

After:

sequenceDiagram
    participant Sweep
    participant Recovery
    participant Roster
    participant Peer
    participant ChainStore
    Sweep->>ChainStore: Reconcile missing ordinal and fingerprint
    Sweep->>Recovery: Check bounded slot record
    alt Backoff proof is current
        Recovery-->>Sweep: Suppress before roster, dial, or admission work
    else Fresh candidate evidence required
        Recovery->>Roster: Resolve bounded canonical candidates
        Recovery->>Peer: Fresh exact fetch from an uncredited peer
        Peer-->>Recovery: Found, clean-absent, or incomplete
        Recovery->>ChainStore: Authenticate and reconcile current binding
        Recovery-->>Sweep: Back off only after a complete clean-absence rotation
    end
Loading

Graph-scoped shutdown ownership

Before:

sequenceDiagram
    participant DurableSync
    participant Agent
    participant Chain
    participant Store
    participant Daemon
    DurableSync->>Agent: Store verified graph-scoped asset
    Agent->>Chain: Authenticate asset and binding
    Daemon->>Agent: Stop node
    Agent-->>Daemon: VM reconcile work retired
    Daemon->>Store: Close backing stores
    Chain-->>Agent: Authentication completes late
    Agent->>Store: Persist binding or materialize after teardown
Loading

After:

sequenceDiagram
    participant DurableSync
    participant Agent
    participant Chain
    participant Store
    participant Daemon
    DurableSync->>Agent: Store verified graph-scoped asset
    Agent->>Agent: Register deferred physical store promise
    Agent->>Chain: Authenticate captured binding generation
    Daemon->>Agent: Stop node and close admission
    Agent->>Agent: Drain all admitted graph-scoped work
    Chain-->>Agent: Authentication completes
    Agent->>Store: Strictly persist and atomically materialize if current
    Store-->>Agent: Physical work settles
    Agent-->>Daemon: Retirement complete or typed quarantine
    Daemon->>Store: Close only after retirement
Loading

Files changed

File What
packages/agent/src/sync/priority-admission-queue.ts, packages/agent/src/sync/backpressure.ts Preserve raw priority, add one bounded aged-service debt, protect an aged displacement recipient, and keep queue timing monotonic/work-conserving.
packages/agent/src/sync/requester/*, packages/agent/src/sync/exact-assets.ts Derive exact-fetch dispositions from fresh complete phase evidence, share physical results across coalesced callers, and cap cumulative accepted bytes/quads.
packages/agent/src/dkg-agent-swm-host.ts, packages/agent/src/vm-reconcile-service.ts Add bounded slot/fingerprint peer rotations, proof expiry/backoff, currentness guards, paged curator walking, and lifecycle cleanup.
packages/agent/src/dkg-agent*.ts, packages/agent/src/context-graph-*.ts Fence subscription/binding persistence, bound membership writes, capture binding generations, and physically own graph-scoped work through shutdown.
packages/agent/src/discovery.ts, packages/agent/src/p2p/* Add abortable duplicate-free curator peer paging and lifecycle-aware peer preparation.
packages/publisher/src/metadata.ts Hold the per-KA materialization lock through stale-binding validation and compensating quarantine.
packages/cli/src/daemon/lifecycle.ts, packages/cli/src/daemon/teardown.ts Keep backing stores alive while typed sync retirement quarantine is retried or forces a nonzero hard-timeout exit.
packages/agent/test/*, packages/publisher/test/*, packages/cli/test/* Cover fairness, exact evidence, bounded state, discovery, binding races, atomic materialization, restart, and shutdown fault paths.

Test plan

  • pnpm --filter @origintrail-official/dkg-agent exec tsc --noEmit
  • pnpm --filter @origintrail-official/dkg exec tsc --noEmit
  • pnpm --filter @origintrail-official/dkg-publisher exec tsc --noEmit
  • 18-file final sync/reconcile matrix: 475 tests passed
  • Exact-requester matrix: 79 tests passed
  • CLI teardown/quarantine matrix: 36 tests passed
  • Publisher materialization-lock matrix: 8 tests passed
  • Detached final-head workspace build at 4bd402de89af4b73a200cf120796c3617474edbe
  • Three independent final adversarial reviews: protocol, fault injection, and resource/liveness; no blockers remain
  • git diff --check and clean final candidate worktree
  • Matched Base Sepolia fixture-A baseline/candidate run from the same immutable receiver snapshot: candidate materialized 57 versus 19 VM asset graphs, reduced maximum active age by 51.9%, reduced second-half saturation by 21.2%, and ended idle. Result is intentionally inconclusive for a regression-free claim because the curator was unreachable, no clean-absence rotation formed, final work was unequal, and resource/API ceilings need controlled retest.
  • Post-merge Blackbox conductor run on testnet-canary: require full VM convergence, clean-absence/backoff suppression, curator-arrival freshness, foreground fairness, and normalized API/Oxigraph resource ceilings before promotion beyond canary.

// Use the exact same memory-only canonicalizer as the suppression gate.
// Curator resolution may connect a missing peer, but it must not substitute
// a different ranking algorithm and invalidate an otherwise stable roster.
const orderedPeerIds = this.vmReconcileObservedCandidatePeerIds(localCgId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Resolved curator peers can be dropped from the exact-recovery proof roster

What's wrong
The code still resolves and connects curator peers, but the added canonical roster ignores those resolved IDs unless one also happens to be the single preferred peer or a known core. Because the roster is capped before the exact fetch loop, an authoritative curator can be excluded from both fetching and clean-absence evidence. That can cause the node to back off a missing ordinal after asking unrelated peers instead of asking the curator most likely to have the asset.

Example
If a wallet-scoped CG resolves registry curator peerD, preferredSyncPeers is empty, no peers are known cores, and connected peers sort as [peerA, peerB, peerC, peerD], the new roster is capped to [peerA, peerB, peerC]. Exact recovery can collect clean absence from those three and enter backoff even though the resolved curator peerD was just dialed and may have the missing KA.

Suggested direction
Carry curatorPeerIds into the roster used for orderedPeerIds or otherwise rank resolved curators ahead of generic connected peers before applying the three-peer cap, while keeping the intended stable rotation behavior.

For Agents
In recoverVmReconcileBatch, preserve the resolved curator peer IDs when building the capped exact-recovery roster. Add a scenario where the registry curator is not preferredSyncPeers, not a known core, and would be outside the first three canonical connected peers; the test should prove exact recovery attempts that curator before clean-absence backoff can be recorded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3151cd5. Exact recovery now caches the resolved structural curator IDs per CG and ranks connected cached curators ahead of ordinary peers before applying the three-peer cap. The pre-network suppression pass reuses that memory-only roster on later sweeps, so it adds no discovery/chain/dial work. Added a regression with three ordinary peers and a fourth non-preferred/non-core curator that alone returns the asset; the first physical request now goes to the curator.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Exact-recovery backoff is lost when peers disconnect before the next sweep

What's wrong
The new rotation state is meant to damp repeated exact recovery after every candidate has cleanly reported absence. However, the pre-network suppression pass uses only currently connected peers. When that list is empty, it deletes the stored rotation record, including completed backoff evidence. That makes backoff depend on sockets staying open and can re-trigger network recovery every sweep after ordinary disconnects.

Example
A slot has already completed a clean-absence rotation and is in backoff until nextRetryAt. If all peers disconnect before the next sweep, observedCandidatePeerIds is []; line 3655 deletes the backoff record, the method proceeds to resolve/dial the curator, installs a fresh collecting record, and sends another exact fetch immediately instead of suppressing until nextRetryAt.

Suggested direction
Do not treat an empty transient connection view as an authoritative roster change for an existing backoff record. Keep the record and suppress while now < nextRetryAt, unless the target fingerprint changes or a non-empty authoritative roster invalidates it.

For Agents
In dkg-agent-swm-host.ts, preserve active backoff records when the observed pre-dial roster is empty, or move suppression to a point where cached/resolved curator identity can be considered. Add a test where a clean-absence backoff exists, getConnections() is empty before curator dialing, and recovery remains suppressed until nextRetryAt.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Backoff can suppress the curator lookup that would make recovery succeed

What's wrong
A clean-absence backoff gathered while curator discovery was unavailable can be replayed before the next curator lookup. That makes ordinary peers' empty responses temporarily block the code from discovering and querying the actual curator once discovery recovers, delaying VM repair unnecessarily.

Example
For a structural CG with no cached curator peers: pass 1 has resolveCuratorPeerIdsForCg return lookupFailed: true, two ordinary connected peers return fresh empty exact responses, and the slot enters backoff. On pass 2 the registry is healthy and would resolve the curator holding the KA, but initiallyEligible.length === 0 returns before curator resolution, so recovery waits for nextRetryAt instead of trying the now-authoritative curator.

Suggested direction
Only honor exact-absence backoff before curator resolution when the stored candidate roster is known to include the authoritative/cached curator set, or defer suppression until after a fresh curator lookup when the previous cycle was built during lookup failure.

Confidence note
This depends on the intended authority of clean-absence evidence from non-curator peers, but the changed code explicitly treats failed curator lookup as non-authoritative elsewhere, so the backoff should not prevent the next curator lookup.

For Agents
In recoverVmReconcileBatch, distinguish rotation records collected without an authoritative/cached curator roster from records collected against the authoritative roster. Preserve the existing shutdown/stale-target guards, but keep failed-lookup/no-cache cycles fail-open or force curator resolution before honoring their suppression. Add a test where first curator discovery fails with only ordinary clean-absent peers, then the next pass resolves a curator and fetches immediately despite the prior ordinary absence.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Disconnected cached curators can make ordinary-peer absence look authoritative

What's wrong
The backoff proof can be built from ordinary connected peers while being marked as curator-confirmed solely because a stale cached curator list exists. That can delay or block VM recovery from the actual curator after a transient dial/discovery failure.

Example
Cached curator peer C exists from a prior successful lookup, but ensurePeerConnected(C) fails during this sweep. Ordinary peer A is connected, so orderedPeerIds is [A]; line 4278 still marks the roster confirmed because the cache is non-empty. If A returns clean-absent, the slot enters backoff, and the next sweep skips before retrying discovery or dialing C even if C is now available.

Suggested direction
Compute confirmation from the candidate roster used for proof collection. If curator discovery failed and no cached curator peer is present in the connected candidate set, keep the rotation unconfirmed so clean-absence evidence cannot suppress the next discovery attempt.

For Agents
In recoverVmReconcileBatch, make curatorRosterConfirmed depend on the actual proof roster, not just the existence of cached curator ids. Preserve suppression only when discovery succeeded or at least one cached authoritative curator is included in orderedPeerIds; add a test where cached curator connection fails, an ordinary peer returns clean-absent, and the next pass still retries curator discovery/dialing.

Comment thread packages/agent/src/dkg-agent-swm-host.ts
};
}

function classifyExactDurableFetch(params: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Decompose exact-fetch classification instead of pushing durable-sync.ts past 1k lines

What's wrong
The new proof-specific classification logic is added directly to an already oversized orchestration file, crossing the 1000-line threshold without a strong structural reason. It also leaves identical merge semantics duplicated in lifecycle code, which makes this harder to maintain than a single canonical exact-disposition boundary.

Example
The durable sync requester now owns exact payload filtering, exact disposition classification, disposition merging, fetch orchestration, verification, checkpointing, and storage in one file. A reader changing clean-absence semantics has to edit this file and separately remember the lifecycle-level merge helper.

Suggested direction
Create a small exact-durable-disposition helper module for filterExactAssetDurablePayload, classifyExactDurableFetch, and mergeExactDurableFetchDisposition. Keep the main durable sync loop focused on orchestration and have lifecycle reuse the same exported merge function.

For Agents
Extract exact asset filtering/classification/merge helpers from sync/requester/durable-sync.ts into a focused module, then import the shared merge helper in dkg-agent-lifecycle.ts. Preserve runDurableSync returning only the public result and runDurableSyncDetailed returning the optional disposition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3151cd5. Added sync/requester/exact-durable-fetch.ts as the canonical home for filtering, classification, disposition typing, and merge semantics. durable-sync.ts imports/re-exports the compatibility surface, and lifecycle now imports the same merge helper instead of duplicating it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Fold exact-fetch disposition into the durable-sync accumulator

What's wrong
The implementation bolts exact-fetch disposition onto durable sync as a separate mutable side channel instead of extending the existing aggregation model. That increases orchestration complexity and makes the detailed result depend on scattered updates in every branch that can complete, defer, skip, abort, or fail a context graph.

Example
The durable-sync path now has to remember both markDurableTerminalBoundary(accumulator, ...) and exactFetchDispositions[...] = settledExactDisposition(); lifecycle-level deferral/cancellation paths similarly have to call markExactFetchIncomplete(). Any future early return or new mark path has two summaries to keep in sync.

Suggested direction
Add exact disposition to the accumulator shape, or introduce a small DetailedDurableSyncAccumulator with a single merge function. Then runOrderedContextGraphSyncs and runDurableSyncWithBudget can merge one result object instead of maintaining a second mutable side channel.

For Agents
Look at runDurableSyncWithBudget, runLegacyDurableSyncDetailed, and the durable accumulator helpers. Preserve the public runDurableSync projection and exact coalescing behavior, but move exact disposition into the same accumulator/merge model used for durable sync completion. Existing multi-CG exact disposition tests should still pass.

private readonly hooks: PriorityAdmissionQueueHooks<Payload>;
private readonly now: () => number;
private nextSequence = 0;
private agedTurnOwed = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Make fairness debt an explicit scheduler policy instead of scattered queue state

What's wrong
The new single boolean agedTurnOwed is a hidden state machine whose transitions are split between selection, pumping, acquisition, removal, timeout, and abort paths. That makes the scheduler harder to reason about and increases the chance that the next queue feature bypasses one of the debt invariants.

Example
To understand why a low-priority aged entry starts next, a reader has to combine selectNext() producing createsDebt/servesDebt, pump() mutating agedTurnOwed after start(), and queueChanged()/reconcileAgedTurnOwed() clearing the flag on cancellation, timeout, or removal.

Suggested direction
Collapse the aging/debt rule into a pure selector or small policy object, for example selectNextAdmission(queue, now, debt) returning the selected index plus the next debt state. That would keep debt creation, debt service, and stale-debt clearing in one place instead of distributing the invariant across queue operations.

Confidence note
The current behavior may be well covered, but the concern is structural: the new debt rule is spread across state, selection, pump, and queue mutation rather than isolated as one policy.

For Agents
Look at PriorityAdmissionQueue selection and debt handling. Preserve numeric priority, one-overtake debt, cancellation/timeout cleanup, handoff behavior, and start-failure behavior; the existing scheduler tests should continue to prove those cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated as a structural concern. The current change keeps the debt state local to PriorityAdmissionQueue and now also makes start failure rollback explicit and tested, but I am deferring a pure-selector extraction to a dedicated no-behavior-change follow-up. Combining that refactor with the scheduler semantic change would make this liveness PR harder to audit without changing its runtime result.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Make fairness debt an explicit scheduling policy instead of hidden queue side state

What's wrong
The new fairness behavior is implemented as a mutable side-channel flag that several queue operations must remember to maintain. This works against the queue’s already subtle admission lifecycle and makes future changes to cancellation, timeout, displacement, or start failure harder to reason about.

Example
Adding another path that removes or rejects a queued entry would have to know to call the right reconciliation hook, or the queue can retain/clear fairness debt inconsistently. That coupling is not visible from the remove/reject operation itself.

Suggested direction
Move the aged-debt transition logic into a small pure helper that takes queued entries and current debt and returns the selected entry plus next debt state. That keeps queue mechanics, capacity side effects, and fairness policy from being interleaved.

Confidence note
This is a maintainability concern from the changed structure; the existing tests may cover the current behavior, but the debt model remains implicit in queue internals.

For Agents
Look at packages/agent/src/sync/priority-admission-queue.ts. Preserve the one-overtake fairness behavior, but extract selection/debt transitions into a pure scheduler policy or represent debt as an explicit owed aged entry/sequence. Tests in sync-backpressure.test.ts should continue proving overtakes, cancellation, timeout, handoff, and start-failure behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Fairness debt is modeled as an implicit queue-wide flag

What's wrong
The scheduler now has hidden cross-call state that is only valid if every queue mutation path remembers to call the right reconciliation hook. That makes the admission queue harder to reason about and raises the cost of future changes to displacement, handoff, timeout, or start-failure behavior.

Example
When a high-priority entry overtakes an aged low-priority entry, agedTurnOwed becomes true. Later cancellation, timeout, start failure, or temporary unrunnability relies on separate calls to reconcileAgedTurnOwed() from multiple paths to keep that global bit meaningful.

Suggested direction
Make the fairness debt an explicit scheduling concept instead of a bare boolean updated opportunistically. A small policy object or pure selector that owns debt creation, repayment, and invalidation would reduce the number of places that must remember to reconcile this state.

Confidence note
This is a design-risk finding from the diff structure. The current behavior may be intentional, but the maintainability issue is that a single boolean encodes fairness debt without naming the owed entry or policy state explicitly.

For Agents
Look in packages/agent/src/sync/priority-admission-queue.ts around agedTurnOwed, selectNext, reconcileAgedTurnOwed, remove, and queued-start failure handling. Preserve the current scheduling semantics, but refactor the fairness state into an explicit scheduler policy/helper that returns {entry, decisionEffects} or tracks the owed aged sequence/owner rather than a bare queue-wide boolean. Prove with existing fairness tests plus cancellation, timeout, start-failure, and handoff tests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Model scheduler fairness debt explicitly instead of as a bare boolean

What's wrong
The new fairness behavior is subtle, but the state model is too lossy. A single boolean hides which queue entry earned the debt and why, so the scheduler now depends on scattered reconciliation calls to keep the flag meaningful.

Example
If one aged low-priority entry is overtaken, then cancelled, displaced, or becomes unrunnable, the boolean debt can later be consumed by a different aged entry. That may be intended, but the code does not encode that policy explicitly, so the invariant is hard to reason about from the type/model.

Suggested direction
Use a small typed state object for the fairness debt or fold the rule into one comparator that can explain which entry is protected and why. That would make cancellation, timeout, displacement, handoff, and temporary unrunnability easier to verify without relying on incidental boolean resets.

Confidence note
This is a maintainability concern about the scheduler model, not a claim that the current tests fail.

For Agents
Replace agedTurnOwed: boolean with an explicit debt model, such as a sequence/owner/priority snapshot or a documented overtake counter, and make selectNext consume that model directly. Preserve the intended one-overtake behavior and queue/displacement semantics with the existing scheduler tests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Give priority-aging debt a single owner instead of a hidden queue-wide flag

What's wrong
The queue now has an implicit second state machine layered on top of the queued entries. Because agedTurnOwed is a boolean with cleanup calls scattered through the class, the invariant is not locally obvious and future queue changes will have to remember to reconcile it in every path that can remove or start an entry.

Example
When a high-priority entry overtakes an aged one, selectNext() returns createsDebt, pump() flips agedTurnOwed, later queue removal or timeout calls queueChanged() to reconcile it, and the next selectNext() may serve the oldest aged entry. The policy is spread across selection, mutation, and cleanup instead of being owned by one scheduler model.

Suggested direction
Represent fairness debt explicitly, ideally as part of a scheduler policy with selectNext and afterStart/afterRemove transitions, or track the owed aged sequence rather than a bare boolean. That would let selection and debt cleanup live in one place.

Confidence note
This is a maintainability finding, not a claim that the current fairness behavior is wrong.

For Agents
In packages/agent/src/sync/priority-admission-queue.ts, pull the aging/fairness policy into a small explicit scheduler state object or pure selection helper. Preserve raw-priority ordering, one-overtake debt, cancellation/timeout cleanup, and displacement protection. Existing sync-backpressure tests should continue proving the same ordering.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Make priority aging debt an explicit scheduler policy instead of a hidden flag

What's wrong
The shared scheduler now has implicit temporal state whose lifecycle is scattered across queue admission, pumping, timeout/cancel cleanup, and victim selection. That makes the generic queue harder to evolve because fairness behavior is no longer localized to a single selection policy.

Example
To understand one scheduling decision, a reader has to simulate selectNext() plus the previous pump() result plus whether remove() or a timeout cleared the debt. The debt is not represented as a typed scheduling decision or tied to the entry/sequence that created it.

Suggested direction
Replace the bare boolean with a typed fairness state and centralize all updates around the scheduling decision. A pure selectNext(queue, capacity, fairnessState) style helper would make the generic queue much easier to maintain.

For Agents
Refactor PriorityAdmissionQueue so fairness policy is explicit and centralized. Preserve the new one-overtake behavior, but make selection a small policy object or pure function that returns both the selected entry and the next fairness state; then mutate queue state in one place after successful start.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Model scheduler fairness as an explicit policy, not scattered mutable debt

What's wrong
The new fairness behavior is implemented as a hidden queue-wide boolean that is reconciled opportunistically from acquire, selectNext, remove, and pump. This is exactly the kind of stateful special-case branching that makes shared scheduling code fragile: the invariant is not represented as a clear model, it is maintained by scattered calls in several control paths.

Example
To know why the next item starts, a reader has to inspect reconcileAgedTurnOwed(), then selectNext(), then the post-start mutation in pump(). A direct selector like selectNext({ debt }) -> { entry, nextDebt, reason } would make the transition explicit and testable without hidden side effects.

Suggested direction
Collapse aged-turn handling into a dedicated scheduler policy object or pure selector result. The queue should own mechanics; the policy should own priority-vs-aging decisions. That would make the fairness invariant legible and reduce the chance that future queue operations forget to reconcile the hidden debt bit.

For Agents
Refactor PriorityAdmissionQueue so admission selection is a small pure policy function that receives queued entries, current time, and current fairness state, then returns the selected index plus next fairness state. Keep the same scheduling behavior and metrics outcomes, but remove selectNext/reconcileAgedTurnOwed side effects from multiple call sites.

);
}

async syncExactKnowledgeAssetsFromPeerDetailed(this: DKGAgent,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Collapse the duplicated public/detailed exact-sync wrappers

What's wrong
The new detailed API adds a second wrapper that manually mirrors the existing exact-sync setup. This is thin indirection with drift risk: the two methods express the same operation but now have to stay synchronized by convention.

Example
Both wrappers call requireExactAssetUals, create a sync operation context, pass [contextGraphId], set exactAssetUals, stopOnBackoffWorthyFailure: true, priority: 1_000, and source: 'vm-recovery'. A future timeout/source/priority change has to be mirrored in both methods.

Suggested direction
Make the detailed exact-sync path the single implementation and let the public method be a projection over it. That removes the duplicated option assembly while keeping the new disposition available to VM recovery.

For Agents
In dkg-agent-lifecycle.ts, introduce one private/shared exact-sync helper that returns the detailed result. Have syncExactKnowledgeAssetsFromPeer project .result and syncExactKnowledgeAssetsFromPeerDetailed return the detailed shape. Preserve single-flight coalescing and the public method's result shape.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3151cd5. syncExactKnowledgeAssetsFromPeerDetailed is now the single option-assembly and physical implementation, while the public method only projects .result. Reverse-order public+detailed callers also share the canonical exact-set single-flight identity.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Collapse the parallel detailed durable-sync lifecycle APIs

What's wrong
The PR threads one extra exact-fetch disposition by duplicating multiple lifecycle methods. That adds indirection and synchronization burden without deleting complexity; it also makes exact-recovery details leak through general durable-sync orchestration layers.

Example
A future change to durable sync options now has to stay aligned across runLegacyDurableSync, runLegacyDurableSyncDetailed, runLegacyDurableSyncForContextGraph, runLegacyDurableSyncForContextGraphDetailed, and the exact-sync pair, even though the public/detailed difference is just whether the exact disposition is exposed.

Suggested direction
Use one canonical internal durable-sync execution path and one small projection wrapper for legacy callers. Avoid maintaining parallel lifecycle methods whose signatures and option plumbing must remain identical.

Confidence note
This is a maintainability finding; the current behavior may be intentional, but the new surface area is noticeably larger than the extra datum being returned.

For Agents
Review packages/agent/src/dkg-agent-lifecycle.ts around the durable sync wrappers. Preserve public return types and single-flight behavior, but make the detailed physical run the single internal path. Then expose projection only at the public boundary, or report exact disposition through a narrow exact-recovery observer instead of cloning lifecycle methods.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Avoid mirroring the durable-sync API just to carry exact-fetch disposition

What's wrong
The PR adds a second, nearly identical durable-sync call chain whose main purpose is to expose one extra VM-recovery datum. That doubles the lifecycle surface area readers have to follow and creates long-term drift risk: every future durable-sync change now has to be threaded through both normal and detailed variants.

Example
Adding any future durable-sync option or callback now requires keeping the public method, detailed method, per-context public method, per-context detailed method, requester runDurableSync, and requester runDurableSyncDetailed in sync, even though the new disposition is only meaningful for exact-asset recovery.

Suggested direction
Prefer one canonical internal return shape with a projection at the public boundary, or keep the detailed return scoped to the exact-asset helper instead of creating parallel Detailed variants through the whole legacy durable-sync stack.

For Agents
Look in dkg-agent-lifecycle.ts and sync/requester/durable-sync.ts. Preserve the public result shape, but collapse to one canonical internal durable-sync result or isolate the exact-asset path behind a single runExactKnowledgeAssetSync helper that returns { result, disposition }. Add/adjust tests that prove public callers still receive only DurableSyncResult and exact recovery still receives disposition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Fold exact disposition into the durable-sync accumulator instead of a closure side channel

What's wrong
The detailed exact-fetch outcome is now bolted onto the legacy durable orchestration as mutable outer state. That preserves behavior, but structurally it splits one operation summary across two aggregation mechanisms, which makes future changes to durable-sync result shape or exact-response classification easy to wire inconsistently.

Example
A deferred exact sync updates the durable summary through markDeferred(summary) and updates exact disposition through the captured markExactFetchIncomplete() closure. Those two summaries travel through different control paths even though they describe the same physical run.

Suggested direction
Make the detailed result the value that runOrderedContextGraphSyncs merges. A small createDetailedDurableSyncAccumulator/mergeDetailedDurableSyncAccumulatorInto would keep exact disposition, terminal-boundary marking, skipped/deferred handling, and final projection in one model.

For Agents
In dkg-agent-lifecycle.ts, introduce a composite detailed accumulator or extend the ordered sync accumulator so result and exactFetchDisposition are merged together. Preserve public runLegacyDurableSync as a projection of .result, preserve single-flight behavior, and cover multi-CG exact aggregation/deferred/skipped cases with the existing lifecycle tests.

if (!isTargetCurrent() || targets.length === 0) return noRecovery();

const expectedOnChainCgId = onChainCgId.toString();
const currentTargets = targets.filter((target) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Stale recovery-target filtering lacks a regression test

What's wrong
The PR adds localCgId and onChainCgId to recovery targets and uses them to discard targets that do not belong to the current batch. That is a safety boundary for exact VM recovery, but the tests only exercise correctly matched targets, so a regression in this filtering would not be caught.

Example
A regression test could call recoverVmReconcileBatch(currentCg, 1n, [targetFromOtherCgOrOnChainId], ...) with syncExactKnowledgeAssetsFromPeerDetailed throwing if called, then assert no fetch, no attempted ordinals, and no rotation state for the stale target. A mixed batch should assert only the matching target is fetched.

Suggested direction
Cover the new target identity contract at the batch boundary so a future change cannot accidentally fetch or suppress recovery for the wrong Context Graph slot.

Confidence note
The added recovery tests all build matching targets through vmRecoveryTarget; I did not find a case that passes stale or mixed-CG targets into recoverVmReconcileBatch.

For Agents
Add a focused test in packages/agent/test/core-fills-gap.test.ts around recoverVmReconcileBatch: pass mismatched localCgId/onChainCgId recovery targets and prove they are ignored without creating rotation state or consuming exact-fetch budget; also cover a mixed stale/current batch if continuation behavior matters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3151cd5. Added a batch-boundary regression with both a mismatched local CG target and a mismatched on-chain CG target. It proves no exact fetch, no attempted ordinal, and no rotation state is created for stale work.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Empty-roster reset lacks a regression test

What's wrong
This branch is meant to prevent partial clean-absence evidence from surviving across a period where no candidate peers are observed. That is a meaningful suppression/backoff safety property, but the tests added in this PR keep at least one candidate present and would not fail if this reset were removed.

Example
Failing-test sketch: create a rotation record for [peerA, peerB], credit peerA as clean-absent, call prepareVmReconcileRotationTarget(target, [], now + 1), then call prepareVmReconcileRotationTarget(target, [peerA, peerB], now + 2) and assert the returned record has empty attemptedPeerIds and cleanAbsentPeerIds.

Suggested direction
Cover the empty-candidate branch directly, including the subsequent reconnect behavior, so stale absence evidence cannot survive a full peer-disconnect interval unnoticed.

For Agents
Add a focused test in packages/agent/test/core-fills-gap.test.ts near the rotation identity tests. It should prove that an empty observed candidate roster deletes partial absence evidence and that a later reconnect starts a fresh collection cycle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Terminal reconcile cleanup is not directly covered

What's wrong
The PR adds slot cleanup to direct terminal reconciliation paths, but the visible coverage mostly exercises cleanup through recoverVmReconcileBatch, which independently deletes the rotation slot after any non-pending outcome. That means a regression removing these reconcileChainOrdinal cleanup calls could pass while leaving stale process-local suppression state in inline reconciliation paths.

Example
Seed vmReconcileRotationState for a target slot, make reconcileChainOrdinal hit the recentReconciledUals fast path or return already-confirmed from handleChainReconciledKC, and assert that the slot is removed. Without the new clear call, the state would remain installed.

Suggested direction
Add direct regression coverage for reconcileChainOrdinal terminal paths that proves stale rotation/backoff evidence is cleared outside recoverVmReconcileBatch.

Confidence note
The batch recovery tests assert cleanup after terminal outcomes, but recoverVmReconcileBatch also deletes the slot itself, so they do not isolate these newly added reconcileChainOrdinal cleanup calls.

For Agents
Add focused reconcileChainOrdinal tests in packages/agent/test/core-fills-gap.test.ts that pre-populate a rotation record for the target slot and verify direct terminal paths clear it: recent cache hit, promoted, already-confirmed, and stale-target.

| { status: 'skip' };

export interface OrdinalRecoveryTarget {
localCgId: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Do not leak exact-recovery rotation identity into the pure chain reconciler target

What's wrong
The PR widens the generic reconciler contract with agent-local identity data. That couples the pure watermark engine to one recovery implementation and makes future callers/tests pay for fields they do not conceptually own.

Example
The tests now need a vmRecoveryTarget helper that fabricates localCgId, onChainCgId, and merkleRoot for every target even when the scenario only cares about ordinal scheduling.

Suggested direction
Keep OrdinalRecoveryTarget minimal or make the recovery payload owned by the VM recovery layer. The pure reconciler should not carry fields whose only purpose is process-local rotation/backoff bookkeeping.

For Agents
Keep chain-reconciler.ts focused on ordinal reconciliation. Move exact-recovery identity into an agent-local target type or construct a VmExactRecoveryTarget at the recoverVmReconcileBatch boundary from the batch context plus the pending outcome. Preserve the existing recovery callback behavior, and verify the chain reconciler tests no longer need VM rotation-specific fields.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Avoid duplicating batch identity on every recovery target

What's wrong
The new target shape creates two sources of truth for the context graph identity: the method parameters and the per-target fields. That weakens the type boundary and pushes stale-target defense into incidental control flow rather than making the invariant clear at construction time.

Example
recoverVmReconcileBatch('current', 1n, [{ localCgId: 'other', onChainCgId: '1', ... }], ...) is now a representable state and is silently filtered out. The type permits contradictory batch identity, so the implementation has to defend against that contradiction in multiple places.

Suggested direction
Replace the duplicated fields with a single explicit batch/slot model so contradictory target identity is impossible or isolated at one boundary. That should remove the ad-hoc filtering and reduce repeated equality checks in the recovery loop.

For Agents
Start with OrdinalRecoveryTarget in packages/agent/src/chain-reconciler.ts and its construction in reconcileChainOrdinal. Preserve stale in-flight response protection, but model graph identity once: either pass a typed RecoveryBatch/RecoverySlot object through the flow, or keep targets context-free and derive the slot key from the batch boundary. Add focused tests for mismatched/stale target handling after the boundary is explicit.

&& phase.nextOffset === 0
&& phase.quads.length === 0
);
if (freshEmptyPhase(params.metaResult) && freshEmptyPhase(params.dataResult)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Data-phase freshness is not covered for clean-absence classification

What's wrong
The new exact-absence proof depends on both fetched phases being fresh and empty, but the added tests only prove the metadata-side guard. Because this disposition feeds VM recovery backoff, a future regression could classify a stale or resumed data phase as clean-absent without a test failing.

Example
A regression that changed this to check only freshEmptyPhase(params.metaResult) would still pass the current table. Add cases like runExact({ data: { responderSessionStartedFresh: false } }) and runExact({ data: { resumedFromOffset: 4, nextOffset: 4 } }), both expecting incomplete.

Suggested direction
Mirror the existing metadata-only negative cases for the data phase so the clean-absence proof is verified for both halves of the physical fetch.

For Agents
Look in packages/agent/test/sync-requester-progress.test.ts under exact durable fetch disposition. Add negative cases that make only the data phase stale/resumed while metadata remains a fresh empty response, and assert the detailed disposition remains incomplete.

libp2p: { getConnections: () => [{ remotePeer: connectedPeer }] },
};
(internals as any).vmReconcileCuratorPeersByCg.set(localCgId, [cachedPeer]);
(internals as any).resolveCuratorPeerIdsForCg = async () => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Curator discovery failure test stubs away the new failure detection

What's wrong
The PR adds lookupFailed as the signal that separates a discovery outage from a successful empty curator resolution, and recoverVmReconcileBatch uses that distinction to retain or clear cached authoritative curators. The added test asserts the caller behavior with a stubbed lookupFailed value, but it does not verify that real discovery exceptions produce that value, which is the fragile part of the change.

Example
A regression that changed resolveCuratorPeerIdsForCg to return { peerIds: [], lookupFailed: false } when discovery.findAgents throws would clear vmReconcileCuratorPeersByCg in recoverVmReconcileBatch, but the current retention test would still pass because it bypasses resolveCuratorPeerIdsForCg entirely.

Suggested direction
Exercise the actual discovery.findAgents failure path instead of precomputing lookupFailed in a resolveCuratorPeerIdsForCg stub.

Confidence note
The caller branch is covered, but I did not find a test that exercises the new resolver behavior through a throwing discovery registry.

For Agents
Add a focused regression test using the real resolveCuratorPeerIdsForCg path: set up a structural wallet-scoped CG, seed vmReconcileCuratorPeersByCg, make discovery.findAgents throw, then run recoverVmReconcileBatch and assert the cached curator is retained and used. Consider a companion successful-empty discovery case to prove the cache is cleared only on a real successful lookup.

export interface PriorityAdmissionQueueHooks<Payload> {
canRun: (entry: PriorityAdmissionEntry<Payload>) => boolean;
onStart: (entry: PriorityAdmissionEntry<Payload>) => PriorityAdmissionRelease;
/** Undo capacity claimed by onStart when it throws before returning its release. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Make start admission rollback atomic instead of a caller-mirrored hook

What's wrong
This hook makes the generic queue harder to reason about because rollback correctness depends on each caller manually mirroring side effects from onStart. That is brittle API design, especially in a scheduler that is supposed to centralize admission invariants.

Example
The queue contract now says onStart may mutate capacity, may throw before returning a release, and the caller must remember to provide a separate rollback that undoes whatever part of onStart happened. That is a split transaction encoded by convention.

Suggested direction
Replace the separate rollback hook with an atomic start contract. The cleaner design is one owner for capacity state, not paired callbacks that must remain behaviorally inverse.

For Agents
Look at PriorityAdmissionQueueHooks and start() in packages/agent/src/sync/priority-admission-queue.ts, plus the global backpressure hook in packages/agent/src/sync/backpressure.ts. Preserve rollback-on-start-failure behavior, but refactor the API so capacity claim and release are owned atomically, e.g. by making the queue own inflight accounting or by requiring onStart to return a claim object before any fallible work.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Do not make callers compensate for half-started admissions

What's wrong
onStartFailureRollback leaks the scheduler's failure semantics into every caller that mutates capacity during onStart. That makes the generic queue less self-contained and turns admission atomicity into an optional convention rather than an invariant enforced by the abstraction.

Example
A future queue consumer can mutate its own capacity in onStart, throw before returning a release, and forget to implement onStartFailureRollback; the generic queue will still look reusable, but its atomic start contract will silently depend on caller discipline.

Suggested direction
Make the queue's start path atomic by construction instead of exposing a compensating hook. The queue should either own the capacity counter or require start hooks to be side-effect-free until the release is available.

Confidence note
I am judging this as an abstraction-boundary issue rather than asserting a current behavior bug; it depends on whether this queue is intended to stay generic for more callers.

For Agents
In priority-admission-queue.ts, redesign start admission so the queue owns the atomic transition. Options include splitting capacity reservation from side-effectful start, making onStart return a transaction/release before external work can throw, or moving the inflight accounting into the queue payload/policy. Preserve the new tests for start failure rollback.

});

it('runs the oldest aged entry before newer elevated work', async () => {
it('bounds an aged lower-priority entry behind one raw-priority overtake', async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Do not push sync-backpressure.test.ts past 1k lines

What's wrong
This PR takes a test file from under 1k lines to well over 1k lines. The added scenarios are valuable, but keeping them in the already broad backpressure suite makes the scheduler behavior harder to scan and maintain.

Example
The block beginning with bounds an aged lower-priority entry behind one raw-priority overtake adds many local now, running, enabled, starts, and PriorityAdmissionQueue setups that repeat the same scheduler harness in slightly different shapes.

Suggested direction
Move the new PriorityAdmissionQueue fairness/debt coverage into a dedicated test file with a reusable harness. Keep production-helper tests in the existing backpressure suite.

For Agents
Split the new scheduler fairness tests out of packages/agent/test/sync-backpressure.test.ts into a focused priority-admission-queue test file, and extract a small queue harness/factory for clock, running count, release capture, and starts. Preserve the scenarios; just decompose the file before it crosses the 1k-line boundary.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Split the new state-machine tests instead of growing mega specs

What's wrong
The PR pushes sync-backpressure.test.ts from below 1k lines to well over 1k lines and appends a large amount of direct queue policy coverage to a global backpressure test file. The same sprawl pattern appears in core-fills-gap.test.ts, which becomes a 4k-line catch-all for exact VM recovery state-machine coverage. This makes the test suite harder to navigate and hides the ownership boundary of the new abstractions.

Example
The tests beginning at sync-backpressure.test.ts:475 instantiate PriorityAdmissionQueue directly and assert low-level scheduling policy. Those belong with the queue, not mixed into the global withGlobalSyncBackpressure coverage. The exact recovery tests similarly exercise rotation internals through (internals as any) in a 4002-line fixture file.

Suggested direction
Create focused test files around the units this PR actually changes: one for PriorityAdmissionQueue policy/debt behavior and one for exact VM recovery rotation. Keep only global backpressure integration cases in sync-backpressure.test.ts and only core-fill integration cases in core-fills-gap.test.ts.

For Agents
Split the new queue scheduler cases into priority-admission-queue.test.ts and move the exact VM rotation scenarios into a dedicated VM reconcile/exact recovery spec. Preserve all assertions, but extract shared fixture builders so the production changes are covered by focused tests rather than expanding unrelated mega-files.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Decompose the backpressure test file before it grows past a healthy size

What's wrong
This PR turns the backpressure test file into a large mixed-level suite. The file-size jump makes it harder to find the production-helper tests, while the new direct queue tests deserve their own focused home. Even though this is test code, the structure matters because these scheduler rules are subtle and will be maintained as executable documentation.

Example
Many new cases create the same local now, running, enabled, starts, and new PriorityAdmissionQueue setup, then assert queue ordering directly. That is a separate scheduler spec living inside a higher-level backpressure test file.

Suggested direction
Split the low-level queue policy cases from the global backpressure integration cases. A dedicated queue test file plus a small fixture builder would cut repetition and keep both suites easier to scan.

Confidence note
The diff context strongly suggests this file crossed the 1k-line threshold in this PR, but confirm against the exact base if needed.

For Agents
Move direct PriorityAdmissionQueue scheduler-policy tests into packages/agent/test/priority-admission-queue.test.ts or a nested scheduler suite with shared fixture helpers. Leave sync-backpressure.test.ts focused on the production withGlobalSyncBackpressure wiring and metrics behavior.

Comment thread packages/agent/src/dkg-agent.ts Outdated
// cancelling midway could strand a partially applied VM transition.
// Clear exact-absence rotations first so a late in-flight response cannot
// restore process-local suppression while the dispatcher drains.
this.closeVmReconcileRotationState();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Shutdown rotation cleanup is not verified through stop()

What's wrong
The change relies on DKGAgent.stop() closing process-local exact-recovery rotation state before the dispatcher drains. Current tests validate the helper behavior directly, but they do not prove that the agent shutdown path invokes it, leaving a regression gap for late exact responses mutating suppression state during shutdown.

Example
Start recoverVmReconcileBatch with syncExactKnowledgeAssetsFromPeerDetailed blocked, call agent.stop(), then release the blocked fetch. Expected: vmReconcileRotationState remains empty and the late response cannot enter backoff.

Suggested direction
Cover the actual stop() path, not only the helper method, because the new safety depends on this wiring being present.

Confidence note
I found tests that call the cleanup helper directly, but not a test that exercises this new production stop() wiring.

For Agents
Add a lifecycle regression test in a shutdown/core-fill test file that drives DKGAgent.stop() during an in-flight exact VM recovery, then asserts vmReconcileRotationClosed is set and no rotation record is recreated after the pending fetch settles.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Shutdown rotation cleanup is not verified through agent.stop()

What's wrong
The diff’s shutdown safety depends on DKGAgent.stop() calling closeVmReconcileRotationState() before dispatcher drain. The added tests exercise the close helper directly, so they would not catch a regression where the production stop path omits or reorders this call, leaving a late exact-recovery response able to restore process-local suppression during shutdown.

Example
Failing-test sketch: start or simulate an in-flight exact VM recovery that has installed a rotation record, call agent.stop(), then release the pending exact request with disposition='clean-absent'. The test should assert that vmReconcileRotationClosed is true and vmReconcileRotationState remains empty. If the new stop() call were removed or moved too late, the current helper-level tests would still pass.

Suggested direction
Add a behavior-level stop() test that seeds or creates rotation state, triggers stop while recovery work is pending, then verifies the state is cleared/closed and late settlement cannot recreate suppression.

For Agents
Add a lifecycle regression around DKGAgent.stop, likely near the VM reconcile tests or stop lifecycle tests. Preserve the existing helper behavior, but prove the public stop path invokes it before late exact-recovery settlement can mutate rotation state.

@@ -2367,6 +2597,1004 @@ describe('Phase D — reconcile gate + core-fill telemetry', () => {
expect(second.outcomes.get(0)).toEqual({ status: 'reconciled', blockNumber: 100 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Split the new exact-recovery rotation tests behind the same extracted boundary

What's wrong
The tests are reinforcing the production sprawl: they add a large private-internals harness to an already very large integration file. That makes the behavior harder to maintain because small state-machine changes will require editing a broad integration suite instead of a focused unit-level boundary.

Example
The new rotation cases around line 2597 stub vmReconcileRotationNow, mutate private maps, override internals, and call private helpers directly. That is a sign the production rotation logic wants its own module with a public test harness.

Suggested direction
Create a dedicated rotation-state test suite and a small fixture API. This will make the test coverage easier to scan and reduce the amount of private-agent setup each case needs.

For Agents
After extracting the rotation state machine, move these exact VM rotation cases into a focused test file for that module. Keep only integration-level recovery behavior in core-fills-gap.test.ts. Preserve the same scenarios, but drive them through typed fixtures instead of broad (internals as any) mutation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Decompose the newly expanded test suites before they become unmaintainable

What's wrong
The PR adds substantial scenario coverage by appending to already broad test files. That makes the behavior harder to scan, slows future review, and obscures the natural boundaries between VM rotation policy, host recovery orchestration, and generic scheduler fairness.

Example
A future change to VM exact-recovery rotation now has to navigate a 4k-line mixed fixture file, while scheduler fairness scenarios are packed into a broad global backpressure suite. These are cohesive enough to be their own suites.

Suggested direction
Move the new exact-recovery rotation tests and scheduler fairness tests into focused files with local fixtures. Keep core-fills-gap.test.ts for the higher-level core-fill behavior, and keep global backpressure tests separate from low-level queue fairness mechanics.

For Agents
Split the newly added VM rotation scenarios into a focused file such as vm-reconcile-rotation.test.ts with a shared harness, and split queue fairness/debt cases into a priority-admission-queue-fairness.test.ts suite. Preserve the same assertions; this is decomposition, not coverage reduction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Split the exact-recovery rotation tests out of the 4k-line core-fills spec

What's wrong
The PR adds a large new suite into an already oversized catch-all test file. Even if the cases are useful, this makes the test surface harder to navigate and encourages more copy-pasted internal-agent setup instead of a reusable, domain-specific harness.

Example
The new curator/rotation tests repeatedly build an agent, cast to AgentInternals, stub node.libp2p, resolveCuratorPeerIdsForCg, ensurePeerConnected, selectCatchupPeers, waitForSyncProtocol, syncExactKnowledgeAssetsFromPeerDetailed, and reconcileChainOrdinal. That setup is now duplicated across dozens of cases in a 4k-line spec.

Suggested direction
Create a focused vm-reconcile-rotation.test.ts or similar and centralize the repeated agent/peer stubbing behind a small fixture builder. Keep core-fills-gap.test.ts for the broader core-fill behavior it already owned.

For Agents
Split the exact VM recovery rotation coverage out of core-fills-gap.test.ts into a focused spec, and extract a local harness for peer rosters, curator resolution, exact fetch disposition, and reconcile outcomes. Preserve the same test scenarios; the change should only reduce fixture duplication and file size.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Split the exact-recovery rotation tests out of the oversized core-fill suite

What's wrong
The test coverage for this change is structurally coupled to private implementation details and concentrated in an already very large file. That makes the test suite harder to scan and also reinforces the production-code smell: there is no clear boundary for the rotation state machine, so tests have to manipulate the whole agent object directly.

Example
The block starting with exact VM recovery cases sets up node.libp2p, preferredSyncPeers, resolveCuratorPeerIdsForCg, ensurePeerConnected, waitForSyncProtocol, syncExactKnowledgeAssetsFromPeerDetailed, and reconcileChainOrdinal inline in many tests. That setup is longer than the behavior each test is trying to express.

Suggested direction
Create a dedicated rotation test harness that exposes domain-level operations such as withPeers, nextDisposition, runRecovery, and rotationRecord. That would make the tests describe the state machine instead of reassembling the agent internals in every scenario.

For Agents
Move the exact VM recovery rotation cases into a focused test file, for example vm-reconcile-rotation.test.ts, and introduce a small harness for peer rosters, recovery targets, dispositions, and cooldown control. Preserve the same assertions, but stop growing the already broad core-fill suite.

}

closeVmReconcileRotationState(this: DKGAgent): void {
this.vmReconcileRotationClosed = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Restarting an agent leaves exact VM recovery permanently closed

What's wrong
The new shutdown flag is one-way. stop() calls closeVmReconcileRotationState(), which sets vmReconcileRotationClosed to true, but start() never flips it back. Since the recovery path treats that flag as a hard suppressor, reusing an agent instance after stop/start disables exact-recovery fetches for the rest of the process.

Example
Call await agent.start(); await agent.stop(); await agent.start(); then run VM reconcile with pending recovery targets. prepareVmReconcileRotationTarget() sees vmReconcileRotationClosed === true and returns suppressed/noRecovery, so exact asset recovery never fetches again on that agent instance.

Suggested direction
Reopen the process-local rotation state during start/reinitialization, or make the closed flag tied to a stop generation so stale responses from the old run stay inert without blocking the next run.

For Agents
Check DKGAgent start/stop and VM reconcile rotation state. Preserve the late-response guard during stop, but reset or generation-scope vmReconcileRotationClosed on start; add a restart test proving recoverVmReconcileBatch can prepare and fetch after stop/start.

return 'clean-absent';
}

function stripLiteral(raw: string): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Reuse the canonical RDF literal helper instead of adding another regex parser

What's wrong
This adds avoidable parsing duplication in a boundary that already depends on exact RDF metadata shape. Duplicated literal handling tends to drift, and the local regex makes the new exact-fetch helper less trustworthy and harder to maintain.

Example
stripLiteral here is another bespoke literal parser in a proof-sensitive sync path. Even if it matches today’s expected metadata shape, future escaping or typed-literal handling has to be kept consistent by hand across multiple local implementations.

Suggested direction
Delete the local stripLiteral and use a shared parser such as parseRdfLiteralTerm/decodeRdfLiteralBody from the RDF utility package, or a single agent-level helper if that is the intended boundary.

For Agents
Replace the local regex helper with the canonical RDF literal parser/helper used elsewhere, or move one sync-safe helper into a shared module that both durable sync and exact durable fetch can import. Preserve descriptor filtering behavior for plain, typed, and raw IRI-style values.

snapshotRef,
sinceBatchId,
assetUals,
maxAcceptedBytes: exactAccumulationLimits?.maxBytes,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Exact recovery limit wiring is not covered by an integration test

What's wrong
The PR adds the important production wiring that makes exact VM recovery bounded against legacy responders, but the tests only verify the lower-level guard when limits are supplied manually or stub the guard as already failed. That leaves the actual production path unverified: a regression that stops passing these limits would keep the new tests green while exact recovery loses the bound.

Example
Delete maxAcceptedBytes/maxAcceptedQuads from this fetchSyncPages call: the low-level accumulation tests would still pass because they call fetchSyncPages directly with explicit limits. Add a lifecycle-level test that invokes the exact recovery fetch path with assetUals and asserts the underlying fetch receives the calculated byte and quad ceilings.

Suggested direction
Add a test at the lifecycle wrapper boundary, either by spying/stubbing the requester call or by driving an exact recovery fetch through fetchSyncPage, so removing this wiring would fail.

For Agents
Look in packages/agent/src/dkg-agent-lifecycle.ts around fetchSyncPage and the existing sync fetch coalescing/lifecycle tests. Preserve ordinary full-sync calls with no limits, and add an exact-asset call that proves assetUals wires exactSyncPhaseAccumulationLimits into fetchSyncPages.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Exact recovery accumulation limits lack a production-wiring test

What's wrong
The PR adds a security/resource-boundary for exact VM recovery, but the tests only prove the low-level page fetcher enforces limits when called with explicit max values. They do not prove exact recovery actually supplies those max values on the production path.

Example
A regression that drops maxAcceptedBytes/maxAcceptedQuads from the fetchSyncPages call at the lifecycle boundary would let VM exact recovery accept an oversized legacy full-CG response in production, while sync-exact-accumulation.test.ts would remain green because it bypasses this adapter and supplies the limits itself.

Suggested direction
Cover the lifecycle binding, not only the low-level limiter. A focused test can reuse the sync-fetch-coalescing agent helper or mock the requester boundary to capture/trigger the maxAcceptedBytes and maxAcceptedQuads behavior.

For Agents
Add a lifecycle-level exact-fetch test around DKGAgent.fetchSyncPages or syncExactKnowledgeAssetsFromPeerDetailed that passes assetUals and returns a response over the exact per-asset byte/quads limit, proving the production adapter rejects it. Also assert a full sync without assetUals is not capped by those exact limits.

peerIds: string[];
curatorIsLocal: boolean;
legacyTripleResolved: boolean;
lookupFailed?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Make curator resolution state explicit instead of adding an optional mode flag

What's wrong
The optional lookupFailed flag turns a previously simple resolver contract into a loosely-shaped mode object. That is a maintainability smell because the absence of the field has meaning, false has meaning, and empty peerIds still has multiple meanings depending on other flags. The VM recovery cache policy is also now encoded inside a resolver whose documentation is still framed around the write path.

Example
The return shape can now mean at least four different states with overlapping fields: local curator, resolved remote peers, authoritative empty, and lookup unavailable. Because lookupFailed is optional, callers must infer meaning from combinations like peerIds: [], curatorIsLocal: false, legacyTripleResolved: false, and missing versus false lookupFailed.

Suggested direction
Use a typed discriminated union or a dedicated recovery resolver that returns an explicit unavailable state. That makes each caller handle the states it actually understands and keeps recovery-specific cache policy from leaking into a shared write-path resolver.

Confidence note
This is a boundary/design finding rather than a behavioral claim; it assumes resolveCuratorPeerIdsForCg remains a shared resolver for write/SWM paths, as its existing comment says.

For Agents
Replace the ad-hoc optional flag with a discriminated result, for example status: 'local' | 'resolved' | 'empty' | 'unavailable', or move the cached-roster/unavailable distinction into a VM-recovery-specific resolver. Preserve existing write-path semantics: empty authoritative resolution should still be unconfirmed, while unavailable recovery resolution may reuse the cached roster.

Comment thread packages/agent/src/sync/requester/graph-scoped-materialization.ts
Comment thread packages/agent/src/context-graph-membership-persist-scheduler.ts
Comment thread packages/agent/src/context-graph-binding-generation.ts Outdated
Comment thread packages/agent/src/dkg-agent-lifecycle.ts
Comment thread packages/agent/src/dkg-agent-lifecycle.ts
Comment thread packages/cli/src/daemon/teardown.ts Outdated
Comment thread packages/agent/src/p2p/peer-connect.ts
Comment thread packages/agent/src/discovery.ts Outdated
Comment thread packages/agent/src/dkg-agent-lifecycle.ts
Comment thread packages/agent/src/dkg-agent.ts

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: retry_exhausted

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.

2 participants