Skip to content

fix(rfc64): resume fresh durable Edge selections - #2041

Open
branarakic wants to merge 2 commits into
codex/rfc64-m1-finalization-orderingfrom
codex/rfc64-m1-first-boot-periodic-scope
Open

fix(rfc64): resume fresh durable Edge selections#2041
branarakic wants to merge 2 commits into
codex/rfc64-m1-finalization-orderingfrom
codex/rfc64-m1-first-boot-periodic-scope

Conversation

@branarakic

@branarakic branarakic commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

User impact

An Edge operator can put selected Context Graph IDs in config.json, keep broad syncOnConnectEnabled=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:

  • Edge role;
  • an explicit subscription;
  • always-on intent whose durable store save completed successfully; and
  • a still-live subscribed/always-on state when the periodic round is planned.

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 restart
Loading

After

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 results
Loading

Implementation

  • Maintain a dedicated durableAlwaysOnEdgeIds runtime admission set, separate from startup rehydration/cap diagnostics.
  • Seed it from restored explicit Edge always-on rows and update it only after the canonical persistence projection commits an authoritative save/delete.
  • Remove cleared and failed-delete deactivated rows; clear partially accumulated admission fail-closed if startup rehydration fails.
  • Keep rehydratedAlwaysOnIds startup-only and expose the live durable set separately as durableAlwaysOnEdgeIds.
  • Rename Edge automatic evidence to durableSelectionCount so the journal does not describe post-boot writes as rehydrated.
  • Consolidate the Edge evidence fixture into one parameterized factory.

Validation

Exact head: 82123c71da2166d91f74a292827ac3a99657fc39.

  • GitHub: SPARQL scalability lint passed; Windows SQLite lifecycle passed; all review threads resolved.
  • pnpm --filter @origintrail-official/dkg-agent build: passed.
  • Full Agent lane: 166 files passed; 2,250 tests passed, 5 skipped, 0 failed.
  • Focused Agent evidence suite: 3 files, 33 passed, 0 failed.
  • M1 selective-coverage proof harness: 80 passed, 0 failed.
  • Exact remote full build: all 22 workspace tasks passed, including the UI build.
  • Fresh-home isolated testnet Edge booted from the exact head with no prior SQLite DB, Oxigraph data, agent key, auth state, network state, or RFC64 checkpoints.
  • Five explicit configured selections were durably admitted on that first boot while rehydratedAlwaysOnIds correctly remained empty.
  • The first automatic five-minute round created exactly 20 bounded jobs: five selected CGs across four Core peers. Every job carried trigger=periodic-reconciler, syncMode=always-on, and durableSelectionCount=5.
  • No agents or ontology system graph entered the automatic scope.
  • Successful clear and failed-delete deactivation tests both prove the periodic VM/SWM scope becomes empty afterward.

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

Comment thread packages/agent/src/dkg-agent-lifecycle.ts Outdated
Comment thread packages/agent/test/sync-coverage-evidence-edge-periodic.test.ts
Comment thread packages/agent/src/dkg-agent-lifecycle.ts Outdated
trigger: 'periodic-reconciler';
syncMode: 'always-on';
rehydratedSelectionCount: number;
durableSelectionCount: number;

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: 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>();

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: 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[];

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: 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.

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