Skip to content

feat(agent): chain-driven VM reconciliation engine (Phase B)#910

Closed
branarakic wants to merge 11 commits into
feat/warm-core-connectionsfrom
feat/chain-driven-vm-reconciliation
Closed

feat(agent): chain-driven VM reconciliation engine (Phase B)#910
branarakic wants to merge 11 commits into
feat/warm-core-connectionsfrom
feat/chain-driven-vm-reconciliation

Conversation

@branarakic
Copy link
Copy Markdown
Contributor

Summary

Phase B of the Telegram-style chain-anchored sync plan. Installs the engine that turns "chain says a KA was registered to a CG" into "that KA is in my VM" — the missing chain-driven promotion path behind the monday-fun-facts missing-VM symptom. Stacked on #908 (warm Core connections) → #906 (Core-preferred peer order) → main.

The finalization handler has long logged "will retry via ChainEventPoller" when it can't promote a publish, but that retry was never implemented: the chain-event path was confirm-only-local. A node that was offline (or transport-flaky) during the finalization gossip would get the SWM via regular sync but nothing then promoted it to VM. Phase B finishes that sentence.

How it works

  • Ordering key (B.0 spike): batchId is a KC merkle hash (no ordering), so the cursor key is the per-CG registration ordinalContextGraphStorage.getContextGraphKCCount(cgId) is the chain-head ordinal and getContextGraphKCAt(cgId, i) maps ordinal → kaId. Dense, monotonic, comparable: plain integer cursor math.
  • Sweep-driven, not event-driven. The live KnowledgeAssetRegisteredToContextGraph event is a low-latency nudge (it carries no ordinal, so it's never a cursor position); the periodic + on-startup sweep is the safety net that guarantees eventual reconciliation and catches up late subscribers.
  • Contiguous watermark + reorg depth. Out-of-order completions wait in an in-memory ahead set until the gap below fills; the persisted lastReconciledOrdinal only advances over a contiguous run buried by a confirmation depth (observation-block based, so no per-ordinal block lookup).
  • Verification from chain truth. handleChainReconciledKC verifies the KA→CG binding via a direct getKAContextGraphId read and matches the chain merkle root against a recomputed local-SWM flat-KC root — no untrusted-gossip event re-scan. Active fetch is core-first (reuses Phase A peer ordering) with one retry on no-swm.

Commits

  1. feat(chain,publisher) — per-CG ordinal reads (getContextGraphKCCount/getContextGraphKCAt) + KACG event decode/plumbing (evm + mock adapters, poller callback).
  2. feat(agent,node-ui,cli) — persist lastReconciledOrdinal watermark end-to-end via V17 migration; opportunistically fix the latent onChainHash non-persistence.
  3. feat(agent) — pure contiguous-watermark + confirmation-depth cursor module.
  4. feat(agent) — extract applyVerifiedFinalization; add public handleChainReconciledKC.
  5. feat(agent) — sweep orchestrator + per-CG coalescer + UAL dedupe.
  6. refactor(agent) — chain-reconcile verifies via direct CG-binding read.
  7. feat(agent) — wire it all into DKGAgent (live nudge, periodic/startup sweep, watermark persistence). Gated behind vmReconcileEnabled().

Tunables: DKG_VM_RECONCILE_INTERVAL_MS (default 60s), DKG_VM_RECONCILE_CONFIRMATION_DEPTH (default 5).

Test plan

  • reconcile-cursor.test.ts (12) — watermark contiguity, gap handling, reorg depth, sweep planning
  • chain-reconciler.test.ts (8) — sweep gap-fill, watermark-persist-on-move, reorg depth, coalescer, UAL dedupe
  • finalization-handler.test.ts (15) — incl. 5 handleChainReconciledKC cases (promote / no-swm / unverified / wrong-CG / already-confirmed)
  • chain-reconcile-e2e.test.ts (2) — full sweep + real FinalizationHandler over MockChainAdapter: multi-KC promotion + gap-hold-then-fill
  • mock-adapter-kc-views.test.ts (8), chain-event-poller-ka-registered.test.ts (2), db.test.ts (58), messenger-stores.test.ts (21)
  • Regression: agent.test.ts + context-graph-discovery.test.ts (158), e2e-context-graph.test.ts (11)
  • Full workspace pnpm build, agent + cli builds, lint clean

Made with Cursor

Branimir Rakic and others added 7 commits June 1, 2026 23:59
…ase B foundation)

Foundational layer for chain-driven VM reconciliation:

- chain-adapter: add optional getContextGraphKCCount / getContextGraphKCAt —
  the per-CG registration ordinal that the reconciler uses as its dense,
  monotonic cursor key (resolves the B.0 ordering-key spike).
- evm-adapter: implement the two reads (ContextGraphStorage) and decode the
  KnowledgeAssetRegisteredToContextGraph event in listenForEvents.
- mock-adapter: implement ordinal reads over the in-memory collections map
  (insertion order == registration order) + __emitKARegisteredToContextGraph
  test helper.
- chain-event-poller: subscribe to KnowledgeAssetRegisteredToContextGraph and
  surface it via a new onKARegisteredToContextGraph callback (low-latency
  nudge; gated so it only subscribes when wired).

Tests: mock ordinal reads + event surfacing; poller dispatch + opt-in gating.
Co-authored-by: Cursor <cursoragent@cursor.com>
…nHash (Phase B.3)

Adds the persisted per-CG reconciliation cursor and fixes a latent
persistence gap:

- ContextGraphSub / ContextGraphSubscriptionRecord: add lastReconciledOrdinal
  (the per-CG registration-ordinal VM watermark — count of KAs promoted to VM
  contiguously). The sweep resumes from here, so it MUST survive restarts.
- db.ts: V17 migration adds on_chain_hash + last_reconciled_ordinal columns
  (defensive PRAGMA-then-ALTER), threads them through upsert + row type.
- onChainHash was declared on the type but never persisted (no column + the
  save/load mapping dropped it); V17 folds in that fix so host-mode recovery
  resumes on the right topic after restart.
- dkg-agent: thread both fields through persist + rehydrate.
- lifecycle: thread both fields through loadAll + save.

Tests: column round-trip (NULL default + set/advance), V16->V17 upgrade
preserves rows, fresh install carries columns; bump current-version pins.

Co-authored-by: Cursor <cursoragent@cursor.com>
…/B.3b core)

Pure, side-effect-free cursor over the per-CG registration ordinal:

- watermark === N means ordinals [0,N) are durably in VM; gap to reconcile
  is [watermark, head) where head = getContextGraphKCCount(cgId).
- recordCompletion/absorbConfirmed enforce the two correctness invariants the
  plan flags as critical: (1) no gap is ever skipped (out-of-order completions
  wait in `ahead` until the run below them fills), (2) reorg safety (an ordinal
  only advances the watermark once its registration block is buried by
  confirmationDepth; promotion stays eager, the cursor advance waits).
- ordinalsToReconcile drives the B.2 sweep and skips already-completed-but-held
  ordinals so a sweep never re-fetches them; a failed (unrecorded) fetch is
  naturally retried next sweep.

Extracted as a pure module so the watermark logic is unit-testable without a
chain/libp2p/store harness (12 tests: in-order, gap-hold, out-of-order burst,
depth gate, depth+gap interaction, sweep planning, fetch-retry).

Co-authored-by: Cursor <cursoragent@cursor.com>
…ciledKC (Phase B.5)

- Extract the TOCTOU-safe promoteUnderLocks/runUnderLocks inline closure from
  handleFinalizationMessage into a private applyVerifiedFinalization(...) so the
  gossip path and the new chain-driven path share one locked-promotion impl.
  Gossip behavior is byte-identical (existing 25 finalization tests stay green);
  the only change is captured locals become explicit params.
- Add public handleChainReconciledKC(input, ctx): promotes a chain-registered KC
  to VM with no gossip FinalizationMessage. Idempotent (already-confirmed),
  recovers the published roots by matching the chain merkleRoot to a local SWM
  WorkspaceOperation's RECOMPUTED flat-KC root (SWM meta has no stored root since
  it predates publish), runs the same verifyOnChain + applyVerifiedFinalization,
  and does not re-broadcast. Returns promoted | already-confirmed | no-swm |
  unverified | stale-target so the agent reconciler can drive the watermark.
- handleFinalizationMessage signature unchanged (still the sole gossip entry).

Tests: 4 new cases (promoted via verifying chain, no-swm on root mismatch,
unverified with no chain, already-confirmed idempotency).

Co-authored-by: Cursor <cursoragent@cursor.com>
…se B.2/B.4 core)

Testable, side-effect-injected orchestrator over the ordinal cursor:

- reconcileContextGraph: one sweep pass for a CG — reconcile [watermark, head)
  via injected reconcileOrdinal, advance the contiguous + confirmation-depth
  watermark, persist only when it moves. Pending ordinals hold the watermark and
  are retried next sweep; held ordinals are never re-attempted.
- ReconcileCoalescer: per-CG single-flight so a burst of KACG events triggers one
  sweep (+ one trailing run if events land mid-sweep), not N.
- RecentUalSet: bounded UAL dedupe for the live-event nudge.

Extracted as a module (deps injected) so the sweep/watermark/coalescing logic is
unit-tested without a chain/libp2p/store harness (8 tests). Agent wiring of the
real reconcileOrdinal + poller callback + sweep timer is the next step.

Co-authored-by: Cursor <cursoragent@cursor.com>
…(Phase B.5)

handleChainReconciledKC now verifies the KA→CG binding with a direct
getKAContextGraphId(kaId) chain read instead of re-scanning KCCreated
events. The chain-driven trigger is already chain truth (merkleRoot +
publisher come from getLatestMerkleRoot/Publisher), so the event re-scan
(which defends the untrusted gossip wire) was redundant — and the sweep
path has no event, hence no txHash/block to scan with.

Integrity is still the flat-KC root recompute match against the chain
merkleRoot. The materialization version is stamped with the chain head
block (getLatestMerkleRoot returns latest state, so reconcile is
"as of now"; a later real update supersedes correctly).

Drops txHash/blockNumber from the input in favour of versionBlock; adds
a 'bound to a different CG' -> unverified guard. Tests updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
…e B.4)

Integrates the chain-reconciler sweep engine into DKGAgent:

  * Live nudge: the ChainEventPoller's onKARegisteredToContextGraph
    callback resolves the local CG and triggers a coalesced sweep — the
    event carries no ordinal, so it's a low-latency nudge, never a cursor
    position.
  * Periodic + startup sweep (DKG_VM_RECONCILE_INTERVAL_MS, default 60s):
    the safety net that backfills missed events / transient fetch
    failures and catches up late subscribers (the "Monday Fun Facts"
    case). Primed once on start so a late subscriber reconciles
    immediately.
  * reconcileChainOrdinal: per-ordinal driver — getContextGraphKCAt ->
    kaId -> getLatestMerkleRoot/Publisher -> handleChainReconciledKC,
    with a core-first active catch-up fetch + one retry on no-swm.
    headBlock is reused as the materialization version AND the cursor
    observation block (reorg gate), so no per-ordinal block lookup.
  * Watermark persistence via lastReconciledOrdinal (V17 column); cursor
    state held per-CG in memory and rehydrated from the persisted
    watermark.

Only armed when the chain adapter exposes the per-CG registration-ordinal
reads (vmReconcileEnabled), so non-V10 / no-chain nodes pay nothing.

reconcileOrdinal gains a headBlock param (additive). Adds an end-to-end
test driving the real sweep + FinalizationHandler over MockChainAdapter:
multi-KC promotion + gap-hold-then-fill.

Co-authored-by: Cursor <cursoragent@cursor.com>
subGraphName?: string,
): Promise<{ rootEntities: string[]; sharedMemoryQuads: Quad[] } | null> {
const graphManager = new GraphManager(this.store);
const wsMetaGraph = subGraphName
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: this only looks in the root SWM workspace unless subGraphName is already known, but the new chain-driven path in DKGAgent.reconcileChainOrdinal() never supplies a sub-graph name. Any KA published into a named sub-graph will therefore stay no-swm forever and its ordinal will never reconcile. Please add a fallback that scans all shared-memory sub-graphs (or persist/recover the target sub-graph from SWM metadata before calling this helper).


let cursor = this.reconcileCursors.get(localCgId);
if (!cursor) {
cursor = createCursorState(sub.lastReconciledOrdinal ?? 0);
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: the persisted watermark is reused purely by local CG id here, but the PR never invalidates it when onChainId changes or is cleared/re-registered. If a local CG is repaired/recreated on-chain, the old lastReconciledOrdinal will make the new chain graph start sweeping from the wrong ordinal and permanently skip earlier KAs. Please key the persisted watermark by onChainId, or reset lastReconciledOrdinal whenever the bound on-chain id changes.

…910)

- SWM sub-graph fallback: the chain-driven path never knows a publish's
  sub-graph, so findSwmSnapshotForMerkleRoot now searches the root
  workspace AND every registered sub-graph, returning the namespace it
  matched in so promotion lands in the right data graph. Previously a KA
  published into a named sub-graph stayed `no-swm` forever.
- Reset the reconcile watermark when a local CG's bound on-chain id
  changes (repair/recreate). lastReconciledOrdinal counts contiguous KAs
  for a specific on-chain graph; reusing it across a rebind would start
  the sweep at the wrong ordinal and skip earlier KAs. New
  bindSubscriptionOnChainId() zeroes the watermark + drops the cursor on
  a real id change, and the reset is persisted (restart-safe).

Adds a regression test for the sub-graph fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
}
}

/**
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: This reset is gated on lastReconciledOrdinal > 0, but stale reconcile state can survive even when that is false. In particular, the re-register flow clears sub.onChainId before the new id is bound, and an in-memory cursor can also still hold ahead ordinals while the watermark is 0. In both cases the new on-chain graph inherits old cursor state and the sweep can skip early ordinals. Reset the cursor/watermark whenever the bound on-chain id changes (or a cursor exists), not only when the persisted watermark is positive.

const localCgId = this.resolveLocalCgIdByOnChainId(BigInt(onChainId));
if (!localCgId) return; // chain replay hasn't resolved the cleartext CG yet; sweep heals it
const sub = this.subscribedContextGraphs.get(localCgId);
if (!sub?.subscribed) return; // only populate VM for CGs we actually subscribe to
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: resolveLocalCgIdByOnChainId() returns the first match, which can be the synthetic hash-keyed host record created from ContextGraphCreated (subscribed: false). When that happens this handler exits at the next guard and never nudges the real subscribed cleartext CG that shares the same onChainId. Prefer a subscribed match, or iterate all matches, so live KACG nudges do not become no-ops until the periodic sweep catches up.

// Chain-driven reconciliation never requests same-graph dual-write — the
// root-copy decision is a publisher gossip signal (`keepRootCopyOnLabel`)
// that has no chain equivalent. Promote per-cgId only.
const isDualWrite = 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.

🔴 Bug: For chain-driven backfill this forces every recovery into the per-cgId graph only. If a node missed the original finalization gossip for a same-graph publish, the sweep will promote the VM data but never recreate the root-label copy/meta that label-scoped reads depend on, so agent.query(<cg label>) can still miss data after 'reconciliation'. The reconcile path needs a durable keep/drop-root signal (for example persisted in SWM metadata) and should mirror the normal dual-write decision here.

branarakic pushed a commit that referenced this pull request Jun 2, 2026
Addresses the unresolved automated review findings on #908/#910/#911/#912/#914:

#908 warm-core:
 - only claim a maxCores slot when pin() actually succeeds (skip on failure)
 - serialize reconcile passes with an in-flight guard (no overlapping dials/unpins)
 - freshness-aware core selection: drop stale phonebook rows + sort by lastSeen
   before the cap (reuse AGENT_PROFILE_STALE_THRESHOLD_MS)

#910/#911 chain-reconcile:
 - reset the reconcile cursor AND watermark whenever the bound onChainId changes,
   not only when the persisted watermark is >0 (an in-memory cursor can hold
   ahead ordinals while the watermark is 0)
 - resolveLocalCgIdByOnChainId prefers a subscribed match over a synthetic
   host-only record so live KACG nudges aren't no-ops
 - finalization root-label dual-write on chain backfill: documented as a tracked
   follow-up (needs a share-time persisted keepRootCopyOnLabel signal; a
   heuristic risks double-counting). Scoped reads are unaffected.

#912 sinceBatchId:
 - parse the marker only from the trailing |since|<n> suffix (no segment scan)
 - normalize legacy batchId placement (KA vs KC/UAL) and literal form
   (typed xsd:integer vs untyped) in the delta join so legacy data isn't hidden

#914 core-fill:
 - discard the swmGraphId hint only when it equals the on-chain id (numeric local
   cleartext ids like "1" are now honoured)
 - devnet test: clear stale daemon.pid/devnet.pid before restart-node
 - devnet test: pick a HOST-ONLY victim (core_hosted=1, subscribed=0) so the
   fill-the-gap scenario truly gates Phase D (not subscriber reconciliation);
   expose `subscribed` on the /api/replication/cursors row to enable it

All 1115 agent + 1331 node-ui unit tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Branimir Rakic and others added 2 commits June 2, 2026 12:15
- resolveLocalCgIdByOnChainId now prefers a SUBSCRIBED match over a
  synthetic hash-keyed host record (subscribed:false) that can share the
  same on-chain id, so live KACG nudges + reconcile target the CG the
  user actually reads instead of no-op'ing at the next guard.
- bindSubscriptionOnChainId resets the reconcile watermark AND the
  in-memory cursor on ANY on-chain-id change, not only when the persisted
  watermark is > 0. A cursor can hold `ahead` ordinals (reconciled but
  awaiting confirmation depth) while the watermark is still 0; the old
  gate let that stale state survive a rebind and skip early ordinals of
  the new chain graph.
- Durable keep-root signal for the chain-driven path: the publisher now
  persists keepRootCopyOnLabel per root into SWM workspace meta (replaces
  prior value on update), co-located with privateMerkleRoot so it
  replicates to subscribers. handleChainReconciledKC recovers it via
  getKeepRootCopySignal and mirrors the gossip dual-write decision, so a
  same-graph publish recovered through the sweep still lands the root
  `<cg>` label copy that label-scoped reads need. Absent signal (legacy /
  remap) stays per-cgId-only — no root copy re-added, no data-isolation
  regression.

Adds chain-reconcile dual-write regression tests (keep-root=true mirrors
the root-label copy; absent signal does not).

Co-authored-by: Cursor <cursoragent@cursor.com>
recordCompletion(
state,
{ ordinal, blockNumber: outcome.blockNumber },
headBlock ?? outcome.blockNumber,
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: when getHeadBlock() is temporarily unavailable, this falls back to confirmationDepth = 0 and advances/persists the watermark immediately. A transient RPC failure on the head-block read can therefore move the cursor past registrations that are still inside the reorg window, so they will never be retried if the chain reorgs. Keep completed ordinals in ahead until a real head block is available, or abort watermark advancement for that sweep.

ual,
rootEntities,
publisherAddress,
txHash: '',
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: this materializes chain-reconciled KCs with txHash: '', which means confirmed metadata is written with an empty dkg:transactionHash and no stable publication provenance for every backfilled VM entry. The live KnowledgeAssetRegisteredToContextGraph event already carries txHash; please thread the real hash through when available, and on the pure sweep path omit tx-hash-derived metadata instead of writing a false empty value.

Test 1 (chained sign-at-creation lifecycle) asserted an exact-ordered
list of four standalone-route operations, but captured every
memory_graph_changed event for the CG. A core also emits
verified_memory_finalized whenever it promotes a peer-published KA to
VM — including via the Phase B periodic chain-reconcile sweep, which can
fire mid-test for unrelated bootstrap KAs in the same CG. Those are
correct background events but polluted the CG-only filter, doubling the
captured list and failing the equality assertion once a bootstrapped
6-node devnet is used.

Scope the SSE capture to the four lifecycle ops the test actually
asserts, so legitimate background VM activity no longer leaks in.

Co-authored-by: Cursor <cursoragent@cursor.com>
state,
{ ordinal, blockNumber: outcome.blockNumber },
headBlock ?? outcome.blockNumber,
headBlock !== undefined ? deps.confirmationDepth : 0,
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: Falling back to confirmationDepth = 0 whenever getHeadBlock() returns undefined breaks the reorg-safety guarantee on real chains. A transient getBlockNumber failure will let the watermark commit immediately, and a shallow reorg can then permanently skip ordinals below the new head. Treat "head unavailable" as "hold completed ordinals in ahead until a real head is observed", not as the no-chain case.

// KC root and comparing to the chain root. This is the same flat-KC root
// the gossip path verifies against, so a match is an authoritative
// merkle verification.
const snapshot = await this.findSwmSnapshotForMerkleRoot(contextGraphId, merkleRoot, subGraphName);
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: This reconcile path identifies the SWM snapshot only by matching the recomputed merkle root. Two different publishes in the same CG can legitimately have identical content (same merkle root but different KA/publisher/sub-graph), and in that case the first matching WorkspaceOperation wins and we attach the current kaId/publisher metadata to the wrong roots. Please persist or compare an additional discriminator from SWM meta/chain data, or fail when the root match is ambiguous.

ual,
rootEntities,
publisherAddress,
txHash: '',
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: Chain-driven reconciliation always materializes confirmed metadata with an empty txHash. That drops dkg:transactionHash provenance and suppresses dkg:Publication/dkg:authoredBy triples even when authorAddress is known, so VM metadata differs from the normal finalization path. The live KACG callback already provides txHash/txIndex; the sweep needs to propagate or fetch those values before writing confirmed metadata.

branarakic pushed a commit that referenced this pull request Jun 2, 2026
…subscribe path

Tier 1 (dual-write at chain reconcile, GH #910 follow-up):
The chain-reconcile path now mirrors the gossip-path dual-write decision instead
of hardcoding isDualWrite=false. keepRootCopyOnLabel can't be persisted at
share-time (share predates the publish/remap decision) and a publish-time local
meta update never reaches the missed-gossip recipient that runs reconcile, so we
DERIVE it: a merkle-verified SWM snapshot found under the on-chain-bound CG
proves a same-graph publish (a remap's snapshot lives under the source CG and
never matches here), so isDualWrite = !!ctxGraphId && !resolvedSubGraphName. A
reconcile-recovered node now converges to the identical graph state as one that
received the finalization gossip, with no double-count risk. Tier 2 tracked in
GH #919 (eliminate dual-write via registry-resolved label reads).

Host-only Phase D gate (#914 follow-up):
Add DKGAgent.unsubscribeFromContextGraph + POST /api/context-graph/unsubscribe.
Drops the live member subscription (gossip topics + sync scope) while retaining
any coreHosted hosting obligation, so a core hosting a public CG can be driven
to the pure host-only state (subscribed=0, coreHosted=1). The devnet Phase D
test now manufactures that state via unsubscribe so the missed-publish gap can
only be filled by the chain reconcile sweep, not the gossip fast-path.

Tests: chain-reconcile-e2e asserts a same-graph reconcile dual-writes the root
label graph. Full agent suite (1116) green; agent + cli build clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
branarakic pushed a commit that referenced this pull request Jun 2, 2026
…tion

Brings in the four new review-fix commits other agents pushed to the
original PRs that the integration branch was missing:
  - #908 warm-core: closure in-flight guard at the interval site +
    staleThresholdMs/lastSeen freshness handling inside the module.
  - #910 chain reconcile: prefer subscribed CG in resolveLocalCgIdByOnChainId,
    reset watermark+cursor on ANY on-chain-id change.
  - #912 delta sync: trailing-only `|since|` parse + legacy batchId
    placement/literal-form tolerance.
  - #914 Phase D: numeric cleartext swmGraphId hint kept unless == on-chain id,
    `subscribed` exposed in cursors API, clearNodePidFiles helper.

Conflict-resolution decisions:
  - Dual-write: ADOPT the canonical #910 PERSIST approach
    (`dkg:keepRootCopyOnLabel` persisted to SWM meta + recovered via
    getKeepRootCopySignal) and DROP the integration branch's competing
    derive-based change, so there is a single reviewed implementation.
  - Warm-core: take the PR's design (module-level freshness + interval-site
    closure guard); drop the redundant class-field guard.
  - Kept net-new work not on any PR: unsubscribeFromContextGraph +
    /api/context-graph/unsubscribe, and the devnet Phase D test that
    MANUFACTURES a pure host-only victim via unsubscribe.
  - Realigned chain-reconcile-e2e dual-write test to the persist signal and
    added an absent-signal negative case.

Build + full agent (1124) + node-ui (1331) unit suites green. DO NOT merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
@branarakic
Copy link
Copy Markdown
Contributor Author

Superseded by #927, which collapses this stack into a single squash-merge against the latest main (all per-slice review fixes from this PR are folded in and re-validated). Closing to clean up the PR list; the branch is retained for history. See #927.

@branarakic branarakic closed this Jun 2, 2026
branarakic added a commit that referenced this pull request Jun 2, 2026
feat(agent/core): core-preferred sync + chain-driven VM reconciliation (supersedes #906/#908/#910/#911/#912/#914)
branarakic pushed a commit that referenced this pull request Jun 3, 2026
…-ups)

Follow-up to the #927 squash merge, closing the correctness + coverage
findings from the original stacked PRs (#906/#908/#910/#911/#912/#914)
that weren't carried into #927. Rebased onto main after #941 split the
DKGAgent god class into subsystem mixin holders; the dkg-agent.ts hunks
are relocated into their new homes (dkg-agent-swm-host.ts: #35/#13 +
trackCoreHostRecording; dkg-agent-cg-resolve.ts: #26 parser delegate;
dkg-agent-base.ts: coreHostRecordings field; dkg-agent.ts: #30 stop()
flush; dkg-agent-lifecycle.ts: tracked ACK pre-sign call site).

Correctness:
- #13 reorg-safety: getHeadBlock lets a transient RPC error throw instead
  of masquerading as "no chain"; reconciler holds the contiguous watermark
  until a real head is observed (+ head-fetch diagnostics).
- #35 access-policy existence: recordCoreHostedPublicCg resolves the policy
  via readLiveOnChainAccessPolicy (existence-gated) so an UNKNOWN CG no
  longer looks public(0). Fails closed.
- #30 coreHosted persistence is tracked + flushed in stop() instead of
  fire-and-forget.
- #14 sweep provenance: derive a chain-unique publication id from the KC UAL
  when txHash is empty on the chain-driven path.
- #15 snapshot discriminator: sort WorkspaceOperation candidates by URI so
  every node converges on the same op (per-root tokenId order tracked in #936).
- #8 warm-core unpin: only count successful unpins; retain peer on failure.

Test coverage:
- #26 parsePipeDelimitedSyncRequest extracted to request-build.ts as a pure
  parser with round-trip + misparse-guard tests.
- #27 requester-side sinceBatchId threading (sync-checkpoint-key +
  durable-sync-since-threading suites).
- #37 typed boolean keepRootCopyOnLabel recovery in chain-reconcile-e2e.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant