Summary
When a receiver already holds a graph-scoped/rootless knowledge asset assertion in Shared Working Memory (SWM), and that same assertion later becomes finalized and materialized in Verifiable Memory (VM), the receiver keeps the old SWM graph instead of removing it.
The promotion itself succeeds, but the receiver ends up storing the same assertion in both tiers. Aggregate queries can therefore count the same data twice.
Impact
A successful SWM-to-VM promotion can leave receivers in an inconsistent state:
- curator/publisher:
0 SWM + 1 VM
- receiver:
1 SWM + 1 VM
- aggregate receiver query: duplicated entities/triples
This is persistent, not a short synchronization overlap.
Exact trigger
The problem occurs when all of the following are true:
- the KA is graph-scoped/rootless;
- the receiver already has the exact assertion in SWM;
- that same assertion is later confirmed and materialized in VM on the receiver.
It is easy to miss when testing a VM-only late joiner, using separate assets for SWM and VM, or observing only the publisher, because the publisher still cleans its own SWM copy.
Reproduction evidence
A clean same-asset SWM-to-VM run used one real KA containing 10 entities and 40 triples.
Before promotion:
- receiver:
1 SWM / 0 VM / 40 triples
- receiver content digest exactly matched the curator
After promotion:
- curator:
0 SWM / 1 VM / 40 triples
- receiver after about 13 seconds:
1 SWM / 1 VM / 80 triples
The receiver remained at 1 SWM / 1 VM / 80 triples on checks every 15 seconds for five minutes, confirming this was not a transient overlap.
Expected behavior
After the exact assertion is finalized successfully:
- matching stale SWM state is removed;
- VM contains one finalized copy;
- aggregate queries return the original 40 triples once.
If a newer SWM assertion arrived during finalization, it must be preserved.
Actual behavior
Receiver finalization writes the VM graph and metadata, but does not clean the matching SWM graph. The already-confirmed repair path also returns without cleaning an exact stale SWM copy.
Affected paths on current main:
packages/agent/src/finalization-handler.ts
- exact VM/metadata state returns
already-confirmed without SWM cleanup;
- the normal materialization update writes VM graph and metadata only;
- a comment defers SWM cleanup to the publisher-side per-KA writer lock.
packages/agent/src/dkg-agent-swm-host.ts
- the
already-confirmed result updates caches/telemetry and returns without receiver cleanup.
packages/publisher/src/dkg-publisher.ts
- publisher-side cleanup still exists, which explains why the curator is correct while receivers remain duplicated.
Confirmed history / root cause
This was not introduced by PR #1991; that PR changed scoped query/store scheduling and did not modify the receiver finalization or SWM cleanup paths.
The original graph-scoped finalization implementation in commit 5431468 cleaned receiver SWM state with dropGraph(swmGraph) in both the normal apply and already-confirmed paths.
Commit 2cfbaf1 (fix(agent): harden rootless recovery invariants) removed both receiver cleanup calls and changed the focused tests to expect SWM to remain. It entered testnet-canary through PR #1736 and was promoted to main through PR #1746.
The likely reason was to avoid deleting a newer SWM assertion that races with finalization. That protection is correct, but leaving the exact finalized assertion in SWM permanently is not.
Focused unit evidence
The current finalization test explicitly expects both tiers to retain data after the exact VM graph is written. A focused run of the relevant tests passes under current behavior:
pnpm --filter @origintrail-official/dkg-agent exec vitest run \
test/ka-graph-finalization-handler.test.ts \
--config vitest.unit.config.ts \
--reporter=verbose \
-t 'atomically replaces the exact VM graph|does not delete a newer SWM assertion'
Result: one file passed, two tests passed, 44 skipped. This confirms the duplicate is currently encoded as expected behavior, while the separate newer-assertion test correctly requires newer SWM state to survive.
Why the existing blackbox scenario missed it
The convergence scenario uses one asset for SWM and a different asset for VM, so it never exercises promotion of the same assertion. Its convergence check also does not fail on extra content unless the stricter exact condition is used.
The SWM harness work in dkg-blackbox-harness PR #6 verifies read-only SWM parity independently from VM differences, so it also does not cover same-asset SWM-to-VM promotion.
Proposed fix direction
Do not restore an unconditional dropGraph, because that can delete a newer SWM assertion.
Instead, receiver cleanup should:
- use the same per-KA SWM writer lock used by local and synchronized SWM writes;
- re-read the durable SWM head while holding that lock;
- verify it still represents the assertion that was just finalized (UAL/assertion version, operation/receipt identity, content digest or Merkle root as appropriate);
- atomically delete only the matching SWM graph and metadata;
- preserve the SWM state if it is newer or otherwise does not match.
Apply this cleanup after successful receiver promotion and in the already-confirmed repair path.
Acceptance criteria
Add regressions for all three cases:
Related issue
#1778 is related to rootless SWM behavior but is not a duplicate: it covers a missing assertion seal that blocks curator VM publication, while this issue occurs after publication/finalization succeeds and concerns stale receiver SWM retention.
Summary
When a receiver already holds a graph-scoped/rootless knowledge asset assertion in Shared Working Memory (SWM), and that same assertion later becomes finalized and materialized in Verifiable Memory (VM), the receiver keeps the old SWM graph instead of removing it.
The promotion itself succeeds, but the receiver ends up storing the same assertion in both tiers. Aggregate queries can therefore count the same data twice.
Impact
A successful SWM-to-VM promotion can leave receivers in an inconsistent state:
0 SWM + 1 VM1 SWM + 1 VMThis is persistent, not a short synchronization overlap.
Exact trigger
The problem occurs when all of the following are true:
It is easy to miss when testing a VM-only late joiner, using separate assets for SWM and VM, or observing only the publisher, because the publisher still cleans its own SWM copy.
Reproduction evidence
A clean same-asset SWM-to-VM run used one real KA containing 10 entities and 40 triples.
Before promotion:
1 SWM / 0 VM / 40 triplesAfter promotion:
0 SWM / 1 VM / 40 triples1 SWM / 1 VM / 80 triplesThe receiver remained at
1 SWM / 1 VM / 80 tripleson checks every 15 seconds for five minutes, confirming this was not a transient overlap.Expected behavior
After the exact assertion is finalized successfully:
If a newer SWM assertion arrived during finalization, it must be preserved.
Actual behavior
Receiver finalization writes the VM graph and metadata, but does not clean the matching SWM graph. The
already-confirmedrepair path also returns without cleaning an exact stale SWM copy.Affected paths on current
main:packages/agent/src/finalization-handler.tsalready-confirmedwithout SWM cleanup;packages/agent/src/dkg-agent-swm-host.tsalready-confirmedresult updates caches/telemetry and returns without receiver cleanup.packages/publisher/src/dkg-publisher.tsConfirmed history / root cause
This was not introduced by PR #1991; that PR changed scoped query/store scheduling and did not modify the receiver finalization or SWM cleanup paths.
The original graph-scoped finalization implementation in commit
5431468cleaned receiver SWM state withdropGraph(swmGraph)in both the normal apply and already-confirmed paths.Commit
2cfbaf1(fix(agent): harden rootless recovery invariants) removed both receiver cleanup calls and changed the focused tests to expect SWM to remain. It enteredtestnet-canarythrough PR #1736 and was promoted tomainthrough PR #1746.The likely reason was to avoid deleting a newer SWM assertion that races with finalization. That protection is correct, but leaving the exact finalized assertion in SWM permanently is not.
Focused unit evidence
The current finalization test explicitly expects both tiers to retain data after the exact VM graph is written. A focused run of the relevant tests passes under current behavior:
Result: one file passed, two tests passed, 44 skipped. This confirms the duplicate is currently encoded as expected behavior, while the separate newer-assertion test correctly requires newer SWM state to survive.
Why the existing blackbox scenario missed it
The convergence scenario uses one asset for SWM and a different asset for VM, so it never exercises promotion of the same assertion. Its convergence check also does not fail on extra content unless the stricter
exactcondition is used.The SWM harness work in
dkg-blackbox-harnessPR #6 verifies read-only SWM parity independently from VM differences, so it also does not cover same-asset SWM-to-VM promotion.Proposed fix direction
Do not restore an unconditional
dropGraph, because that can delete a newer SWM assertion.Instead, receiver cleanup should:
Apply this cleanup after successful receiver promotion and in the
already-confirmedrepair path.Acceptance criteria
Add regressions for all three cases:
0 SWM + 1 VMon the receiver.0 SWM + 1 VM.SWM=0,VM=N) and fails on extra content.Related issue
#1778 is related to rootless SWM behavior but is not a duplicate: it covers a missing assertion seal that blocks curator VM publication, while this issue occurs after publication/finalization succeeds and concerns stale receiver SWM retention.