Promote testnet-canary to main (10.0.9 line + fifa-class meta ceiling fix) - #1888
Conversation
…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>
… 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>
# 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>
… cli conflicts
…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.
CG subscribe fix
| }, | ||
| ): Promise<KnowledgeAssetVmPublishRequest> { | ||
| const agentAddress = opts?.agentAddress ?? this.defaultAgentAddress ?? this.peerId; | ||
| const agentAddress = await this.resolveFinalizedAssertionPublishAuthor(contextGraphId, name, opts); |
There was a problem hiding this comment.
🔴 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) { |
There was a problem hiding this comment.
🔴 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; |
There was a problem hiding this comment.
🟡 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; |
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
🟡 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>; |
There was a problem hiding this comment.
🟡 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) { |
There was a problem hiding this comment.
🟡 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.
feat: add Node UI metrics collection toggle
| const peerIds = [...new Set([ | ||
| ...curatorPeerIds.filter((peerId) => connectedPeerIds.has(peerId)), | ||
| ...orderedConnectedPeerIds, | ||
| ])].slice(0, 3); |
There was a problem hiding this comment.
🔴 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()); |
There was a problem hiding this comment.
🔴 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; |
There was a problem hiding this comment.
🟡 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> { |
There was a problem hiding this comment.
🟡 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", |
There was a problem hiding this comment.
🟡 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.
|
Adversarially verified each otReviewAgent finding against the promote head (6 independent code-trace passes). Verdicts:
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) 🤖 Generated with Claude Code |
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
_metasnapshot ceiling — bounded session-plan lane with per-subject count + sha-256 digest page verification; hold-out node reconstructs the full corpus (100/100 quads)devnet/public-cg-sync-proof/) used to gate all of the aboveVerification
🤖 Generated with Claude Code