Skip to content

Promote testnet-canary to main (10.0.9 line + fifa-class meta ceiling fix) - #1888

Merged
branarakic merged 94 commits into
mainfrom
release/promote-canary-to-main-20260721
Jul 21, 2026
Merged

Promote testnet-canary to main (10.0.9 line + fifa-class meta ceiling fix)#1888
branarakic merged 94 commits into
mainfrom
release/promote-canary-to-main-20260721

Conversation

@branarakic

Copy link
Copy Markdown
Contributor

Promotes the full testnet-canary line to main — 92 commits, zero conflicts (main was fully back-merged into canary via #1884/#1886 first, per release-cut convention).

What this brings to main

Verification

🤖 Generated with Claude Code

Zvonimir and others added 30 commits July 13, 2026 13:03
…leanup

# Conflicts:
#	packages/agent/test/messenger-substrate.test.ts
Promote exact main candidate to testnet-canary for certification.
chore(testnet): promote rootless sync recovery candidate
fix(outbox): avoid loading payloads for diagnostics
…net-canary

Promote DKG v10.0.7 release cut to testnet-canary
GH#1778 reported that a sealed rootless named KA shared member -> curator
over SWM is "unsealed" on the curator and cannot be published to VM, and
proposed adding the seal to the SWM wire protobuf.

Investigation (incl. executing the real durable-sync + seal modules)
showed the seal never rode SWM gossip in any version, so there is nothing
to restore there. The seal already reaches the curator via the durable
`_meta` sync lane. The real failure is two stacked off-chain defects, so
no wire/protobuf change (and no metadata mirror) is needed:

- Defect A: VM publish built the seal-lookup URI from the CALLER (curator)
  address, not the KA author. The seal sits at .../assertion/<member>/<name>;
  publish queried .../assertion/<curator>/<name>, found nothing, and reported
  "is not finalized" (surfaced as "unsealed"). Fix: resolveAssertionAuthor()
  reads the author from local `_meta` (prefer the caller's own KA; else the
  sole other author; else 409 AMBIGUOUS_ASSERTION_AUTHOR). The caller hint is
  the effective publish identity, so tokenless self-publish is unchanged. The
  route's existing "author cannot be supplied on vm/publish" 400-gate stays.

- Defect B: the durable-sync integrity filter stripped dkg:assertionVersion
  from a not-yet-published seal subject (13/14 quads), making
  parseAssertionSealQuads throw "Partial graph-scoped assertion seal" once
  Defect A was fixed. Fix: admit assertionVersion for a self-consistent seal
  subject (carries assertionMerkleRoot, contentScopeVersion=2, single kaUal
  whose author == single authorAddress == the /assertion/<addr>/ segment).
  The seal's 13 sibling quads already sync unauthenticated; the real integrity
  boundary is the publish-time Merkle recheck, which stays authoritative.

No receiver/wire/ACK changes; the receiver still writes no context-graph
`_meta`. Adds unit + resolution + adversarial durable-sync regression tests.
Also corrects two stale comments about `_meta` replication / seal transport.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on PR #1780:

- BUG (otReviewAgent 🔴): `opts.agentAddress` was reused as the #1778 caller
  hint, but for direct programmatic callers it is an AUTHORITATIVE author
  selector. A caller requesting an author with no seal could be silently
  served a different same-named author. Split the roles: `agentAddress` stays
  authoritative (no substitution); the daemon publish routes now pass
  `callerAgentAddress` (token/caller identity) and only that hint drives
  resolution. Extracted the shared `resolveFinalizedAssertionPublishAuthor`
  helper so the sync and async publish paths cannot drift (🟡).

- Centralised the assertion-coordinate shape in one core helper
  `parseContextGraphAssertionUri` (inverse of `contextGraphAssertionUri`),
  used by both the publish author resolver and the durable-sync seal identity
  check, replacing the bespoke prefix/suffix and regex parsing (🟡).

- Tests: explicit-agentAddress-not-substituted; async intent auto-resolution;
  route-level 409 `AMBIGUOUS_ASSERTION_AUTHOR` { candidates } for sync and
  async publish; core round-trip for the new parser. Updated two existing
  route tests to the callerAgentAddress contract.

core 1238 / agent 1120 green; CLI publish/route suites green (the remaining
Windows-only CLI failures are pre-existing Hardhat-context / native-binary /
unix-permission env issues, unrelated to this change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alisation

Follow-ups on PR #1780 (otReviewAgent round 2):

- BUG (🔴): `parseContextGraphAssertionUri` counted URI segments, but
  `validateContextGraphId` permits `/` (wallet-scoped CG ids like
  `0xabc…/project`), so such ids were mis-split into a subgraph and #1778
  author resolution fell back to "is not finalized". Reworked the parser to
  anchor on the `/assertion/<addr>/<name>` suffix from the RIGHT and return the
  raw `scope` (cgId or cgId/subGraph, not split — a slash-containing cgId
  cannot be separated from an optional subgraph by the URI alone). Consumers
  compare `scope` to the known cg. Added core round-trip tests for slash cgIds
  (with/without subgraph) and agent resolver + durable-sync tests.

- Moved the graph-seal self-consistency check into core as
  `graphScopedSealAuthor` (beside `ASSERTION_SEAL_PREDICATES` /
  `parseAssertionSealQuads`); durable-sync now calls it and no longer
  re-declares seal predicate constants (🟡).

- Extracted the store/URI/EVM author lookup out of the large publish mixin into
  a focused `finalized-assertion-author.ts` module; the mixin method delegates (🟡).

- Deduplicated the ambiguous-author 409 mapping behind
  `respondAmbiguousAssertionAuthor`, used by both publish routes; centralised
  the route caller-hint via `publishCallerHintLane` (🟡).

- Enforced the selector-vs-hint contract: `resolveFinalizedAssertionPublishAuthor`
  now rejects supplying both `agentAddress` and `callerAgentAddress`
  (PUBLISH_AUTHOR_SELECTION_CONFLICT) (🟡).

- Added subgraph author-resolution coverage (root/subgraph do not cross-match) (🟡).

core 1238 / agent 1124 green (relay.test.ts flakes only under full-suite load;
passes in isolation — unrelated to this change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ds, stronger test

Follow-ups on PR #1780 (otReviewAgent round 3):

- Suffix-cross-match test (🔴): the fixture used one author for both the short
  and long name, so an unanchored suffix match would still pass. Now MEMBER
  authors 'asset' and OTHER authors 'myasset', and the test asserts 'asset'
  resolves to MEMBER alone — an unanchored regression would make it ambiguous
  (observable), not silently green.

- Single seal parser (🟡): extracted `collectAssertionSealFields` in core;
  `parseAssertionSealQuads` (full validation) and `graphScopedSealAuthor`
  (durable-sync admission) now share one field collector with identical
  last-writer-wins semantics, so the seal vocabulary has one source of truth.

- Assertion-coordinate grammar (🟡): added core `contextGraphAssertionQueryBounds`
  (scope/prefix/suffix, derived from the same grammar as
  `contextGraphAssertionUri`); the resolver asks core for its query bounds and
  expected scope instead of encoding the URI layout in SPARQL string-building.

Not changed (deliberate): the ambiguous-author case keeps returning
`409 { AMBIGUOUS_ASSERTION_AUTHOR, candidates }` without an in-route foreign
author selector — the server-side-resolution / no-author-param design was an
explicit product decision; the candidate list is a diagnostic and the author's
own node can still self-publish. The discriminated-union refactor of the
pre-existing `agentAddress` option is declined in favour of the runtime
mutual-exclusion guard (see PR replies).

core 104 (seal+constants) / agent 1124 green; probe still 14/14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…losed admission

Follow-ups on PR #1780 (otReviewAgent round 4):

- BUG (🔴): the async publish intent carried only the resolved AUTHOR as
  `agentAddress`, so CG auto-registration on `CG_NOT_REGISTERED`
  (lifecycle.ts) registered under the member author instead of the operator
  who enqueued the job. Added `callerAgentAddress` to
  `KnowledgeAssetVmPublishRequest` (populated from the effective caller, kept
  OUT of the intentKey hash so it doesn't fork job dedup);
  `resolveFinalizedAssertionVmPublishIntent` sets it, and the async executor
  registers under `request.callerAgentAddress ?? agentAddress`. New CLI unit
  test proves registration uses the curator token while the author stays the
  member.

- Admission fail-closed (🟡): `graphScopedSealAuthor` no longer reuses the
  last-writer-wins field map (which silently collapsed a peer's conflicting
  `kaUal`/`authorAddress`). It now reads DISTINCT objects and requires exactly
  one per identity predicate, failing closed on ambiguity, while still reusing
  `ASSERTION_SEAL_PREDICATES` + the canonical literal decoders. Inlined the
  single-use collector back into `parseAssertionSealQuads` (unchanged behavior).
  New durable-sync test: two conflicting kaUals ⇒ assertionVersion dropped.

Not changed (deliberate, replied on-thread): the two-field author-selection
model keeps its runtime `PUBLISH_AUTHOR_SELECTION_CONFLICT` guard rather than a
discriminated-union rewrite of the pre-existing `agentAddress` option (out of
scope for this bugfix); the `_meta` author lookup keeps `parseContextGraphAssertionUri`
+ query-bounds rather than a separate matcher (the parser is still needed by
durable-sync, which has no expected coordinate).

core 104 (seal+constants) / agent 1125 / publisher 47 green; probe 14/14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Branimir Rakic and others added 22 commits July 21, 2026 00:27
… TTL meta plan (#1868 review)

FreshSwmMetaPlan is now a genuinely immutable pagination description
(deep-readonly graph/subject/count scalars). The mutable per-session
content-digest state that used to live on subject entries moves to a
sidecar WeakMap keyed by plan instance — exactly the binding's intended
lifetime: the memoized plan IS the session, a refreshed/rebuilt plan is a
new object with a fresh empty binding map, and evicting or expiring the
plan releases its digests with it. readFreshSwmMetaSubjectWindowRows is
the only writer. Placement only: same-count replacement failure semantics
are unchanged and every existing mutation test passes unmodified.

Also closes the two remaining #1868 round-2 coverage asks on the plan
budget thread, each proven by a killed mutant:

- time-based TTL expiry (controlled clock) prunes a plan AND releases its
  global budget charge, distinct from the maxEntries eviction the prior
  test covered — a mutant that leaks the charge on expiry passes the old
  test and is killed only by the new one;
- the plan cardinality cap binds in AGGREGATE across root and subgraph
  meta graphs — a mutant that resets the allowance per graph passes the
  single-graph cap test and is killed only by the new multi-graph test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ive recovery

Two review fixes for the batched exact VM reconciliation path:

- recoverVmReconcileBatch now requests at most MAX_EXACT_SYNC_ASSETS UALs
  per peer. A scan batch configured above the protocol cap
  (DKG_VM_RECONCILE_BATCH_SIZE > 10) previously produced a filter that
  requireExactAssetUals rejects, so every exact fetch threw and recovery
  was silently dead. Over-cap targets stay in `remaining` for a later
  peer or pass; fetch telemetry and revalidation (which costs chain
  reads per ordinal) are restricted to the requested slice.

- The batch fetch is now gated on the per-CG active-fetch cooldown. The
  batched path deliberately skips the per-UAL negative cache (consulting
  it primes connections to every discovered agent), which left no damper
  at all: a CG with permanently unavailable KAs ran a 3-peer,
  priority-1000 exact sync on every reconcile pass. An unproductive
  batch now costs one bounded fetch per sweep interval. Mirroring the
  inline path, progress or an unreachable network clears the cooldown,
  so a draining backlog still proceeds slice after slice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t-sync test coverage

Review blockers (lupuszr):

- Re-check the captured local-CG -> on-chain-CG binding immediately after
  recoverPendingOrdinals resolves. Recovery is the longest await in a
  reconcile pass; a rebind landing during it could let outcomes recovered
  under the old binding advance or persist cursor state for the rebound
  CG. Staleness detected there is now treated exactly like staleness
  during ordinal work: outcomes discarded, no watermark move, scan
  cursor reset.

- Gate every exact-recovery peer through ensurePeerAdmittedForRecovery
  before sending. Curator hints and getConnections() both predate the
  network-identity probe, so a merely-connected peer is not necessarily
  admitted; the batch path could previously send authenticated exact
  requests to unverified or rejected-network peers.

- Contain ordinal-worker failures. The pool used fail-fast Promise.all,
  so one rejecting ordinal left sibling workers running past the pass's
  lifetime, overlapping their network/store side effects with the
  caller's retry. The first error now stops dispatch across all
  workers, in-flight ordinals drain, and only then does the pass reject.

Coverage (review-bot asks):

- runDurableSync integration: exactAssetUalsFor reaches both fetch
  phases and an old-responder full-CG payload is filtered before the
  verification worker sees it.
- Page-fetch coalescing + durable single-flight identity now covered
  for assetUals (different batches never share a run; identical
  batches do).
- parseSyncRequest wire tests for both formats: valid filters survive,
  present-but-invalid fail closed to [], absent stays undefined.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c-v2

fix(sync): batched exact VM reconciliation (#1871) + lazy on-chain probe (#1852)
# Conflicts:
#	packages/agent/vitest.unit.config.ts
* Merge pull request #1846 from OriginTrail/fix/1836-publisher-maxretries-control-plane

fix(publisher): honor publisher.maxRetries for API- and agent-admitted lift jobs (#1836)

* Merge pull request #1851 from OriginTrail/fix/publisher-broadcast-durability

fix(publisher): fsync the broadcast record before send to prevent crash-recovery double-submit

* Merge pull request #1849 from OriginTrail/fix/1828-intent-key-lookup

feat(publisher): exact intent recovery lookup for lost async-publish admissions (#1828)

---------

Co-authored-by: Jurij89 <138491694+Jurij89@users.noreply.github.com>
Co-authored-by: Zvonimir <zvonimir@MacBook-Pro-2.local>
…1875)

* feat(publisher): append-only admission journal — type, serialize, appendJournal hook (#1829 chunks 1-3)

Node-local append-only journal for the named-KA lift-publish path (epic PR3).

- AdmissionJournalEntry + JournalKind union (lift-job-types.ts); free journal* graph
  and predicate namespace + serialize/parse with a hashed, zero-padded entry subject
  and xsd:integer seq (async-lift-control-plane.ts).
- appendJournal hooked into writeJob(job, kind), DEFENSIVE by construction (mirrors
  serializeVmPublishIntentIndex): no-ops unless journalWrites is enabled (daemon-only),
  for the 'rollback-noop' sentinel, and for non-named-KA jobs; derives lineageKey via
  the U+001F-guarded key helper in try/catch and skips a legacy delimiter-bearing job;
  swallows any store/allocation error so the journal never fail-closes the authoritative
  state machine. Per-lineageKey MAX(seq)+1 allocation under a dedicated journal mutex
  (separate from withClaimLock; lock order claim->journal, no reentrancy).
- All 21 writeJob call sites pass an explicit JournalKind; the #1851 broadcast rollback
  passes 'rollback-noop' so it never appends a spurious 'validated' entry.

Tests: serialize/parse round-trip + xsd:integer seq + stable subject (6); append
lifecycle — contiguous per-lineage seq, correct kinds, daemon-only gate, named-KA
scope, and a legacy U+001F job neither throws out of writeJob nor is journaled (5).
Publisher tsc clean; #1849 intent-lookup suite still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(publisher): journal reads + route + chain-reset/daemon-only wiring (#1829 chunks 4-6)

- Reads: facts-pure readJournalByIntent (derives lineageKey from retained facts, never
  the ephemeral #1828 index, so it resolves after clear/cancel) + readJournalByJob, on a
  segregated VmPublishAdmissionJournalReader interface. JournalReadResult carries
  {entries, maxSeq, complete, txHashes}; txHashes are ATTEMPTED submissions (reconcile vs
  chain, never "sent"); complete = no seq gap (authoritative on oxigraph, best-effort on
  external SPARQL backends per the durability note).
- Route GET /api/publisher/journal (by jobId, or facts-pure by lifecycle identity, reusing
  admission's normalization + empty-subGraphName + control-char guards) + api-client
  publisherJournal().
- chain-reset-wipe now wipes all urn:dkg:publisher:* graphs (journal + the pre-existing
  control-plane/wallet-locks gap on external backends).
- Daemon-only journalWrites: enabled on createPublisherControlFromStore + the daemon
  runtime (createPublisherRuntimeFromAgent); OFF for the CLI inspector + standalone
  `dkg publisher run` so a second process never races the node-local per-lineageKey seq.

Tests: AC4 immutability (cancel/clear leave the journal intact); facts-pure reads incl.
post-clear resolution; the flush-fail rollback appends broadcast + failed but NO duplicate
'validated' (mutation-checked: reusing 'validated' instead of the 'rollback-noop' sentinel
fails it); journal route by-jobId/by-facts/empty/400s. Publisher tsc clean.

Note: the journal is NODE-LOCAL — the sync responder serves only did:dkg:context-graph:*,
so urn:dkg:publisher:* is structurally excluded (confirmed at graph-plan.ts). A dedicated
cross-package sync-exclusion regression test is deferred (createAdmissionContext is private).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(publisher): address #1875 local-review findings (journal read/route/exports)

- 🔴 summarizeJournal reported spurious complete:false / wrong maxSeq on SUBSET reads
  (readJournalByJob, intentKey-filtered readJournalByIntent): a successor job continues
  the lineage seq, so its slice never starts at 0. Now maxSeq/complete are computed over
  the full LINEAGE (both callers resolve it), while entries/txHashes stay the queried
  subset. Regression: a successor-job read reports the lineage complete (was spuriously
  incomplete).
- 🟡 publisherJournal() now returns the exported JournalReadResult instead of a lossy
  hand-rolled subset that dropped blockNumber/ual/failureCode and widened kind to string.
- 🟡 extracted parsePublisherLifecycleFactsFromQuery shared by GET /job-by-intent and
  GET /journal, removing the duplicated facts-validation block and its regex drift
  (control-char check is now charCodeAt-based).
- 🟡 export AdmissionJournalEntry + JournalKind + PersistedJournalKind alongside
  JournalReadResult so the public surface is self-contained.

Publisher tsc clean; publisher journal suite 24/24 (incl. successor-completeness
regression); cli journal route 5/5; cli my-file tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssionless TTL paging as best-effort

Review cleanup (branarakic): the regenerated evm-module deployment artifact
leaked into the composition — 289 lines of dev-deploy churn pointing the
recorded deployment at an unrelated commit and downgrading the recorded
PublishingConviction version. Restored byte-identical to the base. This
repo's known churn trap; caught for the third time this cycle.

Also addresses review observation 1: a doc comment on createSessionPlanGetter
stating plainly that sessionless TTL paging is best-effort and the digest
guard does not cover it, so the guard is not later mistaken for offset-page
consistency protection it does not provide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(sync): remove 64k _meta ceiling + materialize catch-up snapshots (fifa-class CGs)
…cast durability)

Hand-resolved the same 11 publisher/cli files as the canary->main direction:
canary's #1875 journal was built atop #1849/#1851's lineage and is a strict
superset in every hunk (identical five durability test bodies plus one journal
test; parsePublisherLifecycleFactsFromQuery carries all of #1849's inline
route checks). Resolution reuses that verified union tree.
Merge main into testnet-canary (#1849 intent-key lookup + #1851 broadcast durability)
},
): Promise<KnowledgeAssetVmPublishRequest> {
const agentAddress = opts?.agentAddress ?? this.defaultAgentAddress ?? this.peerId;
const agentAddress = await this.resolveFinalizedAssertionPublishAuthor(contextGraphId, name, opts);

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: Intent recovery loses curator-published member jobs

What's wrong
The change correctly separates caller from author for publishing, but the recovery API still assumes the authenticated caller is the lifecycle agentAddress when the client omits that field. That assumption is false for the new curator-publishes-member flow, so the durable admission recovery and facts-based journal can miss exactly the jobs this PR now supports.

Example
Curator 0xCurator publishes member 0xMember's shared KA through publish-async. The admitted job is keyed by (contextGraphId, name, subGraphName, 0xMember). If the 202 response is lost, the curator retries recovery with GET /api/publisher/job-by-intent?contextGraphId=cg&name=asset using their token; the route fills agentAddress=0xCurator, derives a different lifecycle key, and returns none even though the job exists.

Suggested direction
Keep the new resolved-author publish behavior, but make retained-facts recovery derive the same author before building the lifecycle key, or return an explicit recovery token/facts shape that includes the resolved author in every admission response and caller flow.

For Agents
Update the publisher intent/journal lookup path to mirror publish author resolution for omitted agentAddress: preserve explicit agentAddress as an author selector, but when omitted resolve the finalized assertion author from contextGraphId, name, optional subGraphName, and the caller as a hint. Add a regression where a curator enqueues async VM publish for a member-shared KA, then /api/publisher/job-by-intent and /api/publisher/journal without agentAddress find the member-keyed job/lineage.

if (outcome === 'no-swm') {
swmState = await this.collectVmReconcileSwmCandidateState(localCgId);
if (outcome === 'no-swm' || outcome === 'verified-vm-metadata-pending') {
if (options.deferActiveFetch) {

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: Metadata-pending recovery is not tested through the production batch path

What's wrong
The change is meant to fix the reconcile state called out in the verification notes, but the tests do not exercise the path production now uses for that condition. This leaves the exact watermark-stuck regression underverified.

Example
A regression that keeps inline fetching for verified-vm-metadata-pending but returns plain { status: 'pending' } when deferActiveFetch: true would leave the production watermark stuck, while the current metadata-pending and batch tests still pass.

Suggested direction
Add a regression test for verified-vm-metadata-pending in the deferred/batched reconciler path, not only the legacy inline fetch path.

For Agents
In packages/agent/test/core-fills-gap.test.ts, add a runVmReconcileForCg or createVmReconcileDeps test that uses the real reconcileChainOrdinal with handleChainReconciledKC returning verified-vm-metadata-pending, asserts recoverVmReconcileBatch receives a target with that reason, and proves the recovered outcome advances the watermark.

* missed the live gossip stayed empty forever ("0 data + N meta triples").
* Absent entirely => materialization is skipped (never half-applied).
*/
snapshotMaterializer?: SharedMemorySnapshotMaterializer;

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: Model snapshot materialization as one dependency, not two loose optionals

What's wrong
The new comment says materialization needs “ONE cohesive dependency”, but the interface still exposes the materializer and snapshot store independently. That keeps the half-configured mode alive at the type boundary and forces readers to discover the real invariant inside the control flow.

Example
A test or alternate runtime can pass publicSnapshotStore but forget snapshotMaterializer; TypeScript accepts it, runSharedMemorySync still runs, and public snapshot materialization is quietly skipped.

Suggested direction
Make the type boundary express the invariant the comment already describes: materialization is either fully configured or absent. Avoid a half-configured state that compiles and degrades silently.

For Agents
In packages/agent/src/sync/requester/shared-memory-sync.ts, replace the two independent optionals with one cohesive option, for example snapshotRecovery?: { materializer: SharedMemorySnapshotMaterializer; publicSnapshotStore: WorkspacePublicSnapshotStore }, and update production/tests to either supply both or omit the whole feature.

.filter((row) => row.s && row.p && row.o && row.g);
}

const FRESH_SWM_META_PLAN_SUBJECT_CHUNK = 100;

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: Extract the new TTL SWM meta planner out of the monolithic graph planner

What's wrong
This PR drops almost a thousand net lines into a file that was already nearly 3k lines. The new code is not a small extension; it is a distinct pagination subsystem with its own state model and budget semantics. Keeping it embedded makes the responder planner harder to scan and makes future changes to unrelated lanes riskier.

Example
To reason about TTL SWM meta paging now, a reader has to jump between the new plan types near the top of the file, the shared memo helper, readSwmMetaPage, and several hundred lines of fresh-meta implementation near line 2454.

Suggested direction
Use the self-contained nature of this change as the extraction boundary. Moving the new planner into its own module would delete a large amount of local coupling from graph-plan.ts without changing behavior.

For Agents
Extract the TTL-filtered SWM meta plan into a focused responder module, such as fresh-swm-meta-plan.ts, with a small exported readFreshSwmMetaPage/memo factory. Keep graph-plan.ts as orchestration over graph-plan lanes, and preserve the existing query and budget behavior with the current tests.

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: Split the TTL SWM meta planner out of graph-plan.ts

What's wrong
The PR adds almost a thousand net lines of a distinct subsystem into an already monolithic responder file. The implementation may be carefully reasoned, but the structure makes unrelated responder concerns harder to scan and makes future changes riskier because every paging mode now shares one oversized module.

Example
To understand the new readSwmMetaPage path, a reader has to chase FreshSwmMetaPlan, freshSwmMetaSessionDigests, createSessionPlanGetter, readFreshSwmMetaSubjects, buildFreshSwmMetaPlan, readFreshSwmMetaRowsPageFromPlan, and readBoundedFreshSwmMetaSnapshot inside the same 3.7k-line module.

Suggested direction
Move this feature’s planner and paging machinery behind a dedicated module boundary instead of growing the already-large responder graph planner.

For Agents
Extract the new TTL SWM meta planner into a focused responder module, for example sync/responder/swm-meta-plan.ts, including the plan types, digest sidecar, discovery/count/window readers, bounded snapshot fallback, and memo factory. Leave graph-plan.ts as a thin dispatcher/export surface and preserve the existing tests against the extracted functions.

*/
recovery?: boolean;
buildSyncRequest: (contextGraphId: string, offset: number, limit: number, includeSharedMemory: boolean, remotePeerId: string, phase?: SyncPhase, snapshotRef?: string, sinceBatchId?: string, syncSessionId?: string, recovery?: boolean) => Promise<Uint8Array>;
buildSyncRequest: (contextGraphId: string, offset: number, limit: number, includeSharedMemory: boolean, remotePeerId: string, phase?: SyncPhase, snapshotRef?: string, sinceBatchId?: string, syncSessionId?: string, recovery?: boolean, assetUals?: string[]) => Promise<Uint8Array>;

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: Stop extending the sync request path with more positional optionals

What's wrong
The exact-asset feature is threaded through the sync stack by appending another optional positional argument to an already-long function type. This makes the sync boundary brittle: the identity fields that define a page request are spread across ordered parameters instead of one explicit model.

Example
A call that needs assetUals must pass through snapshotRef, sinceBatchId, syncSessionId, and recovery in the exact order, often as undefined placeholders. That is the same class of positional-control-flag coupling this PR elsewhere works to remove.

Suggested direction
Reframe this boundary around a typed parameter object or a SyncPageRequest model so new selection/session fields are named at every call site and cannot drift by argument order.

Confidence note
The current call sites appear updated, but the concern is maintainability of the widened boundary rather than an observed behavior failure.

For Agents
Introduce a request object for sync page construction, for example buildSyncRequest({ contextGraphId, offset, limit, includeSharedMemory, remotePeerId, phase, snapshotRef, sinceBatchId, syncSessionId, recovery, assetUals }), and thread that object through fetchSyncPages/agent adapters. Keep existing wire encoding unchanged.


if (decoded.senderKeyMessage) {
if (!requiresEncryptedPayload) {
if (!supportsEncryptedPayload) {

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: Encrypted public-gated SWM acceptance is only verified at the helper level

What's wrong
The production behavior changed from a single “requires encryption” decision to separate “requires” and “supports” decisions. The test suite validates that split in the pure policy helper, but it does not prove the handler actually accepts encrypted payloads in the public+agent-gated skew case.

Example
If this branch were accidentally changed back to if (!requiresEncryptedPayload), the pure helper test would still pass and the plaintext handler test would still pass, but an encrypted Sender-Key write for a public+agent-gated CG with publicAccessPolicyOnChainOracle returning true would be rejected.

Suggested direction
Cover the real handler encrypted/Sender-Key branch for requiresEncryptedPayload=false and supportsEncryptedPayload=true.

Confidence note
I found helper-level assertions for encrypted support and handler-level assertions for plaintext public-gated writes, but not a handler-level encrypted/Sender-Key public-gated apply case in the changed tests.

For Agents
Add a SharedMemoryHandler test in packages/publisher/test/swm-public-gated-plaintext-accept.test.ts that builds a public+agent-gated CG, provides a public-on-chain oracle returning true, sends a decryptable Sender-Key or legacy encrypted payload, and asserts the handler applies it instead of rejecting it as unsupported.

const peerIds = [...new Set([
...curatorPeerIds.filter((peerId) => connectedPeerIds.has(peerId)),
...orderedConnectedPeerIds,
])].slice(0, 3);

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: Exact VM recovery can permanently skip peers after the first three

What's wrong
The new batched VM recovery replaces the old rotating catch-up path with a fixed first-three peer slice. Because the ordering is stable and there is no exact-recovery rotation cursor, any peer after that slice is never tried on later sweeps. That leaves chain reconciliation stuck for valid network states where only a later connected peer has the missing KA.

Example
With five admitted connected peers, if peers 1-3 do not have the requested VM snapshot and peer 4 does, each reconcile sweep retries only peers 1-3. The exact fetch returns no materialized asset, the ordinal remains pending, and the watermark never advances even though a connected peer could serve it.

Suggested direction
Preserve bounded work per pass, but make the peer window rotate or otherwise prove that all admitted connected peers are eventually attempted before treating the recovery as unproductive.

For Agents
In recoverVmReconcileBatch, keep the per-pass cap if needed but rotate across the full admitted connected peer set, for example by reusing selectCatchupPeerWindow with a peerRotationKey or by adding an exact-recovery cursor. Add a test where the first three peers return empty and a later peer serves the requested UAL; the reconcile pass should eventually clear the pending ordinal.

// A raw-lift enqueue has no VM-publish lifecycle key -> appendJournal early-returns.
// (Exercised indirectly: only VM-publish requests carry knowledgeAssetVmPublish.)
const publisher = createPublisher();
await publisher.enqueueKnowledgeAssetVmPublish(kaVmPublishRequest());

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: Raw-lift journal test exercises a VM-publish job instead

What's wrong
This test gives false confidence for the new journal skip path. It claims to verify raw-lift jobs are not journaled, but it only enqueues a named KA VM publish job and then asserts that job produced one journal row.

Example
If the non-KA guard in appendJournal were removed or broken, this test would still pass because it only observes the one VM-publish admission entry it created itself, not a raw-lift transition.

Suggested direction
Make the test construct and process an actual jobType: 'lift' job so it proves non-named-KA journal writes are skipped.

For Agents
Replace this case with a real legacy raw-lift job, for example using the existing raw-lift test seeder, with journalWrites: true. Drive a transition that calls writeJob, assert it does not throw, and assert no journal entry is written for that raw-lift job.

if (!validateOptionalSubGraphName(rawSubGraphName, res)) return null;
const subGraphName = rawSubGraphName ?? undefined;
const explicitAgentAddress = url.searchParams.get("agentAddress")?.trim() || undefined;
const agentAddress = explicitAgentAddress ?? requestAgentAddress;

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: Journal lookup defaults to the caller even though jobs are keyed by author

What's wrong
The new caller/author split means async publish jobs and journal entries are keyed by the resolved KA author, not necessarily the token holder. The route parser still defaults missing agentAddress to the request caller, so the read APIs miss the exact curator-publishes-member flow this PR adds support for.

Example
A curator 0x11... enqueues async VM publish for a member-authored KA resolved as agentAddress=0x22... and callerAgentAddress=0x11.... Later, the curator calls /api/publisher/journal?contextGraphId=cg&name=asset without agentAddress. The route looks up the lineage under 0x11..., while the journal entry was stored under 0x22..., so it returns an empty result despite the job existing.

Suggested direction
Do not silently use the caller as the lifecycle author for these recovery reads. Resolve the finalized assertion author, query the author lane, or return an ambiguity error that tells clients to provide the author address.

For Agents
Update the /job-by-intent and /journal facts path so omitted agentAddress resolves the same author used at admission, or require the author selector explicitly when it cannot be resolved. Preserve the caller/author split and add a curator-publishes-member async test that queries the journal without agentAddress.

}

private async writeJob(job: LiftJob): Promise<void> {
private async writeJob(job: LiftJob, kind: JournalKind): Promise<void> {

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 scatter journal kind literals through every state write

What's wrong
Adding the journal by changing the low-level persistence method forces many unrelated branches to know the journal taxonomy. That spreads a cross-cutting concern through the state machine and makes future transitions easy to misclassify or forget.

Example
recordPublishResult advances one job through broadcast, included, and finalized by pairing each mergeJob(...) with a separate writeJob(next, '...') literal. The state transition and its journal classification are now two parallel facts that must remain synchronized by hand.

Suggested direction
Collapse the journaling taxonomy into the publisher state-transition abstraction so callers persist a transition, not a job plus a manually synchronized journal label.

Confidence note
This is a structural maintainability concern, not a claim that the current journal kinds are behaviorally wrong.

For Agents
Centralize journal classification around the state-transition boundary. One direction: introduce a persistTransition helper that derives the default persisted kind from the transition and accepts a small explicit override only for exceptional cases like recovery reset or rollback no-op. Make the no-op sentinel unrepresentable inside buildJournalEntry rather than cast away the type.

"test/generic-sql-source.test.ts",
"test/imported-artifact.test.ts",
"test/publish-finalized-agent-lane.test.ts",
"test/publish-foreign-author-resolution.test.ts",

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: Public metadata proof tests are not wired into the unit suite

What's wrong
The PR adds dedicated tests for the new public metadata proof and repair behavior, but the selective unit config does not execute them. This leaves the most direct validation out of the standard unit test lane.

Example
Running the normal agent unit suite via pnpm --filter @origintrail-official/dkg-agent test:unit will not select the new public metadata proof/repair tests, so a regression in the canonical public proof query or creator-owned repair can still pass that lane.

Suggested direction
Include the new public metadata tests in the selective unit config so standard CI validates the new proof and repair paths.

For Agents
Add test/context-graph-public-meta-proof.test.ts, test/context-graph-public-meta-repair.test.ts, and test/context-graph-public-meta-repair-http.test.ts to packages/agent/vitest.unit.config.ts, or move equivalent assertions into included files. Verify with the standard agent test:unit command.

@branarakic

Copy link
Copy Markdown
Contributor Author

Adversarially verified each otReviewAgent finding against the promote head (6 independent code-trace passes). Verdicts:

Finding Verdict Real severity
🔴 Intent recovery loses curator-published member jobs CONFIRMED Real bug, non-blocking — bounded to recovery/journal visibility. Admission keys the lifecycle lane by the resolved MEMBER author (dkg-agent-publish.ts:4201, by design per bc0c4ea), while /job-by-intent + /journal default an omitted agentAddress to the authenticated caller → false none for the curator flow. No double-publish is possible (re-enqueue dedups to the same member-lane jobId) and explicit ?agentAddress=<member> is a full workaround.
🔴 Metadata-pending not tested through batch path CONFIRMED as test gap only — production path traced correct: dkg-agent-swm-host.ts:3891-3910 emits the recovery target under deferActiveFetch:true and recoverVmReconcileBatch fetches metadata. But no test in the repo calls the real reconcileChainOrdinal with deferActiveFetch:true — all batch tests stub it with reason no-swm.
🟡 Two loose optionals (materializer/store) PARTIALLY CONFIRMED — type looseness real, but the dangerous half-config is unreachable from the single production call site, the reachable half throws loudly, and the digest-bound guard self-heals. Type-hygiene follow-up.
🟡 Extract TTL planner from graph-plan.ts CONFIRMED — numbers verified (2906 → 3692 lines; ~700-line self-contained lane). Behavior-preserving extraction is straightforward.
🟡 Positional optionals in sync path CONFIRMED — worse than stated: assetUals is the 11th positional param, three adjacent same-typed optional strings would transpose silently, and it sits at different positions in four parallel signatures. Same bug class as the fallbackOnPerSnapshotBudget positional bug behind #1847.
🟡 Handler-level encrypted public-gated test missing CONFIRMED test gap — the supportsEncryptedPayload branches (workspace-handler.ts:1077, :1113) have no handler-level encrypted apply test; the suggested mutation would survive the suite.

Disposition: none of these block the promote — every line is already merged to testnet-canary and running on the testnet fleet; holding this PR protects nothing. All six are queued as follow-up PRs against main after promotion: (1) recovery-route author fallback + resolved author in the 202 response + curator/member recovery test, (2) deferActiveFetch:true regression test, (3) cohesive snapshot-recovery dep, (4) swm-meta-plan.ts extraction, (5) SyncPageRequest object, (6) handler-level encrypted apply test.

🤖 Generated with Claude Code

@branarakic
branarakic merged commit 4c779c1 into main Jul 21, 2026
65 checks passed
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.

6 participants