Skip to content

perf(sync): bound selected SWM responder planning - #2283

Open
branarakic wants to merge 7 commits into
codex/rfc64-responder-parallel-lanesfrom
codex/rfc64-selected-swm-responder-scaling
Open

perf(sync): bound selected SWM responder planning#2283
branarakic wants to merge 7 commits into
codex/rfc64-responder-parallel-lanesfrom
codex/rfc64-selected-swm-responder-scaling

Conversation

@branarakic

Copy link
Copy Markdown
Contributor

User impact

A provider can serve large selected public context graphs without repeatedly scanning unrelated graphs or probing payload shapes that cannot contribute. Metadata is negotiated and paged by bytes, and selected-graph planning uses bounded subject and graph indexes.

Before

sequenceDiagram
    participant R as Receiver
    participant P as Provider
    participant S as Store
    R->>P: Catch up selected CG
    loop Metadata and data pages
        P->>S: Global graph discovery
        P->>S: Rootless payload probes
        P->>S: Rebuild broad plan
    end
    Note over R,P: Store work grows with unrelated local data
Loading

After

sequenceDiagram
    participant R as Receiver
    participant P as Provider
    participant I as Selected CG indexes
    participant S as Store
    R->>P: Catch up selected CG with byte budget
    P->>I: Resolve bounded subject and graph plan
    loop Byte-bounded pages
        P->>S: Read only planned selected graphs
        P-->>R: Bounded metadata or data page
    end
    Note over R,P: Work follows the selected CG, not the whole store
Loading

Safety

  • Page byte ceilings remain negotiated and enforced.
  • No global sync-all behavior is introduced.
  • Maintenance reads stay lower priority than already-admitted user sync work.
  • Empty and rootless cases remain fail-closed.

Validation

Focused byte-budget, metadata-ceiling, SWM-subgraph, and snapshot-cache suites cover the bounded path. The complete stack reached 500/500 SWM on Testnet without manual catch-up.

},
),
})
: [],

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: TTL SWM DATA no longer serves graph-backed snapshot payloads

What's wrong
The empty graph list avoids inventory scans, but it also empties the allowlist that the graph-backed snapshot planner uses. Modern rootless SWM operations with dkg:publicSnapshotGraph can still sync their metadata, but their payload graph is never returned, leaving cold receivers unable to materialize those assets.

Example
A fresh graph-scoped SWM operation has head metadata plus dkg:publicSnapshotGraph <G> and dkg:publicQuadsCount 3, with 3 rows stored in graph G and no legacy dkg:rootEntity. Through registerSyncHandler, the TTL DATA request now calls readSwmDataPage with graphList: []; graph-backed discovery skips the meta graph and the response is empty. Expected behavior is that the DATA phase returns the rows from G so the requester can count/digest-materialize the advertised graph-backed KA.

Suggested direction
Do not pass an empty inventory into the graph-backed path unless that path is also changed to discover/count the advertised snapshot graphs without graphSet. Alternatively, if graph-backed content is being moved to another phase, update the requester contract at the same time.

For Agents
Look at registerSyncHandler's SWM DATA branch and readFreshGraphBackedSwmSnapshotEntries. Preserve the no-full-inventory optimization for root-scoped legacy data, but still allow graph-backed snapshot metadata/graphs to be discovered and counted. Add a handler-level test for a publicSnapshotGraph operation with no rootEntity that proves the DATA response includes the immutable snapshot graph rows.

// index lookup (0.1s rather than 10s+ on the 500-KA canary corpus).
const branches = chunk.map((metaGraph) => `{
GRAPH <${assertSafeIri(metaGraph)}> {
?op <${DKG_ROOT_ENTITY}> ?root ;

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: Fresh root preflight admits non-operation metadata roots

What's wrong
The responder and requester now disagree on what makes a legacy SWM root valid. A stray or malicious metadata subject with rootEntity and publishedAt can cause the responder to transmit data that the requester will reject, wasting byte-budget pages and potentially exposing rows that were not backed by a valid workspace operation.

Example
Insert SWM metadata rows { subject: 'urn:not-an-op', predicate: dkg:rootEntity, object: 'urn:r' } and { subject: 'urn:not-an-op', predicate: dkg:publishedAt, object: freshDate }, but no rdf:type dkg:WorkspaceOperation, plus payload rows rooted at urn:r. The new planner selects urn:r and serves those payload rows; the requester verifier later treats the root as invalid and drops them. Expected behavior is for the responder plan to ignore the root just like the verifier does.

Suggested direction
Require rdf:type dkg:WorkspaceOperation in the preflight, or perform an equivalent validation before adding roots to freshRootsByMetaGraph.

For Agents
In buildFreshSwmDataGraphPlan, keep the cheap metadata preflight but make its root admission match sync-verify-worker-impl.ts: rootEntity should come from a fresh WorkspaceOperation in the same meta graph. Add a plan/handler test where rootEntity+publishedAt without WorkspaceOperation does not produce DATA rows.

expect(new TextEncoder().encode(first).byteLength)
.toBeLessThanOrEqual(SYNC_BYTE_BUDGET_RESPONSE_BYTES);

const second = await cap.invoke({ ...request, offset: firstRows.length });

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 byte-truncated continuation test does not verify snapshot retention

What's wrong
The test added for continuing a byte-truncated shared-memory meta page gives false confidence: because the store is unchanged between requests, it would pass even if the responder released the session snapshot after the short serialized page and rebuilt from live data on the second request.

Example
After first is read, delete one early row or insert a new row that sorts before the second-page offset, then fetch offset: firstRows.length. A responder that dropped the cached snapshot could skip or duplicate rows, but the current test does not exercise that failure mode.

Suggested direction
Capture the expected first-session row set before mutation, mutate the store between page requests, then assert page 2 continues that original row set and does not reflect the mutation.

For Agents
Strengthen packages/agent/test/sync-byte-budget-pages.test.ts in the byte-truncated SWM meta continuation case: mutate the meta graph between page 1 and page 2 and assert the combined pages are the original snapshot, with no duplicates or missing original rows. This should prove the releaseCacheOnShortPage behavior rather than just byte sizing.

);
const rows = await readSwmMetaPage({
store,
graphList: cutoff == null

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 use an empty graph list as a hidden planner mode

What's wrong
This adds a sentinel-based control path across the handler and graph planner. It makes the boundary harder to reason about because graphList no longer consistently means "the admitted graph inventory"; its meaning depends on phase and TTL state. That is exactly the kind of implicit mode that tends to become permanent debt as more SWM lanes are added.

Example
A reader of readSwmDataPage({ graphList: [] }) cannot tell whether the caller means "there are no graphs" or "the planner should discover candidates itself". The same required parameter is now an admission filter in legacy paths, ignored in TTL meta, and a discovery-mode flag in TTL data.

Suggested direction
Make graph discovery/admission an explicit model instead of overloading graphList. The code should say whether the planner owns candidate discovery or is filtering a known inventory, so each helper has one clear contract.

For Agents
Look at sync-handler.ts SWM meta/data calls and graph-plan.ts readSwmMetaPage/readSwmDataPage/buildFreshSwmDataGraphPlan. Preserve the no-full-inventory TTL behavior, but replace the empty-array sentinel with an explicit typed input such as graphInventory?: readonly string[] or candidateSource: 'registered-prefixes' | 'known-inventory'. Keep tests proving TTL rootless requests do not call listGraphs.

// META negotiates byte-budget paging on the wire (#1916/#1923). Durable
// meta is subject-atomic; SWM meta is reassembled and verified as one
// retained manifest. Both remain bounded by the common response serializer.
const usesMetaByteBudget = phase === 'meta' &&

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: Centralize byte-budget response policy instead of reimplementing it for META

What's wrong
The PR expands byte-budget paging from DATA into META, but the responder implementation does not extend the existing policy boundary. Instead it threads a second ad-hoc policy through the already busy handler. That increases branching complexity and makes the transport contract depend on several scattered conditionals staying aligned.

Example
Adding another negotiated page mode or another phase now requires keeping resolveDataRequestPolicy, usesMetaByteBudget, metaLimit, SWM meta serialization, durable meta serialization, and cache-release flags in sync by hand.

Suggested direction
Promote the byte-budget decision into a single policy abstraction that covers both DATA and META, rather than adding parallel META booleans and ternaries in the handler.

For Agents
Start in packages/agent/src/sync/responder/data-request-policy.ts and generalize it into a responder page policy for DATA and META. Have it return normalized hint rows, serializer mode, cache-release behavior, and durable-meta oversized policy. Then make sync-handler.ts consume that policy instead of recomputing phase-specific booleans in multiple branches. Existing byte-budget page tests should continue to pass unchanged.

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