fix(rfc64): resume fresh durable Edge selections - #2041
Conversation
| trigger: 'periodic-reconciler'; | ||
| syncMode: 'always-on'; | ||
| rehydratedSelectionCount: number; | ||
| durableSelectionCount: number; |
There was a problem hiding this comment.
🔴 Bug: Evidence journal V1 field is renamed without a version or compatibility path
What's wrong
The diagnostics snapshot shape is an exported API and HTTP response. Renaming rehydratedSelectionCount to durableSelectionCount while leaving schemaVersion at 1 breaks existing V1 consumers and gives them no reliable way to distinguish old and new records.
Example
A V1 client polling GET /api/diagnostics/sync-coverage-evidence currently expects edge-reconciler-job entries to contain rehydratedSelectionCount when schemaVersion is 1. After this change the same schemaVersion=1 response omits that property and only includes durableSelectionCount, so the client reads undefined or rejects the entry without any version signal.
Suggested direction
Keep the old V1 property as an alias during the transition or introduce a new schema version and make consumers branch on it.
For Agents
In coverage-evidence-journal.ts and the devnet parser, either preserve rehydratedSelectionCount as a backward-compatible alias while adding durableSelectionCount, or bump the evidence schema/versioned codec and accept both shapes where V1 data can still appear. Add a contract test for the diagnostics response shape.
| * always-on member intent. Kept separate from startup rehydration diagnostics | ||
| * so post-boot persistence cannot redefine rehydration bookkeeping. | ||
| */ | ||
| protected readonly durableAlwaysOnEdgeIds = new Set<string>(); |
There was a problem hiding this comment.
🟡 Issue: The durable admission set adds a second mutable source of truth
What's wrong
The PR separates durable periodic admission from rehydration diagnostics conceptually, but implements it as a mutable sidecar on the agent. That sidecar has to stay synchronized with rehydrate, async save, async delete, bulk clear, and status reporting. This makes the lifecycle file harder to reason about because the durable-store truth is no longer represented by one canonical model; readers must audit every store mutation path to know whether periodic admission is current.
Example
A future path that writes ContextGraphSubscriptionRecord through persistContextGraphSubscriptionStrict or another store-write helper now also has to know whether to update durableAlwaysOnEdgeIds; the persistence projection itself does not carry or own that post-commit side effect.
Suggested direction
Push this behind a dedicated durable-subscription/admission projection instead of maintaining a raw Set from scattered lifecycle hooks. A cleaner shape would make the committed persistence projection produce the admission decision, then have one owner update/query the index used by periodic planning and diagnostics.
For Agents
Look at context-graph-subscription-policy.ts, persistContextGraphSubscription, rehydration, and bulk clear. Preserve the rule that only committed durable explicit Edge intent enters periodic admission, but move the admission-index update behind one persistence/projection boundary. Tests should cover startup restore, post-boot save, delete, clear, and failed-clear deactivation through that single boundary.
| * always-on intent. Includes startup-restored rows and successful post-boot | ||
| * writes; excludes on-demand, deactivated, and cleared selections. | ||
| */ | ||
| durableAlwaysOnEdgeIds?: string[]; |
There was a problem hiding this comment.
🟡 Issue: Live periodic admission should not be folded into rehydration status
What's wrong
This muddies a boundary the PR comments are trying to preserve. Rehydration status is a historical startup diagnostic, but the new field is a live mutable periodic-admission view backed by post-boot persistence. Putting both in the same interface makes the model less legible and increases the chance that later code treats live admission as rehydration bookkeeping or vice versa.
Example
completedAt and rehydratedAlwaysOnIds describe the startup wave, while durableAlwaysOnEdgeIds can change after post-boot saves and clears. A caller reading one ContextGraphSubscriptionRehydrationStatus object now has to remember which fields are startup diagnostics and which fields are live admission state.
Suggested direction
Keep ContextGraphSubscriptionRehydrationStatus as startup rehydration bookkeeping and move live durable Edge admission to a separate, explicitly named status surface. If it must be returned together for compatibility, nest it under a field that makes the different lifecycle clear instead of making it a peer of rehydratedAlwaysOnIds.
For Agents
Review the public/status model around ContextGraphSubscriptionRehydrationStatus and getContextGraphSubscriptionRehydrationStatus. Preserve existing startup diagnostics, but expose live Edge periodic admission through a separate status/getter or a clearly named nested model. Tests should assert that startup rehydration fields remain stable while live admission changes are reported through the new boundary.
User impact
An Edge operator can put selected Context Graph IDs in
config.json, keep broadsyncOnConnectEnabled=false, and have those selections enter the bounded periodic VM + SWM catch-up lane on the first cold boot. A restart is no longer required before selective synchronization begins.This does not enable sync for every public CG. Admission still requires all of the following:
On-demand selections remain process-local and excluded. The agents and ontology system graphs also remain outside this bounded Edge lane.
Why
The exact-head isolated testnet canary for the M1 stack exposed a first-boot gap. Five configured selections were persisted as explicit always-on subscriptions, but the periodic lane admitted only IDs restored from a previous process. After the first five-minute reconciler tick the fresh node remained at 0/5 and had created no sync job. Restarting would have hidden the defect by turning the same rows into rehydrated state.
Before
sequenceDiagram participant Operator participant Edge participant Store as Durable subscription store participant Timer as Periodic reconciler participant Peer as DKG peer Operator->>Edge: Configure selected CGs Edge->>Store: Persist explicit always-on rows Store-->>Edge: Save committed Timer->>Edge: First periodic tick Edge->>Edge: Check startup-rehydrated IDs only Edge-->>Timer: Empty scope Note over Edge,Peer: No VM or SWM catch-up until restartAfter
sequenceDiagram participant Operator participant Edge participant Store as Durable subscription store participant Timer as Periodic reconciler participant Peer as DKG peer Operator->>Edge: Configure selected CGs Edge->>Store: Persist explicit always-on rows Store-->>Edge: Save committed Edge->>Edge: Admit committed durable IDs Timer->>Edge: First periodic tick Edge->>Edge: Revalidate live subscribed/always-on state Edge->>Peer: Catch up selected CGs only (VM + SWM) Peer-->>Edge: Verified bounded resultsImplementation
durableAlwaysOnEdgeIdsruntime admission set, separate from startup rehydration/cap diagnostics.rehydratedAlwaysOnIdsstartup-only and expose the live durable set separately asdurableAlwaysOnEdgeIds.durableSelectionCountso the journal does not describe post-boot writes as rehydrated.Validation
Exact head:
82123c71da2166d91f74a292827ac3a99657fc39.pnpm --filter @origintrail-official/dkg-agent build: passed.rehydratedAlwaysOnIdscorrectly remained empty.trigger=periodic-reconciler,syncMode=always-on, anddurableSelectionCount=5.Testnet observation outside this PR's bounded claim
The first-boot admission behavior above passed. The full M1 distributed completeness gate did not pass yet: the 20 peer jobs ended incomplete because the cold Edge had no historical hash-to-numeric-CG binding, Core responses contained zero metadata/data for those requests, and SWM authorization therefore remained unconfirmed. That is being handled as a separate stacked fix rather than broadening this PR.
Stack base: #2040