# Changelog

All notable changes to the DKG V10 node are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [10.0.5] - 2026-07-08

Node stability and sync hardening on top of 10.0.4, plus contract updates. Ends the oversize-literal retry loop that could pin sync CPU, keeps the `agents/_meta` graph off the sync plane (reduces sync fan-out load), gates node-UI metric store-scans on consumer presence, and repairs the 10.0.4 npm tarball, which shipped without its `network/*.json` overlays and `project.json` (npm-installed nodes were falling back to built-in defaults). Also ships updated PublishingConviction 10.0.8 contracts (sources + ABIs); the on-chain deploy is a separate, coordinated activity (see **Deployment**). Content is identical to the `10.0.5-canary.1` testnet build (commit `6a91a16d`).

### Added

- **PublishingConviction 10.0.8 contract updates** — updated contract sources and regenerated ABIs (#1524). The runtime contract surface (`packages/evm-module/abi/*.json`) is updated; nodes resolve `PublishingConviction` from the Hub per call and hot-swap the new logic with no restart, so 10.0.4 and 10.0.5 nodes both interoperate with either the 10.0.7 or 10.0.8 contract.

### Fixed

- **Oversize-literal retry loop fixed** (#1529, #1530, #1532; OT-RFC-56 Fixes 2 & 3): an ingest-side guard rejects oversized literals and tombstones the offending assertion instead of re-fetching it repeatedly, a boot-time sweep clears graphs affected before the guard existed, and producer-side guards stop a node from emitting oversized literals in the first place.
- **`agents/_meta` kept off the sync plane** (#1526, follow-up #1533): the per-agent `_meta` graph is no longer carried on the sync/catch-up path (reduces sync fan-out load), with retention behaviour corrected in the post-merge review follow-up.
- **Node-UI metric store-scans gated on consumer presence** (#1525): the dashboard metric collector no longer issues store `COUNT` scans when nothing is consuming them, removing idle store load.
- **npm tarball now ships runtime assets** (#1523): `network/*.json` overlays and `project.json` are guaranteed to be packed into the `@origintrail-official/dkg` tarball. The 10.0.4 tarball omitted them (a turbo-cache prepack regression), so npm-installed nodes silently fell back to built-in network defaults.

### Changed

- **Devnet harness hygiene** (#1521): core-peers restore and the RS-rotation window fixes carried over from the 10.0.4 test run (test infrastructure only).

### Deployment

- **Contract sources/ABIs change in this release** (PublishingConviction 10.0.8, #1524) — this is **not** a "no contract changes" release.
- **Testnet (Base Sepolia): deployed.** PublishingConviction 10.0.8 at `0x99341dEb6cdfACA94F359591ED3fD20da1901DAf` (deployment block 43880924); the updated deployment registry ships with this release.
- **Mainnet (Base + Gnosis): pending.** Both chains remain on PublishingConviction 10.0.7 at release-cut time; the 10.0.8 deploy and PCA emission-schedule migration are a separate, coordinated on-chain activity. No node action is required for that deploy — nodes hot-swap the new logic from the Hub.

## [10.0.4] - 2026-07-08

2026-07-07 mainnet-incident hardening + transaction-dispatcher release. Publisher-side: ACK candidacy ranks-not-gates, empty-reply retry, responder ACK deadlines, SWM merkle-filter parity. Network-side: relay-watchdog churn fix and sync-storm calming (bounded catch-up fan-out, superseded-resume loop kill). Chain-side: the universal per-wallet transaction dispatcher lands end-to-end — every node write serialized per wallet, funding-aware generalized signer selection, and Random Sampling rotating across registered operational wallets instead of pinning wallet #0. Plus the codified manual release flow with signed-tag gates. **No smart-contract changes — no deployment required** (no Solidity source, ABI, or mainnet/testnet deployment-registry changes since 10.0.3).

### Added

- **Universal transaction dispatcher (Phases 0-4)**: every node write funnels through the per-operational-wallet serializer, closing the cross-type nonce race (#1503); funding floors (`minPublisherNativeWei`/`minPublisherTracWei`) plumbed config→adapter with fail-fast parsing of persisted string/number values, and the generalized `selectSigner(spec)` seam with typed funding modes (#1504 via #1516); **Random Sampling rotates `createChallenge`/`submitProof` across registered operational wallets** — fail-closed registered-set eligibility, per-metric funding cache so RS probes never poison publish TRAC reads, `ProfileDoesntExist` self-heal (#1506 via #1516), and on-chain identity revalidation of the chosen wallet at selection, evicting out-of-band re-registrations (#1519).
- **Manual release promotion tooling and docs**: `release:verify-versions` / `verify-tag` (annotated+signed structure, tag-on-origin, main ancestry) / `build-info` / `promote` / `verify-published`, with the staged canary→testnet→mainnet promotion flow documented as the release process (#1498; signed-tag canary docs #1518).
- **Devnet coverage for the incident class**: N-version harness + mixed-version interop suite with strictly-older `prev` resolution (#1513), storage-ACK store-outage suite — a real store paused mid-publish must yield a typed decline, quorum via healthy cores, and clean recovery (#1517), and an RS wallet-rotation suite (#1516).

### Changed

- **Release CI** now validates the manual release flow and keeps release automation focused on signed release gates instead of the removed disabled npm-publish workflow (#1498).
- **Catch-up sync fan-out is bounded** (default 4 concurrent peers, `DKG_CATCHUP_MAX_CONCURRENT_PEERS`) on both the agent-internal paths and the daemon subscribe worker — the registry-wide unbounded fan-out was the 2026-07-07 sync-storm engine (#1511).

### Fixed

- **ACK peer selection** now uses the bundled relay list only for preference ordering, never eligibility: any connected, chain-valid core can complete quorum (the 2026-07-07 outage cap), while `ackCandidatePeerIds` remains a true allowlist for SDK callers that set it (#1501).
- **Zero-length ACK replies** are treated as retryable transport failures instead of terminal `INVALID_SIGNATURE`, so a store hiccup or connection churn no longer permanently deselects a healthy peer (#1507).
- **ACK handlers answer within a deadline** (send-timeout minus a safety margin, publish and update paths): a saturated store now produces a typed `CORE_TEMPORARILY_UNAVAILABLE` decline that rides the transient-retry ladder instead of dead-air the publisher mislabels (#1510).
- **SWM merkle-root exclusion filter unified** publisher⇄responder via one shared helper, closing the `MERKLE_MISMATCH_IN_SWM` class for bookkeeping quads resident in the data graph (#1509; responder-boundary regression pinned in #1520).
- **Public relay watchdog** no longer churns sibling-core connections on publicly-reachable nodes chasing a circuit reservation it can never obtain (#1508); a `/dnsaddr`-only announce no longer counts as direct reachability, so NAT'd nodes keep maintaining their reservations (#1518).
- **Superseded-resume sync loop killed**: an aborted resumed round drops the remembered responder session and checkpoint, so the retry restarts fresh at offset 0 and makes progress instead of blind-retrying a doomed resume for the session TTL (#1512).

### Deployment

- **No new contract deployment.** No Solidity source, ABI, or mainnet/testnet deployment-registry changes since 10.0.3; existing v10 registries remain the deploy targets (re-verified across the full 10.0.4 merge set).

## [10.0.3] - 2026-07-07

Stability and node-operator UX release: Node UI PublishingConviction (PCA) owner-access hardening, folded-private ACK anchoring, reduced node RPC load, the WM→SWM promote fix, and a full repair of the devnet regression lane. **No smart-contract changes — no deployment required** (no Solidity source or ABI changes since 10.0.2).

### Added

- **RPC usage metrics** (#1409) and **reduced DKG node RPC load** across startup and steady-state polling (#1440).

### Changed

- **Node UI — PublishingConviction (PCA) owner-access model** refactor: a centralized owner-access + signer-planning model, an approve reducer/runner, and `ConvictionDetailView` decomposition (#1446, #1453, #1468, #1472; from the #1348 / #1350 / #1375 series).

### Fixed

- **Folded-private ACK anchoring** gated on storage-ack v2, with legacy V10 ACK-provider fallbacks (#1369, #1371).
- **WM→SWM promote migration** isolated on a dedicated context graph (#1464, #1483).
- **Base mainnet ACK candidate filtering** by active network relays (#1482).
- **Devnet UI solc / config** (#1403) and **testnet store preflight resilience** (#1408).
- **Devnet regression lane repaired** against the #1410 KA-publish CLI unification — a shared `runKaPublishLifecycle` helper, the rpc-quiet-window budget, the `daemon.log` double-write, and 3 flaky/blind spots (#1496).

### Deployment

- **No new contract deployment.** No Solidity source or ABI changes since 10.0.2; existing Base / Gnosis / Base-Sepolia registries recorded via #1405 remain the deploy targets.

### Fixed — `dkg init` on monorepo checkouts + oxigraph-server default for adapter/MCP setups (#960)

- **V10 publish/update allowance recovery now translates raw ethers custom-error data before `TooLowAllowance` classification** (`packages/chain/src/evm-adapter.ts`): the shared populate-and-sign recovery gate decodes raw provider revert data first, so a fresh node can force one re-approval and retry when RPC allowance reads lag behind a mined TRAC approval.
- **`dkg init` runs from a monorepo / checkout clone again** (`packages/cli/src/cli.ts`). PR #753 (RFC-41 Bundle B1c) hard-refused `dkg init` from a monorepo checkout; that was over-strict — the CLI's home resolver (`dkgDir()` → `resolveDkgConfigHome`) already routes a clone to `~/.dkg-dev` (kept separate from an npm install's `~/.dkg`), the same home the local dev daemon resolves, so there was no real risk of divergence. The hard refusal (and its non-functional `DKG_HOME` escape) is replaced by a one-line notice showing which home is being written. Restores the pre-#753 dev workflow: a clone runs `dkg init` exactly like an npm install, differing only in the home directory.
- **OpenClaw / Hermes / MCP setups now seed the `oxigraph-server` default on a fresh node** (`packages/core/src/ensure-dkg-node-config.ts`). The rc.15 `oxigraph-server` default previously reached only nodes set up via the `dkg init` store-wizard; the shared `ensureDkgNodeConfig` helper (used by `dkg openclaw/hermes/mcp setup`) left `store` unset, so those nodes fell back to `oxigraph-worker`. It now adopts `oxigraph-server` on a **fresh** install (no existing config) with no explicit store — matching the wizard default — while never rewriting an existing node's backend (which would force a store reset) and preserving any explicit `store` block.

## [10.0.2] - 2026-07-01

**Google Open Knowledge Format (OKF) support.** The DKG can now turn a portable **Google Open Knowledge Format** bundle — Markdown + YAML frontmatter + untyped cross-links — into owned, cryptographically-provenanced RDF Knowledge Assets, and serialise a Context Graph cleanly back out to a conformant bundle. Alongside it: PCA agent- and operational-wallet management, a Context-Graph registration deposit waiver, sub-graph Random Sampling fixes, and backend-independent leaf canonicalization so a node can run on any SPARQL 1.1 store.

### Added — Google OKF → DKG integration (#1388)

- **`dkg okf import ./bundle`** maps a Google Open Knowledge Format bundle to owned, verifiable Knowledge Assets — **deterministic and offline** (no LLM, no network): the same bundle always yields identical triples and IRIs. OKF standardises *how* knowledge is written and exchanged but ships no verification, provenance or ownership layer; `@origintrail-official/dkg-okf` is the bridge that adds them, reconstructing the bundle's cross-concept link graph as RDF. The same portable Markdown, now provenanced, owned and shareable across agents.
- **`--share`** finalizes the import into Shared Working Memory (free, team-visible); **`dkg okf verify`** gates the shared Context Graph against the source bundle per-predicate; **`dkg okf export <graph> ./out`** is a clean inverse that serialises a Context Graph back into a conformant OKF bundle. Ships as a new published package, `@origintrail-official/dkg-okf`.

### Added — PCA agent & operational-wallet management (#1387, #1384, #1370)

- **Bulk `registerAgents(uint256, address[])`** (#1387) — add many PCA agents in a single transaction instead of one-by-one.
- **Agents are preserved when a PCA is transferred** (#1384) — a PublishingConviction account keeps its registered agents across ownership transfer.
- **Node UI Admin page** (#1370) — manage operational wallets, PCAs and agent encryption keys from the Node UI via wallet-connect, gated to node-admin callers.

### Added — Context-Graph registration deposit waiver (#1366)

- An anti-spam Context-Graph registration deposit with a **waiver** for accounts holding a sufficient **active PublishingConviction commitment** (quota + minimum-commitment floor), pinned in the mainnet parameters.

### Fixed — backend-independent V10 leaf canonicalization (#1386, #1399)

- V10 merkle leaves are now computed from a **backend-independent value canon**, so nodes running **any SPARQL 1.1 store** (Oxigraph, Blazegraph, …) produce identical leaves for the same triple — Random Sampling no longer depends on which store backend a node runs. Validated against a live cross-backend oracle and a mixed-backend devnet. Spec: OT-RFC-57.

### Fixed — sub-graph Random Sampling extraction (#1385)

- The Random Sampling extractor now resolves a sub-graph Knowledge Asset's content across its data homes, so a KA published into a named sub-graph is extractable and provable (no `KCDataMissingError` stall).

### Changed — Node UI (#1391)

- Hide the Admin page entry point (icon + `/admin` route) by default.

## [10.0.0-rc.15] - 2026-06-03

**Off-chain runtime release.** No Solidity changes since rc.13 — the Base Sepolia (chainId 84532) deployment is **unchanged**, the `chainResetMarker` stays `v10-rc12-ka-rename-2026-06-01`, and **no contract redeploy is required**. The sync protocol ID is **unchanged** (`/dkg/10.0.2/sync`), so rc.15 nodes interoperate with rc.14 peers — nodes upgrade in place with **no local-state wipe**. This release makes `oxigraph-server` the default store backend for **new** installs, fixes the Node UI Working-Memory scoped-query violation (and the SWM/VM bleed + reserved-meta false-positives it surfaced), reclaims the `node-ui.db` WAL on a running node, splits the `DKGAgent` god class into subsystem mixin holders, and lands typed publisher errors plus CI/bench hardening.

### Changed — `oxigraph-server` is now the default triple-store backend for new installs

- **`dkg init` defaults to `oxigraph-server`** (PR #946, `packages/cli/src/store-wizard.ts`): the store-backend menu now lists `oxigraph-server` (daemon-managed local RocksDB server) as the first, recommended option and the default answer; the embedded in-process worker (`oxigraph`) moves to option 2 for minimal-footprint / single-reader nodes. A fresh `dkg init` — or one that accepts the default — writes an explicit `"store": { "backend": "oxigraph-server" }` block, so new nodes get MVCC concurrent reads + incremental persistence out of the box.
- **The existing fleet is unaffected on auto-update.** The runtime fallback for a config with **no** `store` block stays `oxigraph-worker`, and auto-update never re-runs `dkg init` — so existing nodes keep booting on the embedded worker unchanged. A re-init only flips a **block-less** node (an explicitly-chosen local backend — `oxigraph` / `oxigraph-worker` / `oxigraph-persistent` — is preserved on Enter-through), and even then the daemon's `STORE-SWITCH` guard makes the switch an opt-in (it refuses to start until `DKG_ACCEPT_STORE_RESET=1`) rather than a silent store reset.
- **Platform note:** new installs fetch the prebuilt `oxigraph` v0.5.8 binary on first boot. The Linux artifacts are glibc-only, so musl hosts (Alpine/distroless) should pick option 2 (`oxigraph`) or an external `sparql-http` endpoint at the `dkg init` prompt.

### Fixed — Node UI Working-Memory layer scoped-query violation

- **Keep scoped `GRAPH` variables top-level in WM layer queries** (PR #944, `packages/node-ui/src/ui/api.ts`, `views/MemoryLayerView.tsx`): the WM layer view and `listWmAssertions` nested `GRAPH ?g` inside a `UNION`, which the scoped local-query guard (PR #749, `constrainGraphVariablesToAllowedSet`) rejects with "GRAPH variables must appear at the top level of scoped local queries". Surfaced as a *"Scoped query violation"* during WM→SWM promotion. The queries now keep both `GRAPH` clauses as top-level WHERE-block siblings and opt into the CG-scoped assertion partitions via `includeContextGraphPartitions`.
- **No SWM/VM bleed into the WM tab**: widening the allow-list to the assertion partitions also exposes `/_shared_memory` and `/_verifiable_memory/*`, so `?g` is gated on the `<cg>/_meta` `dkg:memoryLayer "WM"` lifecycle marker (the same authority `listWmAssertions` uses), and the partition opt-in is scoped to the built-in WM query only — not user-typed custom SPARQL.
- **WM triple-count is best-effort**: a failing count query no longer collapses the whole assertion list to "no assertions"; counts degrade independently.
- **Match the reserved meta bucket by path shape, not `/_meta` suffix** (PR #948, follow-up to #944): exclude UI-config drafts via `FILTER(!CONTAINS(STR(?g), "/meta/assertion/"))` (plus a parser-level guard) so a legitimate user assertion *named* `_meta` is no longer dropped.

### Fixed — Reclaim `node-ui.db` WAL on a running node

- **WAL reclaim** (PR #945, `packages/node-ui/src/db.ts`): the multi-GB `node-ui.db` WAL left behind by the pre-rc.14 messenger-substrate sync bloat is now reclaimed on a running node, complementing the rc.14 fix that stopped the growth at the source.

### Changed — Internal refactors (no behavior change)

- **`DKGAgent` god class split into subsystem mixin holders** (PR #941, `packages/agent/src/dkg-agent-*.ts`): the monolithic `dkg-agent.ts` is decomposed into per-subsystem mixin modules (boot, lifecycle, publish, query, join, registry, crypto, diagnostics, …) applied via `dkg-agent-apply-mixins.ts`. Pure structural refactor; runtime behavior unchanged.
- **Typed publisher errors** (PR #949, `packages/publisher/src/errors.ts`): publisher error classes extracted into a dedicated `errors.ts` module.

### Added — Store read-latency benchmark (regression guard for #939)

- **`bench/store-read-latency.bench.ts`** (PR #943) — a new esbench suite that measures real Oxigraph read latency for the two shapes that hung on a saturated node (issue #939): a trivial `SELECT … LIMIT 1` scan and the production `getTotalTriples` `COUNT(*)` aggregate, on an idle store. On the embedded **`worker` backend** (the single-writer store) it additionally measures those reads **under concurrent write load**, quantifying the read-starvation that drove the rc.13 regression and giving a baseline to measure the MVCC `oxigraph-server` backend (now the default for new installs) against. Contention is worker-only by design — the in-process store is single-threaded/synchronous and cannot exhibit true read/write overlap, so it runs the idle baselines only. Backends via `DKG_BENCH_STORE_BACKENDS` (`inprocess` always; `worker` auto-added when its compiled artefact is built, e.g. in CI), sizes via `DKG_BENCH_STORE_SIZES`. Run with `pnpm bench:store-read`; participates in `pnpm bench` / `bench:baseline` / `bench:compare` like the existing suites.

### Changed — Test / CI hardening

- **e2e devnet preconditions fail in CI instead of skipping** (PR #942, `packages/node-ui/e2e`): missing devnet preconditions now fail the job rather than silently skipping, and the suite rejects a 207 partial VM publish by default.

## [10.0.0-rc.14] - 2026-06-02

**Off-chain runtime release.** No Solidity changes since rc.13 — the Base Sepolia (chainId 84532) deployment is **unchanged**, the `chainResetMarker` stays `v10-rc12-ka-rename-2026-06-01`, and **no contract redeploy is required**. Nodes upgrade in place; no local-state wipe. This release fixes the multi-GB `node-ui.db` bloat by taking the sync RPC off the Universal Messenger substrate, adds per-peer exponential backoff to the sync reconciler, adds an **opt-in** daemon-managed local Oxigraph server backend (Release 2, phase 2a), and stops `eth_getLogs` rate-limit failures from surfacing as unhandled process rejections under gossip/finalization verification storms.

> **Wire-compatibility note (hard cutover):** the sync protocol ID is bumped `/dkg/10.0.1/sync` → `/dkg/10.0.2/sync`. An rc.14 node will **not** sync with an rc.13 (or earlier) node and vice versa — mixed-version peers cleanly skip each other (`waitForSyncProtocol`). Because sync is a self-healing eventual-consistency catch-up net (not a primary data path), the brief mixed-version window during a network rollout is **no-data-loss**: once both ends are on rc.14 the next reconnect/reconciler tick backfills.

### Fixed — `node-ui.db` bloat: sync RPC taken off the Universal Messenger substrate

- **Sync no longer writes to `message_idempotency`** (`packages/agent/src/dkg-agent.ts`, `packages/agent/src/sync/responder/sync-handler.ts`, `packages/core/src/constants.ts`). rc.9 PR-E had routed sync through the Universal Messenger substrate "for receiver-side dedup", but the sync requester mints a **fresh `messageId` per attempt**, so the dedup key never repeats — the cache-hit rate is structurally zero while every large, never-reused sync page response was being cached on **both** sides of the `message_idempotency` table. On a long-lived node this grew `node-ui.db` to multiple GB (≈3.1 GB observed, plus a same-size WAL). The responder now registers on the **raw `ProtocolRouter`** and the requester sends via `messenger.sendToPeer` (raw pass-through) instead of `messenger.sendReliable`. Sync already carries its own `withRetry`, a 90s auth-freshness TTL, and per-`requestId` replay protection, so it needs none of the substrate's idempotency/outbox machinery. A regression test pins that `PROTOCOL_SYNC` is registered off the substrate.

### Added — Per-peer exponential backoff for the sync reconciler

- **Sync reconciler backoff** (`packages/agent/src/dkg-agent.ts`, `packages/agent/src/dkg-agent-constants.ts`): a peer that can never be synced (dead, NAT-stuck, or persistently stream-resetting) never stamps `lastSuccessfulSyncAt`, so it read as perpetually stale and was dialed on **every** reconciler tick forever. The reconciler now applies per-peer exponential backoff — `SYNC_BACKOFF_BASE_MS * 2^(failures-1)`, capped at `SYNC_BACKOFF_MAX_MS`, with ±`SYNC_BACKOFF_JITTER` randomisation — that resets the instant a sync succeeds and is cleared on `connection:close`. Only the **periodic reconciler** is gated; `connection:open` and `peer:update` still trigger an immediate attempt, so a newly-reachable peer is never delayed. The failure-record path is guarded against re-creating backoff for a peer that disconnected mid-attempt, and the per-peer backoff/`lastSuccessfulSyncAt` state is surfaced in node diagnostics.

### Added — Opt-in daemon-managed local Oxigraph server (Release 2, phase 2a)

- **`store.backend: 'oxigraph-server'`** (`packages/cli/src/daemon/oxigraph-binary.ts`, `oxigraph-server.ts`, `oxigraph-managed.ts`): a new **opt-in** triple-store backend that runs a daemon-supervised local [Oxigraph](https://github.com/oxigraph/oxigraph) server (RocksDB-backed) instead of the embedded in-process worker. This gives **MVCC concurrent reads** (queries stop blocking on the single writer — the root cause of the slow-CG-create-under-sync-load symptom) and incremental persistence (no O(total-triples) full-dump flush), with the same engine for semantic parity.
  - **Fetch-on-install**: the daemon downloads the pinned prebuilt `oxigraph` v0.5.8 binary for the host (`process.platform`/`process.arch`), verifies it against a baked-in SHA-256, caches it under `<DKG_HOME>/oxigraph`, marks it executable, and clears the macOS quarantine xattr. No binary is bundled in the npm package (~15–20 MB fetched once per host; the six platform variants would be ~100 MB).
  - **Supervision**: the daemon spawns `oxigraph serve` bound to loopback (`127.0.0.1`), health-checks it before the agent boots (verifies the spawned child's PID owns the listen port, not merely that something answers HTTP 200), restarts it with capped backoff on unexpected exit, and stops it after the agent during shutdown — with a synchronous best-effort kill registered on process exit so no fatal boot path orphans the server.
  - **Zero new downstream plumbing**: boot uses a runtime `sparql-http` view of the managed server (the persisted config stays `oxigraph-server`), so config validation, the boot reachability probe, namespace-identity tagging, and chain-reset wipe all reuse the external-backend path unchanged. `managedByDkg: true` is set so chain-reset wipe uses `DROP ALL` on the locally-owned RocksDB.
  - **Enable it**: set `"store": { "backend": "oxigraph-server" }` in `~/.dkg/config.json`, pass `--store oxigraph-server` to the setup commands, or type `oxigraph-server` at the `dkg init` store prompt. Default backend is **unchanged** (`oxigraph-worker`); switching backends starts on an empty store and requires `DKG_ACCEPT_STORE_RESET=1` on first switch.
  - **Security**: `oxigraph serve` has no native auth; the security boundary for the managed server is the loopback bind. The endpoint is never exposed off-host.
  - **Linux caveat**: the prebuilt Linux artifacts are glibc-only (`*_linux_gnu`); musl hosts (Alpine/distroless) must use a system-provided `oxigraph` or an external `sparql-http` endpoint.

### Fixed — Chain RPC read path (`eth_getLogs` rate-limit storm)

- **Disable ethers JSON-RPC request batching on the chain provider** (`packages/chain/src/evm-adapter.ts`, PR #940): under RPC rate limiting a *batched* `eth_getLogs` response is a single whole-batch JSON-RPC error, and ethers' coalesce path rejected on the un-awaited batch-drain promise — surfacing as ~30k unhandled `could not coalesce error` / `over rate limit` process rejections under a gossip/finalization on-chain-verification storm (issue #939). Constructing providers with `batchMaxCount: 1` makes each read its own awaited request whose rejection is caught by the existing `verifyOnChain` try/catch. Transport-only change; the number of `eth_getLogs` operations issued is unchanged.

## [10.0.0-rc.13] - 2026-06-02

**Off-chain stabilization release.** No Solidity changes since rc.12 — the Base Sepolia (chainId 84532) deployment in `packages/evm-module/deployments/base_sepolia_v10_contracts.json` is **unchanged**, the `chainResetMarker` stays `v10-rc12-ka-rename-2026-06-01`, and **no contract redeploy is required**. Nodes upgrade in place; no local-state wipe. This release lands the core-preferred sync + chain-driven VM reconciliation work, the Kafka route-plugin MVP, and a batch of publish/SWM runtime hardening and Node UI fixes accumulated on top of rc.12.

### Added — Core-preferred sync + chain-driven VM reconciliation (#927)

- **Core-preferred sync + chain-driven verifiable-memory reconciliation** (PR #927, `packages/agent/src`, `packages/core/src`; supersedes #906/#908/#910/#911/#912/#914): the sync path now prefers Core peers and reconciles verifiable-memory against on-chain state, including host-mode catch-up. Closes the long-running sync/catch-up workstream tracked across the superseded PRs.

### Added — Kafka route-plugin MVP (#607)

- **`@origintrail-official/kafka-plugin`** (PR #607, `packages/kafka-plugin`, `demo/kafka-streams`): new route-plugin consumer package that ingests from Kafka topics into the DKG publish path. Added to `pnpm-workspace.yaml` and the runtime build set.

### Changed — Default publish lifetime is now 12 epochs (#926)

- **`random-sampling` default epochs** (PR #926): the default publish lifetime is set to **12 epochs**. Publishes that don't specify a lifetime now default to 12 instead of the previous default.

### Fixed — Publish / SWM / sync runtime hardening

- **Single-root SWM publish boundary** (PR #925, `packages/publisher/src`): enforce a single-root boundary on shared-working-memory publishes (multi-root publish flow correctness).
- **SWM plaintext for on-chain-public CGs with an allowed-agent list** (PR #884, `packages/agent/src`): keep SWM payloads plaintext for context graphs that are on-chain-public but carry an allowed-agent list.
- **Sub-graph SWM in catch-up + approve-time race** (PR #885, `packages/agent/src`): cover sub-graph SWM during catch-up and close a late-joiner / approve-time race (`scripts/devnet-test-swm-late-joiner-subgraph.sh`).
- **SWM Sender Key setup after joined-CG approval** (PR #900, `packages/agent/src`): fix sender-key setup that was missed after a joined-CG approval; parallel fan-out correctness.
- **rc.12 publish ACK / allowance races + SPARQL parse-error classifier** (PR #896, addresses #887/#888/#889): resolve publish ACK and allowance race conditions and add a SPARQL parse-error classifier.
- **Stabilize V10 publish runtime paths** (PR #899): runtime fixes across the V10 publish path.
- **Harden context-graph registration RPC handling** (PR #883, #883/orch): tolerate RPC 429s on context-graph register.
- **Per-publish allowance auto-replenish** (PR #876, `packages/chain/src`): auto-replenish the per-publish allowance floor (#870).
- **Skip recovery `getIdentityId` on boot chain-timeout** (PR #903): don't run identity recovery when the boot-time chain call times out.
- **Artifact read-gate for public CGs** (PR #879, forward of #872).

### Fixed — Forward-ports from rc.12 hotfix line

- **Rich error when promote finds an empty assertion graph** (PR #898, forward-port of #874, `packages/publisher`, `packages/node-ui`).
- **Respect explicit `accessPolicy="public"` over allowlist heuristic** (PR #897, forward-port of #873, `packages/agent`).

### Fixed — Node UI

- **Unified render-correct triple derivation via `useCanonicalTriples`** (PR #847, GH #819, `packages/node-ui/src`).
- **WM Assertions subtab empty after #844** (PR #877): derive the list from `_meta`.
- **4 QA-found UI bugs** (PR #924, addresses #904/#905/#913/#915).
- **Markdown import root-children partitioning** (PR #920): fix markdown section root partitioning.
- **Post-#847 follow-ups** (PR #890): GH #881 fused-counts helper + GH #882 hydrating tooltip.
- **Settings page cleanup** (PR #855): remove stale sections, sleeker cards, accessibility fixes.
- **Selected `DKG_HOME` daemon status detection** (PR #878, orch).

### Added — Test coverage

- **Node UI e2e against a real node** (PR #918, `packages/node-ui/e2e`).
- **`core-peers-features` devnet suite** (`devnet/core-peers-features`).
- **Build unblock**: augment `NodeJS.ProcessEnv` with `DKG_HOME` (PR #892); resolve 3 #892-surfaced test regressions (PR #901, addresses #893/#894/#895).

### Added — Operator observability for per-publish 1-wei allowance floor (#871)

- **Diagnostic log line in `EVMChainAdapter.ensureV10ApproveTrac`** (`packages/chain/src/evm-adapter.ts`): when the V10 publish/update path emits its per-publish floor approval (`targetAllowance == V10_PUBLISH_ONCHAIN_MIN_ALLOWANCE` and `chain.approvalPolicy.mode == 'per-publish'`), the daemon now logs a single `console.warn` line identifying that the resulting 1-wei allowance is the intentional `#720` workaround (the contract's `transferFrom(..., 1n)` minimum on zero-cost publishes) and is not a stuck or ghosted approval. This closes a reportability gap surfaced by [#871](https://github.com/OriginTrail/dkg/issues/871), where an operator manually inspected `allowance()` against the wrong (typo'd) spender address and read the daemon's pre-existing 1-wei floor as a daemon-side bug. Investigation note: [`docs/investigations/871-curator-wallet-allowance-ghosting.md`](docs/investigations/871-curator-wallet-allowance-ghosting.md). No behaviour change for any caller — only the diagnostic line is new; the underlying floor logic and the `effectivePublishAllowance` / `computeApprovalAction` invariants are unchanged.

## [10.0.0-rc.12] - 2026-06-01

**Cutover release.** No compatibility shims, no fallback resolvers, no backward-compatible codepaths. Testnet (Base Sepolia) and devnet are intentionally broken by this change and must be redeployed. Every package in the workspace upgrades in lockstep. This is a forward-only release.

**For builders upgrading from rc.11**, see [`docs/UPGRADE_RC11_TO_RC12.md`](docs/UPGRADE_RC11_TO_RC12.md) for the focused migration guide (TS rename tables, Solidity selector changes, greenfield KA model, economic floors, agent-prompt template).

### Added — Greenfield Knowledge Asset model (BREAKING)

- **Greenfield KA model** (PR #815, `packages/evm-module/contracts/DKGKnowledgeAssets.sol`, `packages/evm-module/contracts/KnowledgeAssetsLifecycle.sol`, `packages/publisher/src/dkg-publisher.ts`, `packages/agent/src/dkg-agent.ts`, `packages/evm-module/docs/greenfield-ka-ual.md`): exactly **one** ERC-721 KA per publish (`knowledgeAssetsAmount == 1` is the only valid value). Stable UAL — `did:dkg:{chainId}/{DKGKnowledgeAssetsAddress}/{kaId}` — that does **not** change across merkle-root updates. ERC-721 minted to the **author** at publish (publisher pays TRAC; author holds the token). Owner-sealed updates: only the current `ownerOf(kaId)` can update, by signing an EIP-712 `UpdateAuthorAttestation(kaId, newMerkleRoot, authorAddress, schemeVersion)` once (domain `KnowledgeAssetsLifecycle` v2.0.0, kept stable for attestation continuity) and passing it as `precomputedUpdateAttestation` on the first `publisher.update()` call. The publisher (any node operator) does the chain write; the attestation is the seal the chain validates against `ownerOf`. No ERC-1155 mint/burn on update — the ERC-721 stays bound to the same `kaId`. Devnet gates: `pnpm test:devnet:greenfield-10min`, `pnpm test:devnet:rich-scenario`. Canonical UAL builder exported as `buildKnowledgeAssetUal(chainId, contract, kaId)` from `@origintrail-official/dkg-chain`. Surface reference: `packages/evm-module/docs/greenfield-ka-ual.md`.

### Added — Protocol treasury fee on publish / update / extend

- **Governance-set bps fee** (PR #821, `packages/evm-module/contracts/storage/ParametersStorage.sol`, `packages/evm-module/contracts/KnowledgeAssetsLifecycle.sol`, `packages/evm-module/contracts/PublishingConviction.sol`, `packages/chain/abi/ParametersStorage.json`, `packages/chain/abi/PublishingConviction.json`): skims a governance-set bps cut (default **300 bps = 3 %**, capped at **MAX_PROTOCOL_TREASURY_FEE = 1000 bps**) from the staker-bound TRAC on every paid publish/update/extend and routes it to a treasury address. **The publisher pays the same gross price** — the fee comes out of what would otherwise flow into the staker reward pool. **Dormant by default** (`treasury == address(0) ⇒ fee == 0`), so a fresh deploy is unchanged until governance opts in via the new owner-gated `setProtocolTreasury` / `setProtocolTreasuryFee` setters. New `ProtocolTreasurySet(uint256 fee, address treasury)` event. `KnowledgeAssetsLifecycle._addTokens` splits at the source (`net → CSS vault, fee → treasury`) and returns `net` so publish/update/extend distribute `net` into the pool while CG-value writes stay on gross. `PublishingConviction` skims the fee on the active sink + every passive window sweep + final dust/topUp tail, accumulates, and pays via a single `CSS.transferStake` **after** all state writes so the permissionless `settle()` stays reentrancy-safe. **Lifetime invariant**: `pool + treasury == committed + topUps`. ParametersStorage version: `1.0.0` → `1.1.0`. PublishingConviction version: `1.0.1` → `1.1.0`. `KnowledgeAssetsLifecycle` patch bump `2.0.0` → `2.0.1`; the EIP-712 domain hash is **pinned at `KnowledgeAssetsLifecycle@2.0.0`** so existing author attestations keep verifying across the patch bump.

### Added — Blazegraph + SPARQL-HTTP external triple-store (RFC 120)

- **External triple-store backend** (PR #766, `packages/storage/src/blazegraph-store.ts`, `packages/storage/src/sparql-http-store.ts`, `packages/cli/src/storage-wizard.ts`, `docs/setup/STORAGE_SPARQL_HTTP.md`): the daemon's persistence layer can now point at an external Blazegraph or generic SPARQL-HTTP endpoint instead of (or alongside) the embedded Oxigraph store. Configured via `storage.backend: "sparql-http"` + `storage.endpoint: "<url>"` in `~/.dkg/config.json`, or via the new interactive `dkg storage` wizard. Operator runbook at `docs/setup/STORAGE_SPARQL_HTTP.md`. Companion fixes for Blazegraph integration: (PR #782) namespace-creation 500 on first boot; (PR #810) large publishes + EPCIS private-scope queries; (PR #789) oversized literal guard.

### Added — Operator tooling and observability

- **`dkg doctor`** (RFC-41 Bundle A, `packages/cli/src/commands/doctor.ts`): diagnoses common install/upgrade issues. Six structured checks + a state-summary; JSON-parseable output. Run `dkg doctor` after upgrading to confirm the install is healthy.
- **`/api/build-info`** (RFC-41 Bundle A3, `packages/cli/src/daemon/routes/status.ts`): surfaces `{ version, commit, installMode, builtAt }` so monitoring can correlate daemon behaviour with build provenance.
- **`/api/status` `relay` block** (carried from rc.11; pinned at every node): uniform `{ isCore, reservationsHeld, reservationCapacity, activeCircuits, bytesIn, bytesOut, natStatus, listenAddresses, announcedAddresses }` so dashboards parse one schema rather than switching on `nodeRole`.
- **RFC-41 Bundle B — Edge npm-only update path** (`packages/cli/src/daemon/auto-update.ts`, `packages/cli/src/migrate-to-npm.ts`): Edge nodes update via `npm install -g @origintrail-official/dkg` instead of git-build. Git-based updates are hard-refused at user-facing entry points for edges; Core nodes retain the git-build path for now. Includes `--role` flag on `dkg init`, monorepo guard, stable plugin install root, `installMode` telemetry, deprecation banner on `install.sh` (which has been removed).
- **Node UI — Context Graph Overview and notifications redesign** (PR #745, #818, #811): redesigned Context Graph view with Root mini-card, cross-layer pills, mini-graph colors, count-accuracy fixes; membership-scoped notifications pane with inline join approve/deny actions.

### Changed — Knowledge Collection → Knowledge Asset rename (BREAKING)

Clean cutover from the V8/V9/V10.0 "Knowledge Collection" terminology to the V10.1 "Knowledge Asset" terminology, both on-chain and off-chain. No compat shims, no fallback resolvers — testnet is intentionally broken by this change.

Ships as three stacked PRs:

- **PR #836 (Solidity surface)** — `KnowledgeCollectionLib.sol` → `KnowledgeAssetLib.sol`, struct `KnowledgeCollection` → `KnowledgeAsset`, every `KnowledgeCollection*` error/event renamed to `KnowledgeAsset*`, every `create/update/get/isPartOf/isOwnerOf/extendKnowledgeCollection*` view/mutator renamed on `DKGKnowledgeAssets.sol` and `KnowledgeAssetsLifecycle.sol`. `ContextGraphStorage.kcToContextGraph` → `kaToContextGraph`, `registerKnowledgeCollection` → `registerKnowledgeAsset`. Legacy V10.0 contracts `KnowledgeAssetsV10.sol` and `KnowledgeCollectionStorage.sol` are deleted (they were dead code in V10.1 — the active path is `KnowledgeAssetsLifecycle` + `DKGKnowledgeAssets`). The V8 archive `KnowledgeCollection.sol` is deleted (referenced the renamed lib). `deployments/parameters.json` config block `KnowledgeCollectionStorage.knowledgeCollectionSize` → `DKGKnowledgeAssets.knowledgeAssetBatchSize`. ABIs regenerated; legacy ABIs removed from `packages/chain/abi/`.

- **PR #837 (TS surface)** — `ChainAdapter.createKnowledgeAssetsV10` → `createKnowledgeAssets`, `getKnowledgeAssetsV10Address` → `getKnowledgeAssetsLifecycleAddress`, `getKCContextGraphId` → `getKAContextGraphId`. `NodeChallenge.knowledgeCollectionId` → `knowledgeAssetId`. `PublishResult.kcId` was already `kaId` in the publisher; all consumers now agree. `ASSERTION_PUBLISH_RECEIPT_PREDICATES.PUBLISHED_AT_KC_ID` → `PUBLISHED_AT_KA_ID` (predicate URI `publishedAtKcId` → `publishedAtKaId`). Adapter internal handles `knowledgeAssetsV10` → `knowledgeAssetsLifecycle`, `knowledgeCollectionStorage` → `knowledgeAssetStorage`. The legacy V10.0 fallback resolver in `EVMChainAdapter.init` is removed — when `KnowledgeAssetsLifecycle` doesn't resolve, the adapter raises rather than silently trying `KnowledgeAssetsV10`. `BOUND_CONTRACT_INVALIDATORS` and `ERROR_ABI_CONTRACTS` drop the legacy entries.

- **PR #838 (docs/tests/scripts/UI)** — every remaining `kcId`/`KCId`/`KnowledgeCollection*` reference across `scripts/`, `docs/`, `demo/`, `bench/`, `devnet/`, `packages/{evm-module,cli,adapter-openclaw,adapter-elizaos,mcp-dkg,node-ui,network-sim,epcis}/{src,test}`, and the chain README. Prose-level term renames "Knowledge Collection" → "Knowledge Asset" / "KC" → "KA" in active docs.

**Migration impact** for downstream consumers (off-chain SDK callers, custom scripts, dashboards):

- Solidity callers: 4-byte selectors on `DKGKnowledgeAssets` and `ContextGraphStorage` change for every renamed function. Old call data reverts; consumers must rebuild against the new ABIs in `packages/chain/abi/`.
- TS callers: type names + property names renamed as above. The 1-pass mechanical search-and-replace (`kcId` → `kaId`, `knowledgeCollectionId` → `knowledgeAssetId`, `createKnowledgeCollection` → `createKnowledgeAsset`, etc.) covers all known consumers. The `docs/UPGRADE_RC11_TO_RC12.md` guide ships the exact regex sweep.
- ABI-pin tests: `packages/chain/test/abi-pinning.test.ts` pinned digests for `KnowledgeAssetsLifecycle`, `DKGKnowledgeAssets`, `ContextGraphs`, `ContextGraphStorage` match the rename; `KnowledgeAssetsV10` and `KnowledgeCollectionStorage` entries are dropped.
- Hub registration: `KnowledgeAssetsLifecycle` and `DKGKnowledgeAssets` are the only V10.1 entries the daemon looks up. Hub deployments that still expose `KnowledgeAssetsV10` or `KnowledgeCollectionStorage` will silently be ignored by the rc.12 daemon — re-deploy the V10.1 surface before upgrading.

### Changed — V10 EVM module hardening pass (BREAKING for two surfaces)

Consistency and defense-in-depth refinements across the V10 EVM-module contracts (PR #781). No behaviour change for valid callers except where flagged BREAKING.

- **CEI ordering on `DKGStakingConvictionNFT.withdraw`.** The receipt NFT is now burned **before** `StakingV10.withdraw` drives the CSS teardown + TRAC payout. `StakingV10.withdraw` gates on the CSS position (`pos.identityId == 0`), not NFT existence, so the CSS teardown is unaffected. Same CEI-mint-last ordering applied on every ERC-721 mint path on `DKGKnowledgeAssets` (PR #681, supersedes #663).
- **`nonReentrant` perimeter on KAV10 entrypoints.** `publish` / `update` / `extendKnowledgeAssetLifetime` now carry OZ `ReentrancyGuard.nonReentrant` as a defense-in-depth perimeter against the ERC-1155 receiver-hook callback path. ~50 gas/call overhead. KAV10 version: `10.1.0` → `10.1.1`. Surfaces as `ReentrancyGuardReentrantCall()` if a receiver hook re-enters; chain adapter decoder updated to surface it as a structured error.
- **Strict-positive `tokenAmount` floor in `_validateTokenAmount` AND post-discount floor in `PublishingConviction.coverPublishingCost`.** Both branches of the publish flow now charge a non-zero economic cost regardless of input rounding: direct-spend reverts `InvalidTokenAmount(1, 0)` on `tokenAmount == 0`; the conviction (PCA) branch inflates a truncated `discountedCost == 0` to `1` wei TRAC when `baseCost > 0` so the active-sink reward distribution + `windowSpent` accounting always fire. **BREAKING** for any caller that previously relied on dust-CG zero-amount publishes — the on-chain revert is the floor of truth; off-chain callers MUST encode `tokenAmount >= 1`. Same floor applies to the update path (PR #831, #833): `update` and `extendKnowledgeAssetLifetime` enforce the floor in both direct-spend and conviction branches. PublishingConviction version: `1.0.0` → `1.0.1` → `1.1.0`.
- **Single source of truth for op-wallet validation in `Identity.addOperationalWallets`.** Same-identity collisions (primary added by `createIdentity` OR intra-array duplicate within the same call) surface as `OperationalWalletDuplicate(wallet)`; cross-identity collisions still fire `OperationalKeyTaken(key)`; admin/operational wallet overlap surfaces as the existing `KeyAlreadyAttached(key)`. `Profile.createProfile`'s pre-flight validation loop is removed — atomic-revert semantics make the prior "fail-fast at the entrypoint" rationale moot. Identity version: `1.0.0` → `1.1.0`; Profile version: `1.3.0` → `1.4.2`.
- **`Profile.recreateProfile` signature refinement (BREAKING for recovery scripts).** Drops the `uint16 initialOperatorFee` argument; the recovered profile is seeded at fee = 0 and the admin sets the real value via the cooldown-gated `updateOperatorFee` path. Keeps the recovery and steady-state surfaces symmetric on the operator-fee dimension. ADR `packages/evm-module/docs/adr/0001-recreate-profile-admin-only.md` updated.
- **Chain-package ABI sync.** `packages/chain/abi/*.json` re-exported from the freshly regenerated `evm-module/abi/` copies so the chain adapter's error decoder (which prefers its local override over the published artifact) resolves `ReentrancyGuardReentrantCall`, `InvalidTokenAmount`, `OperationalWalletDuplicate`, and the new Profile error surface as structured errors instead of opaque reverts.
- **Regression coverage.** New unit tests pin every new revert surface: `publish` + `extendKnowledgeAssetLifetime` revert `InvalidTokenAmount(1, 0)` on `tokenAmount == 0`; `publish` reverts `ReentrancyGuardReentrantCall()` when re-entered from the ERC-1155 mint acceptance callback (via the new `MockReentrantPublisher` test harness); `PublishingConviction.coverPublishingCost(baseCost=1, ...)` floors `discountedCost` at 1 and propagates that floor through the active-sink reward distribution + `windowSpent` accounting; `Identity.addOperationalWallets` per-class disambiguation; `Profile.createProfile` per-class diagnostics.

Storage layout: no changes — KAV10's added `ReentrancyGuard` storage slot lands at the end of the inheritance chain, and V10's redeploy-and-reinit pattern doesn't preserve storage across upgrades anyway.

### Changed — Active-sink proration + publishing discount ladder centralisation

- **PCA active-sink proration math** (PR #817, multiple commits): centralised the publishing discount ladder, wired the active-sink proration helper into both the direct-publish and conviction paths, preserved final-range dust, handled empty middle and final ranges, and covered one-epoch and large-dust cases. The math is now uniform across publish and update; the previous discount-recomputation drift on update is gone.
- **ConvictionStakingStorage added to the 052b deploy dependency graph** so the treasury-fee + active-sink rework deploys reliably from a clean Hub.

### Fixed — Random Sampling on updated Knowledge Assets (GH #842)

- **Updated KAs are now provable by Random Sampling** (PRs #842, #845; `packages/publisher/src/metadata.ts`, `packages/publisher/src/dkg-publisher.ts`, `packages/publisher/src/update-handler.ts`, `packages/agent/src/finalization-handler.ts`, `packages/random-sampling/test/ka-extractor.test.ts`, `packages/publisher/test/update-handler-gh842.test.ts`): every updated KA was permanently unprovable by Random Sampling pre-rc.12, producing a steady stream of `rs.tick.data-corrupted` on core nodes once any KA was updated. Root cause was a race in the daemon's local projection of the chain's ordered update log: two concurrent writers to the per-cgId materialisation partition (`publishFromSharedMemory`'s publish→per-cgId promotion and the inline update-promotion) had no version discipline, so a late publish-promotion could clobber an already-applied update with no guard. Three changes give the projection layer the same ordering guarantee the chain log already has: (1) **full label-graph restatement** on both update paths (`storeUpdatedQuads` and `UpdateHandler`) via the shared `restateLabelGraphForUpdate` helper — prior root entities' data is purged, `rootEntity` is repointed (rich provenance like `dkg:authoredBy` preserved), `merkleRoot` refreshed; (2) **a per-KA `dkg:materializedVersion` (`block:txIndex`) guard** stamped on the KC's `<ual>` in the meta graph each canonical writer touches (publisher promotion, update promotion, receiver `FinalizationHandler`) — every canonical writer now refuses to apply a state older than what's already materialised, so a late publish-promotion is a no-op; (3) **a deterministic-UAL fallback** in `UpdateHandler` (`did:dkg:<chainId>/<kasAddress>/<batchId>`, matching the publisher's own `resolveKaUal`) so a gossip receiver that hasn't yet materialised the `dkg:batchId` edge still promotes instead of silently skipping. Helpers live in `packages/publisher/src/metadata.ts`. Regression coverage drives the actual ordering (publish-promotion after update-promotion) rather than just final-state shape. Full root-cause analysis in `GH842-rs-update-race-analysis.md`.

### Fixed — ERC-20 non-reverting `false` returns (PR #814)

- **Staking deposits + withdrawals + operator-fee finalisation now guard against ERC-20 implementations that return `false` instead of reverting** (`packages/evm-module/contracts/StakingV10.sol`, `packages/evm-module/contracts/ConvictionStakingStorage.sol`, `packages/evm-module/contracts/Staking.sol`): every `transferFrom` / `transfer` call site that previously checked only revert behaviour now asserts the bool return, so a misbehaving (or malicious) ERC-20 cannot silently strand TRAC mid-flow. The default v9TRAC token reverts, so this is defence in depth — but it closes the surface for any token that follows the early-OpenZeppelin "return false" pattern.

### Fixed — Update path token-amount floor + ACK digest alignment (PR #831, #833)

- **`update` and `extendKnowledgeAssetLifetime` now pay the marginal growth cost for `byteSize` updates** so the on-chain economic floor matches the publish path (`packages/evm-module/contracts/KnowledgeAssetsLifecycle.sol`).
- **Storage ACK digest binds the precomputed `newTokenAmount`** (`packages/chain/src/evm-adapter.ts`, `packages/publisher/src/storage-ack-handler.ts`) so ACK collectors can't be tricked into signing for a different fee than what the chain enforced. Includes epoch-boundary growth handling + mock parity exempts so the unit-tests stay deterministic.
- **Fail-fast KA / ask / Chronos reads** in the chain adapter so a transient RPC mismatch surfaces as an explicit error rather than silently using stale data on the update path.

### Fixed — Ethers `PollingEventSubscriber` CPU spin

- **Forced ethers' `PollingEventSubscriber` to stop the `FilterId` CPU spin** (`packages/chain/src/evm-adapter.ts`) that was producing 100 % single-core utilization on long-uptime daemons. The subscriber was tight-looping on a stale filter id; the adapter now invalidates and reinstalls the subscription. Companion to rc.11's `createFilterErrorSilencer` work (which masked the *log* spam but not the *CPU* spin).

### Fixed — Context Graph count queries, query scoping, SWM promote ownership

- **RC.12 Context Graph count queries fixed** (PR #844, `packages/cli/src/daemon/routes/context-graph.ts`, `packages/node-ui/src/components/ContextGraphOverview/*`): KA URN encoding follow-ups so per-CG KA counts are accurate end-to-end.
- **Query scoping fixes** (PRs #761, #764, #774, #776, #777, #779): structural sub-graph match, allowing same-CG `_meta` + `_shared_memory_meta` on explicit-IRI scoped queries, dropped unsafe dynamic sub-graph enumeration for wallet-scoped cgIds, gossip recipient dual-write parity, F1 mismatch regression coverage, relaxed over-strict SPARQL PREFIX validation. Net effect: `agent.query()` returns the right rows against curated sub-graphs and the workspace-scoped surface.
- **Durable SWM promote ownership enforcement** (PR #754): a promote-to-canonical that doesn't own its source SWM is now rejected.
- **SWM `prov:wasAttributedTo` binds to agent DID, not peer ID** (PR #756, #748): assertions in the shared sub-graph carry the canonical agent identifier so cross-node attribution stays stable across libp2p peer-id rotation.
- **`dkg:subGraphName` emitted from assertion-lifecycle writers** (PR #696, #770): publish receipts in sub-graph contexts now carry the sub-graph label, fixing UI / query routing for sub-graph publishes.

### Fixed — Misc

- **Receiver defensive `cg-id` lookup + RS backfill for pre-`cd68fa689` KCs** (PR #763): cores that received gossip before the chain `cgId` materialised in their meta graph were skipping the per-cgId promotion; lookups now plumb the on-chain id end to end (PR #cd68fa68 follow-up), and a one-shot backfill walks the historical KCs.
- **Greenfield ACK / profile / devnet fallout** (PR #820): bind ciphertext + `isImmutable` into V10 ACK digests, flexible `createProfile` caller for the rebased rc.12 baseline.
- **Hermes `API_SERVER_KEY` forwarded** (PR #794, #795, #799): Node UI chat survives Hermes v0.15.0's auth tightening.
- **`graph-viz` SPARQL protocol headers pin after caller headers** so callers can't accidentally override the `Accept` / `Content-Type` envelope.
- **`graph-viz` `_triples` normalised on ingress** (PR #692, #768): `removeTriples` now round-trips across bracket styles.
- **CLI strict-TS double-cast for doctor config overlay**; **`dkg doctor` `safe to delete releases/` advisory gated** (PR #750 follow-up); **store-wizard red-tests + sparql-http default** (PR #791 follow-up).
- **Validate context graph write targets** (PR #761): a published assertion can no longer name a CG it doesn't belong to.
- **Slither annotations** for the random-sampling contracts to mute the documented-safe findings in CI.

### Operations — Testnet contract redeploy required

- **Base Sepolia (chainId 84532)**: full V10.1 contract surface redeployed off the rc.12 commit. Hub at `0xC056e67Da4F51377Ad1B01f50F655fFdcCD809F6` and Token at `0x2A58BdD13176D85906D804cdbFFA0D9119282DC8` retain their addresses; every other contract address rotates. `chainResetMarker` bumped accordingly; first boot on rc.12 wipes per-node chain-derived state.
- **Operator action**: bump `@origintrail-official/dkg` to `10.0.0-rc.12`, restart the daemon, run `dkg doctor` to verify. Stakers with V10 conviction NFTs are unaffected (positions live on `ConvictionStakingStorage` keyed by `identityId`, which is preserved across the contract-address rotation since identities aren't redeployed).

## [10.0.0-rc.11] - 2026-05-26

### Added — Node release visible on the libp2p wire

- **`DKGNodeConfig.nodeVersion` → libp2p `nodeInfo.userAgent`** (`packages/core/src/types.ts`, `packages/core/src/node.ts`, `packages/agent/src/dkg-agent-types.ts`, `packages/agent/src/dkg-agent.ts`, `packages/cli/src/daemon/lifecycle.ts`, `packages/agent/test/dkg-agent-diagnostics.test.ts`): the daemon now sets `nodeInfo.userAgent` at `createLibp2p()` time to `dkg/<semver>` (read from the running CLI's `package.json`). libp2p's identify protocol ships that string in its `agentVersion` PB field, and every remote peer stores it under `Peer.metadata.AgentVersion`. Closes the version-on-the-wire gap that surfaced during the v10.0.0-rc.10 rollout: before this change `/api/peer-info` could tell operators *which* protocols a peer spoke but not *which DKG release* they were running, leaving "did the network pick up the upgrade?" answerable only by SSHing into each Core node. `getPeerDiagnostics()` now surfaces the decoded string as `peerStore.nodeVersion` (kept distinct from libp2p's `AgentVersion` wire name because in the DKG context "agent" already means `DKGAgent`). Pre-rc.11 peers fall through to libp2p's `js-libp2p/<version>` default — itself a useful "peer hasn't adopted the version-advertisement convention yet" signal. `peerStore.protocolVersion` (libp2p's `ProtocolVersion`, default `ipfs/0.1.0`) is exposed alongside under its libp2p name since it's truly libp2p's protocol version, unrelated to DKG. Lets a single Core node answer "what's every peer's DKG version?" by iterating `/api/agents` → `/api/peer-info?peerId=…` and reading `peerStore.nodeVersion`.

### Added — Operator override for auto-update install source

- **`autoUpdate.source: 'auto' | 'npm' | 'git'` config flag** (`packages/cli/src/config.ts`, `packages/cli/src/daemon/state.ts`, `packages/cli/src/daemon/lifecycle.ts`, `packages/cli/src/cli.ts`, `packages/cli/src/daemon.ts`, `packages/cli/test/resolve-standalone-install.test.ts`, `packages/cli/vitest.unit.config.ts`): operators can now pin which auto-update path the daemon uses by setting `autoUpdate.source: "npm"` (force npm-registry install into the blue/green slot via `performNpmUpdate`) or `"git"` (force git clone + `pnpm build` via `performUpdate`) in `~/.dkg/config.json`. Default is `"auto"` (omitted), which preserves today's behaviour of probing for a `.git` directory at the working tree root. The fix for Core nodes — beacon-01 is the canonical case — that were originally cloned but should now track npm releases: their `.git` directory was routing the auto-updater through the git-build path on every polling cycle, and during the v10.0.0-rc.10 rollout that path picked up a `main`-HEAD commit which then deadlocked the daemon (see the companion shutdown-timeout fix). Setting `source: "npm"` flips the next polling cycle to `npm install @origintrail-official/dkg@<version>` into the inactive blue/green slot — verified in `auto-update.ts:_performNpmUpdateInner` to use the same slot layout as the git path, so no filesystem prep is required to switch. New helper `resolveStandaloneInstall(source?)` in `daemon/state.ts` honours the override, seeds the shared `daemonState.standaloneCache` memo so every later caller (status route, `dkg update` CLI, MCP `dkg_status`) reads the same answer. Network-level default supported via `NetworkConfig.autoUpdate.source` for future testnet/mainnet defaults. Operators on a stuck Core node migrate by: stop daemon → add `"source": "npm"` under `autoUpdate` in config.json → restart. Rollback is removing the field. Recommended operator-template default flip (to `source: "npm"` for fresh installs) and the `dkg migrate-to-npm` CLI subcommand belong to a follow-up PR.

### Fixed — Daemon no longer hangs forever when graceful shutdown deadlocks

- **Hard shutdown timeout in `shutdown()`** (`packages/cli/src/daemon/shutdown.ts`, `packages/cli/src/daemon/lifecycle.ts`, `packages/cli/src/cli.ts`, `packages/cli/test/shutdown-timeout.test.ts`, `packages/cli/test/foreground-supervisor.test.ts`): the daemon's graceful-shutdown closure (SIGINT, SIGTERM, and the auto-update-triggered restart at `lifecycle.ts:1113`) is now wrapped in a 15s wall-clock deadline via the new `raceShutdownWithTimeout` helper. If any cleanup `await` (most often `agent.stop()` waiting on in-flight libp2p reads — the deadlock pattern observed on beacon-01 during a mid-deploy upgrade that left it a zombie until `kill -9`) exceeds the deadline, the process logs `[shutdown-timeout]` once and force-exits with the original exit code plus a `SHUTDOWN_FORCED_OFFSET = 100` (so `shutdown(0)` → exit 100, `shutdown(DAEMON_EXIT_CODE_RESTART=75)` → exit 175). Both supervisors in `cli.ts` (background `runDaemonSupervisor` + interactive `runForegroundSupervisor`) call `decodeForcedExitCode()` to strip the offset, so a forced restart still triggers respawn and a forced final exit still terminates the supervisor — operator intent is preserved through the deadlock. Forced exits are surfaced with a `[supervisor] previous worker forced-exited` console warning so the signal is grep-able alongside the worker's `[shutdown-timeout]` log line, and the [100, 199] exit-code band gives Datadog/Prometheus a clean target for a "daemon couldn't shut down gracefully" alert. Safety net only — the root-cause fix (plumbing an `AbortSignal` through `DKGNode.stop()` to long-await sites) is the next PR in the core-stability workstream; this lands first to stop the network-wide bleed.

### Added — Core-stability hardening (PR-6: AbortSignal plumbing through DKGNode.stop())

- **`DKGNode.stopSignal` + `composeAbortSignals` + `readAllWithSignal`** (`packages/core/src/node.ts`, `packages/core/src/protocol-router.ts`, `packages/core/test/protocol-router-abort.test.ts`): `DKGNode` now owns a per-start-cycle `AbortController`; `stop()` aborts it as its **first** action (before `libp2p.stop()`), so any in-flight stream read can bail out immediately rather than wedge on a silent peer / dead relay. `ProtocolRouter.send` now reads `this.node.stopSignal` and composes it with the per-attempt deadline signal via `composeAbortSignals` (`AbortSignal.any` when available, manual composer fallback) before passing it to a new `readAllWithSignal(stream, max, signal)` wrapper around the existing `readAll`. The wrapper races the for-await loop against the abort signal — when it fires, the underlying stream is `.abort()`-ed which makes the iterator throw on its next `.next()` call, propagating the abort error out of the for-await naturally. This is the graceful counterpart to PR-1's hard-timeout safety net (#655): PR-1 force-exits at 30s; PR-6 lets the routine shutdown path drain in milliseconds instead. Scope-limited: this PR plumbs the signal through the one known sync-read deadlock site (the `await readAll(stream, this.maxReadBytes)` at `protocol-router.ts:747` that was wedging beacon-01 in the rc.10 investigation); broader long-await sites can opt in by composing `node.stopSignal` the same way. 13 unit tests (`test/protocol-router-abort.test.ts`): pre-aborted signal, abort-during-read (the beacon-01 repro using a `hangingStream` helper), abort-after-completion, no-signal passthrough, stream.abort() side-effect, listener-cleanup on success + on abort, `composeAbortSignals` truth table (undefined ×2, lone signal, either-fires-composed-fires, pre-aborted inputs).

### Added — Core-stability hardening (PR-8: chain provider filter log-spam silencer)

- **`createFilterErrorSilencer` + provider.on('error') wire-up** (`packages/chain/src/filter-error-silencer.ts`, `packages/chain/src/evm-adapter.ts`, `packages/chain/test/filter-error-silencer.test.ts`, `packages/chain/vitest.unit.config.ts`): silences the `eth_getFilterChanges` "filter not found" log spam from long-lived ethers `JsonRpcProvider` polling — the symptom that produced **134 MB of `daemon.log` in 24 hours** on beacon-01 during the rc.10 deadlock investigation and masked the actual shutdown-deadlock signal. The classifier (`isFilterNotFoundError`) matches on EITHER the canonical `"filter not found"` substring OR the RPC error code (`-32602` with a filter-mentioning message), so it catches the ethers-wrapped and ethers-unwrapped shapes both. The silencer is stateful — dedup-logs filter errors at most once per 5-minute window (configurable) with a "(N similar errors suppressed in the previous window)" suffix so the suppression itself is auditable. Non-filter provider errors fall through to the default `console.warn` path so they remain visible — this PR is log-spam suppression, NOT recovery. The Hub TTL-refresh fallback in `evm-adapter.ts:startHubRotationListener` already keeps the contract-address pair fresh while filters are silently failing, so the application-correctness side is already covered. Full `RecoverableEventProvider` filter-recreation logic is deferred pending a live RPC repro the planning spike couldn't reproduce locally. New `packages/chain/vitest.unit.config.ts` mirrors the cli pattern from rc.10 PR-2 (#659) — skips the Hardhat globalSetup so this pure-logic test runs in 3s instead of timing out against the 120s hardhat boot. 15 unit tests (`test/filter-error-silencer.test.ts`): classifier truth table (plain message / ethers-wrapped / direct err.code / unrelated -32602 / other provider errors / non-Error inputs) + silencer behaviour (first-emit-immediate / suppress-within-window / re-emit-after-window-with-count / no-suffix-when-zero-suppressed / 5min default window / resetForTest / non-filter passthrough doesn't bump counters) + production wiring sanity.

### Added — `dkg migrate-to-npm`

- **`dkg migrate-to-npm` CLI subcommand + operator runbook** (PR-7, `packages/cli/src/migrate-to-npm.ts`, `packages/cli/src/cli.ts`, `packages/cli/test/migrate-to-npm.test.ts`, `packages/cli/vitest.unit.config.ts`, `docs/operator/MIGRATE_TO_NPM.md`, `docs/RELEASE.md`): operator-facing one-shot conversion from a git-checkout install (`~/dkg-v9/` with `.git`, `packages/`, `package.json`, etc.) to the npm-pinned auto-update path. **Correction vs the planning doc**: the original plan said `.git` was the marker that flips `isStandaloneInstall()` — it's not. `findPackageRepoDir` (in `packages/core/src/blue-green.ts`) walks for the marker pair `package.json + packages/` together; the load-bearing rename is therefore **`package.json`**, not `.git`. The script still renames `.git` as a cosmetic-only step (eliminates the "operator sees `.git`, runs `git pull`" confusion that was the original incident driver) but tags it `[cosmetic]` in dry-run output to make the distinction visible. Also writes `autoUpdate.source = "npm"` into `~/.dkg/config.json` as forward-compat with PR-2 (#659): silently ignored until that lands, then becomes the explicit pin. Dry-run by default (no flags), `--apply` to mutate, `--force` to bypass the daemon-alive blocker. **State-orphan blocker is hard-refusal with NO `--force` override**: if `dkgDir()` currently resolves to `~/.dkg-dev` (the monorepo-state branch in `resolveDkgConfigHome`), post-migration the walker returns null and the CLI silently reads from `~/.dkg` instead — a fresh-install-shaped catastrophe. The script detects this and refuses with the exact `mv ~/.dkg-dev ~/.dkg` or `DKG_HOME=~/.dkg-dev` remediation. **Does NOT touch**: `packages/`, `node_modules/`, `pnpm-lock.yaml` (current `dkg` PATH symlink depends on them; runbook documents the `npm install -g` follow-up after a soak period). 19 unit tests: load-bearing vs cosmetic split, alreadyMigrated short-circuit (npm/undefined + still-git contrast), config-write skip-when-already-npm, alive-check with/without `--force` (blocker ↔ warning downgrade), orphan-blocker hard-refusal, applyPlan refuses when blockers present, real-fixture happy-path (renames + config-write end-to-end), existing config preservation under dotted-key patch, idempotent re-run. Runbook covers: why migrate, pre-checks, step-by-step + verification + rollback, orphan handling, `--force` bypass, optional soak-then-cleanup path, `logrotate.d` snippet for the beacon-01 134MB/24h log-spam issue (with `copytruncate` rationale).

### Added — Core-stability hardening (PR-5: AutoNAT boot self-probe)

- **AutoNAT-driven NAT-status watcher for core nodes** (`packages/cli/src/daemon/nat-status.ts`, `packages/cli/src/daemon/lifecycle.ts`, `packages/cli/test/nat-status.test.ts`): subscribes to libp2p's `self:peer:update` eventbus after `agent.start()` for `nodeRole=core` and updates a module-level NAT-status cache (`'public' | 'private' | 'unknown'`) that PR-4's `/api/status` `relay` block reads. The classifier (`classifyAddressesForNat`) is a pure function over multiaddr strings that treats DNS-form / public-IPv4 / public-IPv6 addresses as `'public'` and rejects RFC1918, CGNAT (100.64/10 — the Tailscale range that caused the beacon-01 silent miscategorization), loopback, link-local, IPv6 ULA, and circuit-relay-only as `'private'`. A 60s soft-timeout reclassifies once when no AutoNAT event has fired, so nodes behind a closed firewall surface a `'private'` status block instead of indefinite `'unknown'`. Observability-only here — PR-3 (#661) owns the refuse-to-boot gate via `core.allowDegradedRelay`; this PR's classifier and that PR's gate share the same address-set view, so when both merge they classify consistently without coupling at the file level (no shared `daemonState` slot writes, no merge conflict). The watcher returns a `stop()` handle that lifecycle's shutdown closure calls to avoid leaking listeners across supervisor respawns. 29 unit tests (`test/nat-status.test.ts`): table-driven classifier coverage (TEST-NET-3 documentation range, real-world public IPv4, RFC1918 192/10/172, CGNAT 100.64, link-local, IPv6 loopback/link-local/ULA/public, DNS form, circuit-relay-only, beacon-01 Tailscale repro) + transition-only callback semantics + soft-timeout firing exactly once + stop() listener removal + module-cache `getNatStatus()` accessor.

### Added — Core relay capability sanity check (boot-time)

- **`[CORE-PREREQ]` boot-time relay capability check** (`packages/cli/src/daemon/core-prereq-check.ts` (new), `packages/cli/src/daemon/lifecycle.ts`, `packages/cli/src/daemon.ts`, `packages/cli/src/config.ts`, `packages/cli/test/core-prereq-check.test.ts` (new, 40 cases), `packages/cli/vitest.unit.config.ts`): the daemon now classifies its actually-bound multiaddrs after `agent.start()` and surfaces a structured `[CORE-PREREQ]` log line if a `nodeRole: "core"` node looks degraded — i.e. all bound listenAddresses are non-routable (loopback / RFC1918 / CGNAT / IPv6 ULA / link-local) and no `announceAddresses` entry classifies as public. **beacon-01** was the canonical Tailscale-only case during the v10.0.0-rc.10 incident (bound only to `/ip4/100.99.142.87/tcp/4001`, CGNAT — could not have served traffic regardless of slot state). Wildcard listenAddresses (`/ip4/0.0.0.0` / `/ip6/::`) classify as the best class across `os.networkInterfaces()` so dual-homed hosts are correctly recognised as public. New config field `core.allowDegradedRelay?: boolean` gates the behaviour: `true` (default) is warn-only — backwards-compatible, no behaviour change for any operator on this PR; `false` is opt-in refuse-to-boot via `shutdown(1)`. Pure-logic classifier module (`checkCoreRelayPrereqs(opts)`) takes no I/O dependency, allowing 24 deterministic unit tests (per-class smoke tests for all 11 classes incl. multicast, wildcard-delegation matrix, 7 canonical cases from the plan including the beacon-01 reproduction + 5 safety cases for edge-node skip / empty listenAddresses / mixed-class reason summarisation / announceAddresses rescue / missing-rescue hint). Test added to `vitest.unit.config.ts` for fast `pnpm test:unit` runs (~2s vs ~2min via the default hardhat-bootstrapping config).

### Added — Relay metrics in `/api/status`

- **`relay` block in `/api/status` response** (`packages/cli/src/daemon/relay-status-block.ts` (new), `packages/cli/src/daemon/routes/status.ts`, `packages/cli/src/daemon.ts`, `packages/cli/src/daemon/state.ts`, `packages/cli/test/relay-status-block.test.ts` (new, 10 cases), `packages/cli/vitest.unit.config.ts`): `/api/status` responses now include a uniform `relay` block on every node — edge and core — so monitoring (Datadog, Prometheus, dashboards, ad-hoc curl) parses one schema rather than switching on `nodeRole`. Block surfaces `isCore`, `reservationsHeld` / `reservationCapacity` (Core saturation alert: `reservationsHeld === reservationCapacity`), `activeCircuits`, `bytesIn` / `bytesOut` (stringified BigInt — preserves precision past `Number.MAX_SAFE_INTEGER` for long-uptime Cores; `JSON.stringify` would otherwise throw), `natStatus` (`'public' | 'private' | 'unknown'` — defaults to `'unknown'` today, populated by a planned AutoNAT-driven boot self-probe via new `daemonState.natStatus` rendezvous slot), `listenAddresses` (post-wildcard-expansion via `agent.multiaddrs`), `announcedAddresses` (operator-configured). Edge nodes get the same keys with role-irrelevant fields as `null` (the one exception: `reservationsHeld: 0` rather than `null` — zero is "no held reservations", null would be "field not applicable" and is semantically wrong for edges). Logic extracted to pure `buildRelayStatusBlock(opts)` helper for fast unit-test coverage (10 cases incl. edge baseline, core full population, boot-race window where `isCore: true` but `relayStats: null`, BigInt round-trip past 2^53, small-value stringification uniformity, `natStatus` pass-through matrix, key-set parity between edge and core).

### Added — Supervisor positive-liveness probe

- **Supervisor positive-liveness probe** (PR-9, `packages/cli/src/daemon/supervisor-liveness.ts`, `packages/cli/src/daemon.ts`, `packages/cli/src/cli.ts`, `packages/cli/test/supervisor-liveness.test.ts`, `packages/cli/vitest.unit.config.ts`): the `dkg start` supervisor loops (`runDaemonSupervisor`, `runForegroundSupervisor`) now run a periodic TCP-connect probe against the worker's API port. After `LIVENESS_CONSECUTIVE_FAILURES_TO_KILL = 5` consecutive unresponsive ticks (~2.5 min at the default `LIVENESS_PROBE_INTERVAL_MS = 30s` tick / `LIVENESS_PROBE_TIMEOUT_MS = 5s` per-probe budget), the supervisor `SIGKILL`s the worker so the existing exit-watcher / respawn path takes over. PR-1 (#655) catches the specific "deadlocked-shutdown" shape via a hard timeout in the shutdown handler; this PR catches the **generic zombie shape** (HTTP listener dead while the process remains alive), defense-in-depth. The probe is a plain TCP connect rather than HTTP so it sidesteps the supervisor needing to load auth tokens, parse responses, or special-case 404/405 from any non-`/health` path. Self-throttling: a slow in-flight probe blocks the next tick instead of stacking, so a CPU-bound worker doesn't generate a probe storm. The watcher waits up to 10s for the worker to write `~/.dkg/api.port`; headless workers (benchmarks, tests) that never bind a listener are silently skipped instead of false-positive-killed. Gated by `DKG_SUPERVISOR_LIVENESS_PROBE` (default on; set to `off`/`0`/`false` to disable; unknown values fail safe to enable). 24 unit tests cover the env-gate truth table (15 cases via `it.each`), threshold tripping, counter reset on success, post-fire counter reset (preventing kill-respawn loops during slow respawn), `stop()` halting probes, no-pile-up under slow probes, healthy-probe quiet, and real `node:net` socket round-trips for `probeWorkerAlive` (listener-accepts → `true`, closed-port → `false`, TEST-NET-1 hang → timeout `false`).

---

## [10.0.0-rc.10] - 2026-05-25

**Testnet contract redeploy required** — `chainResetMarker` bumped to `v10-rc10-rfc38-mainnet-ready-2026-05-25`; first boot on rc.10 wipes per-node chain-derived state for any operator still on the rc.8/rc.9 marker. Hub at `0xC056e67Da4F51377Ad1B01f50F655fFdcCD809F6` and Token at `0x2A58BdD13176D85906D804cdbFFA0D9119282DC8` retain their addresses; every other contract in `deploy/active/` was redeployed and re-registered against the V10 Hub. `DKGStakingConvictionNFT.finalizeMigrationBatch(497)` was executed by the Hub owner post-deploy to seal `ConvictionStakingStorage.v10LaunchEpoch = 497` — the canonical retroactive-rewards cutoff for the rc.10 testnet (tx `0xc72d84cbc01a35f010012a19bc7a10301dfe8a641bfffc0bb3f8ea45d1cbf7eb`). New addresses are committed in `packages/evm-module/deployments/base_sepolia_v10_contracts.json`; agents discover them via `Hub.getContractAddress(...)` at runtime.

**OT-RFC-38 Phase A — Edge curator can publish curated CGs to Verifiable Memory; members verify post-decrypt; outsiders verify via attestation tokens.** Builds on the SPEC_CG_MEMORY_MODEL surface below by closing the curated-CG publish path end-to-end. Edge curators (no on-chain `identityId`) now create curated CGs, share private SWM with named members, publish to VM with `attributionId=0` (no-attribution mode the V10 contract already supports), and ship every member-attested verification artifact a downstream verifier needs. RFC: [`docs/specs/SPEC_CG_HOSTING_MEMBERSHIP.md`](./docs/specs/SPEC_CG_HOSTING_MEMBERSHIP.md) §1.1 and §7.1.1.

### Added — OT-RFC-38 Phase A

- **LU-5: curated payload AEAD wrap + identity-less publish + UI honesty** (`packages/core/src/proto/publish-intent.ts`, `packages/core/src/crypto/v10-publish-payload.ts`, `packages/publisher/src/storage-ack-handler.ts`, `packages/publisher/src/ack-collector.ts`, `packages/publisher/src/dkg-publisher.ts`, `packages/agent/src/dkg-agent.ts`, `packages/cli/src/publisher-runner.ts`, `packages/node-ui/src/ui/views/MemoryLayerView.tsx`, `packages/node-ui/src/ui/views/project/components.tsx`): adds `isEncryptedPayload` (field 14) to `PublishIntent` so cores can ACK curated payloads they cannot decrypt — they verify `chunkDigest` + `byteSize` against the persisted ciphertext, never N-Quad-parse. New `v10-publish-payload.ts` is an AES-256-GCM AEAD helper keyed off the CG chain key (HKDF). Publisher's `storage-ack-handler` branches on `isEncryptedPayload` to persist opaque ciphertext under `(contextGraphId, batchId)` and signs the existing V10 digest verbatim (no contract change). `DKGAgent._resolveEncryptInlinePayload` wires the encryption hook on the agent side for curated CGs. **Critical fix**: dropped the `publisherNodeIdentityId > 0n` gate from `DKGPublisher` — edge agents now reach the on-chain publish path in no-attribution mode (`attributionId=0n`), where the V10 contract accepts the publish as an authority delegation against the curator's wallet signature. UI surfaces the result honestly: green "Published to Verifiable Memory" with full `txHash` + block number only when the daemon reports `status === 'confirmed'` AND a `txHash` is present, red "NOT published to Verifiable Memory" with the on-chain failure reason otherwise. This closes the §1.1 bug surfaced in the RFC.
- **LU-7: SWMCatchupRequest endpoint** (`packages/cli/src/daemon/routes/memory.ts` `POST /api/shared-memory/catchup`): the daemon route lifts the existing per-peer sync substrate (`PROTOCOL_SYNC`) into a caller-initiated catchup primitive. Body `{ contextGraphId, peerId?, includeDurable?, perPeerBudgetMs? }`. Public CGs (`accessPolicy=0`) accept anonymous catchup; curated CGs (`accessPolicy=1`) run the responder's existing `authorizePrivateSyncRequest` against the requester's signed envelope (member → allowed; outsider → denied). When `peerId` is omitted the route fans out to every connected peer in parallel, bounded by `perPeerBudgetMs`. Returns per-peer triple-insertion counts so callers can tell who served the request.
- **LU-8: member post-decrypt root recompute + BatchRejected gossip** (`packages/agent/src/swm/verify-batch.ts`, `packages/cli/src/daemon/routes/memory.ts` `POST /api/shared-memory/{verify-batch,report-batch-rejection}`, `packages/agent/test/verify-batch.test.ts`): pure recompute helper hashes the member's decrypted plaintext quads using V10's `computeFlatKCRootV10` + `computeFlatKCMerkleLeafCountV10` and compares against the on-chain anchor. Mismatch returns `{ ok: false, reason: 'root-mismatch', actualRoot, expectedRoot, leafCount }`. The `verify-batch` daemon route requires callers to POST `{ contextGraphId, expectedMerkleRoot, quads, privateRoots? }`. `quads` is **required** (HTTP 400 on omission); the daemon cannot safely reconstruct a single batch from the local SWM / data graph because that graph can contain triples from other batches in the same context graph, so the verifier API stays caller-supplied-plaintext only. The `report-batch-rejection` route ships a structured `BatchRejection` record through SWM gossip via `agent.share()` so other members can sanity-check and refetch from a different host.
- **LU-9: member-attestation token mint + outsider verification** (`packages/agent/src/swm/member-attestation.ts`, `packages/cli/src/daemon/routes/memory.ts` `POST /api/attestation/{mint,verify}`, `packages/agent/test/member-attestation.test.ts`): a member signs an envelope binding `(chainId, kavAddress, contextGraphId, batchId, merkleRoot, plaintextLeafHash, attesterAddress, attestedAt)` with `keccak256(abi.encodePacked(...))` + EIP-191 secp256k1 (matches V10 chain-side signature layout — outsiders can hand-verify against `ContextGraphStorage`). The daemon `mint` route signs via the node's chain adapter; the `verify` route runs four checks: signature recovery, signer-matches-attester, optional `candidateLeaf` rehash against `plaintextLeafHash`, optional async `membershipResolver` chain hook for "was this attester a CG member at `attestedAt`". Returns structured `{ ok, recoveredSigner, signerMatchesAttester, leafCheck, membership, reason? }` so consumers can decide based on which checks passed.
- **`enumerate-cg-hosts` helper** (`packages/agent/src/swm/enumerate-cg-hosts.ts`, `packages/agent/test/enumerate-cg-hosts.test.ts`): library helper distinct from `enumerate-cg-members`. Returns the dialable peer set the LU-7 catchup primitive will try in turn (Phase A: all connected peers minus self; Phase B will refine to a sharding-table-eligible subset once shard count > 1).
- **Devnet integration tests** (`scripts/devnet-test-rfc38-*.sh`): 11 standalone end-to-end scenarios, all driven through the daemon HTTP API. `devnet-test-rfc38-all.sh` runs the full suite. Covers LU-5 (curated + public), LU-7, LU-8, LU-9, LU-10 (public-CG regression sweep), `e2e` (LU-5→LU-7→LU-8→LU-9 composed), `cross-cg` isolation (member of CG-A cannot decrypt CG-B), `multi-member` (3 distinct member wallets cross-verify the same batch + cross-verify each other's attestations), `scale` (50 triples / 25 KAs single batch), `late-joiner` (member-from-curator + member-from-member-with-curator-offline + the documented LU-6 cores-only gap as a passing fail-soft assertion). Re-runnable; every CG id is timestamp-suffixed.
- **`./scripts/devnet.sh restart-node N`** + UI proxy `DEVNET_UI_NODE` env (`scripts/devnet.sh`, `packages/node-ui/vite.config.ts`): operator surfaces for restarting one node without wiping state, and pointing the Vite dev-server proxy at any devnet node (not hardcoded to node 1). Lets the user test the edge-publish path from `http://localhost:5173/ui/` against node 5 instead of needing custom curl.

### Added — OT-RFC-38 Phase B (cores auto-host curated CGs)

- **LU-6 substrate hosting on cores — four-path discovery model** (`packages/agent/src/dkg-agent.ts` `1737-1803` chain-event auto-subscribe + `1810-1838` discovery beacon + `9347-9376` periodic reconciler, `packages/cli/src/daemon/routes/memory.ts` `960-977` operator-driven enrolment): cores now subscribe to a curated CG's encrypted SWM gossip topic without any per-CG operator action in the common case. Four discovery paths feed into the same `reconcileSwmHostModeSubscription` per-CG idempotency funnel: (1) the chain-event poller's `ContextGraphCreated` handler subscribes any sharding-table core with `swmHostMode` enabled the moment the curator commits a `nameHash`; (2) cores with host mode enabled subscribe to the global discovery topic and pick up curator-driven beacon re-announces (`BEACON_REANNOUNCE_INTERVAL_MS`-cadenced) for pre-registration / off-chain-only CGs the chain event never sees; (3) the `hostModeReconcilerTimer` periodic sweep (default 30s, idempotent) heals any event the first two paths missed across restarts / chain re-orgs; (4) `POST /api/shared-memory/host-mode/subscribe` lets an operator manually enroll a specific core for a specific CG when the first three paths don't apply (off-chain CGs, coverage gaps during rc.10 rollout). The SCENARIO D `devnet-test-rfc38-late-joiner.sh` path is the end-to-end happy case: a member that missed live gossip while offline catches up purely from cores that subscribed via paths 1+3, decrypts with the local AEAD chain key, and applies. The `SwmSenderKey` two-layer Sender Keys construction in `packages/core/src/crypto/swm-sender-key.ts` is wired to the workspace-gossip topic via the host-mode handler, with TTL + byte-cap staging policies enforced in the host-mode store. Operator playbook for path 4 ships in `docs/runbooks/RUNBOOK_HOST_MODE_MANUAL_SUBSCRIBE.md`.

### Deferred (Phase B sub-task, tracked for follow-up)

- **Sharding-table-aware host-mode selection**: today every sharding-table core that hears a chain event / beacon for a curated CG hosts every such CG. This is correct and cheap while `shards === 1` (the rc.10 testnet posture); once `shards > 1` it becomes a routing problem — only the cores whose sharding-table slot covers a CG's anchor should pick it up. The selection hook lives at the `reconcileSwmHostModeSubscription` entrypoint; the routing predicate is the remaining work.
- **Cross-core ciphertext replay**: a fresh sharding-table core that joins after a curator has already gossipped N opaque envelopes can only host the envelopes it sees from now forward — there is no core-to-core catchup of the historical ciphertext stream. Late-joining members served by such a core will miss the pre-join history unless another current member is also online. Path forward: extend the existing per-peer sync substrate (`PROTOCOL_SYNC`) with an opaque-ciphertext replay verb that's authorized by sharding-table membership rather than CG-allowlist membership, so cores can serve other cores without holding chain keys.

---

**Context Graph memory model — edge agents can create curated CGs**: the on-chain Context Graph surface no longer accepts per-CG hosting committees or per-CG ACK quorums; hosting and ACK quorum are network-level concerns (sharding table + `parametersStorage.minimumRequiredSignatures()`). This unblocks edge-node agents who have no on-chain `identityId` from registering invite-only / curators-only CGs — previously the agent SDK threw at register time when `ensureIdentity()` returned `0n`. RFC: [`docs/specs/SPEC_CG_MEMORY_MODEL.md`](./docs/specs/SPEC_CG_MEMORY_MODEL.md). Wire-format break end-to-end (contracts + ABIs + SDK + daemon + CLI + MCP + UI all rev together); no compatibility shim — every package upgrades in lockstep.

### Fixed — Private graph SPARQL filterability (#633)

- **Revert IRI half of `PrivateContentStore.encryptLiteral`** (`packages/storage/src/private-store.ts`, `packages/cli/test/repro-issue-633.test.ts`): the IRI-half of the AES-GCM seal introduced in #379 (commit `de341d88`, a PR #229 round-6 escalation driven by the `github-advanced-security` bot) is rolled back so IRI-position object terms in `_private` graphs land on disk as plaintext again. SPARQL filterability is restored for `eventType`, `bizStep`, `bizLocation`, `readPoint`, `disposition` — the headline EPCIS read-path regression in #633 is no longer reproducible for those filters. The literal half of the seal stays in place (it addresses the original ST-2 audit finding "private literal values must not land on disk in plaintext"), so `epc`, `action`, `eventTime`-range filters still bind ciphertext at the SPARQL layer and need a blind-index or in-app post-decrypt pass to fully close #633 — tracked as a follow-up. Retroactive private data sealed under the IRI-half between 2026-05-05 and this commit remains readable via the safe API (`PrivateContentStore.getPrivateTriples`); the existing `decryptLiteral` key chain still strips the `I|` envelope tag for legacy reads. `getPrivateTriples` additionally normalises bare-IRI bindings back to N-Triples form (`<iri>`) so the `Quad.object` contract stays stable for downstream consumers like `publisher/src/access-handler.ts` that interpolate the value verbatim into an N-Quads string. Regression captured by 5 fresh assertions in `packages/cli/test/repro-issue-633.test.ts`.

### Changed — Context Graph memory model

- **LU-1: contracts + chain adapter** (`packages/evm-module/contracts/ContextGraphs.sol`, `packages/evm-module/contracts/storage/ContextGraphStorage.sol`, `packages/evm-module/contracts/libraries/KnowledgeAssetsLib.sol`, `packages/evm-module/abi/`, `packages/chain/src/chain-adapter.ts`, `packages/chain/src/evm-adapter.ts`, `packages/chain/src/mock-adapter.ts`): removed `hostingNodes` argument, per-CG `requiredSignatures` argument, `setHostingNodes` / `updateQuorum` setters, `getContextGraphRequiredSignatures` / `getHostingNodes` / `isHostingNode` getters, `HostingNodesSet` / `QuorumUpdated` events, `MAX_HOSTING_NODES` constant, the matching field on the `ContextGraph` struct (struct repacked tight to 2 slots), and the corresponding fields on `ContextGraphCreated` + `getContextGraph` return tuple. TypeScript `ChainAdapter.createOnChainContextGraph` lost `participantIdentityIds: bigint[]` and `requiredSignatures: number`; `getContextGraphParticipants` deleted. ABIs regenerated and re-pinned in `packages/chain/test/abi-pinning.test.ts`.
- **LU-2: agent SDK + daemon + CLI + verify rewire** (`packages/agent/src/dkg-agent.ts`, `packages/agent/src/dkg-agent-types.ts`, `packages/cli/src/daemon/routes/context-graph.ts`, `packages/cli/src/cli.ts`, `packages/cli/src/api-client.ts`, `packages/publisher/src/verify-collector.ts`): `DKGAgent.createContextGraph` no longer auto-adds the creator's `identityId` to `_meta`; `DKGAgent.registerContextGraph` no longer falls back to `ensureIdentity()` and no longer forwards dead fields to the chain adapter (the change that unblocks edge agents). `DKGAgent.verify()` quorum reads from `chain.getMinimumRequiredSignatures()`. Daemon `POST /api/context-graph/create` strips `participantIdentityIds` / `requiredSignatures` from request bodies with a one-line deprecation warn for older clients. `dkg context-graph create` lost `--participant-identity-id` / `--required-signatures` flags. `VerifyCollector` takes `requiredSignatures` as optional and falls back to the system minimum.
- **LU-3: MCP + UI dials in AI-dev language** (`packages/mcp-dkg/src/tools/setup.ts`, `packages/mcp-dkg/src/client.ts`, `packages/node-ui/src/ui/components/Modals/CreateProjectModal.tsx`, `packages/node-ui/src/ui/api.ts`): the MCP tool `dkg_context_graph_create` gained `sharing: 'open' | 'invite-only'` (default `invite-only`, maps to `accessPolicy` `0` / `1`) and `contribution: 'open' | 'curators-only'` (default `curators-only`, maps to `publishPolicy` `1` / `0`). `CreateProjectModal` promoted the Contribution radio out of "coming soon" and renamed UI labels: "Access" → "Sharing", "Publish Policy" → "Contribution". Defaults match the MCP tool — both dials lock down by default. The on-chain wire format is unchanged.
- **LU-4: docs** (`docs/specs/SPEC_CG_MEMORY_MODEL.md`, this CHANGELOG, `docs/specs/SPEC_V10_IDENTITY_AND_ACCESS.md` cross-link): the RFC is now marked `IMPLEMENTED (v1.0)`; legacy "edge-owned CG" terminology retired (the pattern is now the only pattern). §4.0 adds a sequence diagram covering the create + publish flow.

### Added — Testnet recovery + WM persistence

- **`Profile.recreateProfile` for testnet recovery (#574)** (`packages/evm-module/contracts/Profile.sol`, `packages/evm-module/abi/Profile.json`, `packages/evm-module/contracts/storage/ProfileStorage.sol`, `docs/adr/`): an admin-only path that resolves a node's `identityId` from its operational wallet and re-registers a Profile that was orphaned by a redeploy or storage wipe. Recomputes the Ask active set on recovery and asserts that the recovered V10 stake matches the on-chain commitment. Sharding-consistency guard prevents recreate from minting a duplicate node entry if the canonical name hash already resolves to a different node. Documented in a new ADR; covered by `recreateProfile` revert-path tests + an integration test that proves state preservation across recreate.
- **WM persistence durability across daemon restarts (#640)** (`packages/storage/src/`, `packages/agent/src/`, `scripts/graphify-import-repro/`): the in-memory Shared Working Memory backing store now drains in-flight flushes before close, propagates `flushNow` errors instead of swallowing them, and persists durably across restarts so a Ctrl+C / SIGTERM doesn't lose the last few seconds of writes. New repro harness ships under `scripts/graphify-import-repro/` with a `--spawn` mode that rejects pre-existing daemons and a `--no-spawn` mode that asserts the inverse, plus matrix tests covering kill / restart combinations. Closes the "newly imported triples vanish after restart" regression reported on the graphify import path.

### Fixed — RFC-38 integration devnet-sweep triage (#647)

- **T2: defer curated CG random sampling to RFC-39 Phase B** (`packages/evm-module/contracts/RandomSampling.sol`, `packages/evm-module/test/unit/RandomSampling-curated.test.ts`, `packages/evm-module/test/unit/RandomSampling.test.ts`): the PR #630 picker change wired the contract to draw against `getCiphertextChunkCount` for curated CGs, but the off-chain prover at `packages/random-sampling/src/prover.ts` still extracts plaintext leaves via `getMerkleLeafCount`. Devnet sweep confirmed every curated draw produced `V10ProofLeafCountMismatchError` and core nodes published zero proofs. Filtered curated CGs at `_isCGEligible` (returns `false` if `contextGraphStorage.getIsCurated(contextGraphId)` is true) instead of unwinding the picker — the curated branches in `_pickWeightedChallenge` steps 2/3 are retained verbatim so the re-enable is a one-line revert here plus the test unskip, contingent on the prover ciphertext path landing in RFC-39 Phase B. Curated-CG picker test moved to `.skip` with a pointer to where the unskip will happen.
- **T6: skip post-publish `dkg:trustLevel` stamps in KA leaf extraction** (`packages/random-sampling/src/kc-extractor.ts`, `packages/random-sampling/test/kc-extractor.test.ts`): the daemon stamps `dkg:trustLevel` (and the legacy `legacy:trustLevel`) onto KCs after publish to mark verification status. The off-chain prover's leaf extractor was hashing these post-publish triples into the proof leaves while the on-chain anchor was set at publish time and never saw them — causing leaf-count mismatch and `V10ProofLeafCountMismatchError` on every public-CG proof attempt. The extractor now filters both predicates via a `POST_PUBLISH_PREDICATES_TO_SKIP` set. Regression test pinned.
- **T8: devnet publish + cli-invite scripts hardened** (`scripts/devnet-test-publish.sh`, `scripts/devnet-test-cli-invite.sh`, `scripts/_devnet-full-sweep.sh`): `devnet-test-publish.sh` rewritten to use the daemon's `dkg context-graph create` + `dkg context-graph register` CLI (was: hand-rolled HTTP calls with stale request shape). `devnet-test-cli-invite.sh` swapped hardcoded addresses for dynamic discovery, case-insensitive grep on member-status replies, timestamp-suffixed CG slugs for idempotency, corrected the API flow to `sign-join` then `request-join`, and raised polling timeouts. New `_devnet-full-sweep.sh` harness composes the whole suite and is the single command operators should run pre-release to verify a devnet cluster.
- **prover: expanded `rs.tick.data-corrupted` diagnostics** (`packages/random-sampling/src/prover.ts`): on leaf-count mismatch the log now carries `expectedLeafCount`, `actualLeafCount`, `merkleRoot`, `ciphertextRoot`, and the offending KA id — enough signal to triage the next regression without re-running the sweep with `DEBUG=*`.
## [10.0.0-rc.9] - 2026-05-17

**Universal Messenger substrate**: every short peer-to-peer DKG protocol (chat, skill request, query-remote, swm-sender-key, private-access, join-request, storage-ack, verify-proposal) now routes through a single reliability layer with at-least-once delivery, receiver-side dedup keyed by `messageId`, sender-side response cache (256 KiB inline; mark-only beyond), SQLite-persisted retry outbox surviving daemon restart, opportunistic flush on `connection:open`, DHT-walk-on-stall recovery, and per-protocol latency observability via `/api/slo`. Architecture in [`docs/messenger.md`](./docs/messenger.md); operator surfaces in [`docs/messenger-operator.md`](./docs/messenger-operator.md); migration recipe in [`docs/messenger-add-protocol.md`](./docs/messenger-add-protocol.md).

**Wire-format break**: all 8 substrate-routed protocols moved from `/dkg/10.0.0/*` to `/dkg/10.0.1/*`. Both daemons in a pair must be on rc.9 for any of these protocols to negotiate between them; mixed-pair deploys (one node rc.8, one rc.9) surface as `delivered: false, queued: true` outbox entries that drain once both sides upgrade. No backward-compatibility codepath ships — hard cutover keeps the substrate's correctness proofs simple. `/dkg/10.0.0/verify-approval` is the sole `/dkg/10.0.0/*` survivor (not a substrate caller; left bare). See the upgrade order in `docs/messenger-operator.md` § "Upgrade from rc.8 to rc.9".

**V12 + V13 SQLite migrations** run automatically on first rc.9 boot. V12 adds `message_idempotency` + `protocol_outbox` tables (additive — chat continues to write to its V11 column until V13 cuts over). V13 drops the V11 `idx_chat_msgid` partial unique index in favour of the substrate-owned dedup. `chat_messages.message_id` column is **preserved nullable** for hot-rollback safety: rc.8 finds a column it recognises if you have to downgrade. In-flight rc.8 chat-outbox entries should be drained before upgrade (let the daemon idle for one tick cycle, typically 30s); new sends post-upgrade route via the substrate outbox.

### Added — Universal Messenger substrate

- **PR-1 (#542): substrate primitives** (`packages/core/src/proto/reliable-envelope.ts`, `packages/core/src/messenger-types.ts`, `packages/core/src/protocol-outbox.ts`, `packages/node-ui/src/db.ts`, `docs/messenger.md`): introduces the `ReliableEnvelope` Protobuf wire wrapper (`{ messageId, version, tsMs, payload }`), the `MessageIdempotencyStore` + `ProtocolOutboxStore` ports, the generic `ProtocolOutbox` retry helper (5s → 15s → 30s → 60s → 5m → 30m → 2h ladder; per-key inflight guard; stale-snapshot guard lifted from rc.8 #538), and SQLite-backed `SqliteMessageIdempotencyStore` + `SqliteProtocolOutboxStore` against a V12 schema migration. 256 KiB inline response cache budget; oversize responses stored mark-only and surface `RESPONSE_GONE` to duplicate receivers.
- **PR-2 (#543): Messenger evolution + lifecycle wiring** (`packages/agent/src/p2p/messenger.ts`, `packages/agent/src/dkg-agent.ts`, `packages/cli/src/daemon/lifecycle.ts`): the `Messenger` class gains `sendReliable` + `register` substrate surfaces wrapping envelope encode + sender/receiver idempotency + outbox enqueue on recoverable failure; legacy `sendToPeer` path preserved bitwise-compatible for any `/dkg/10.0.0/*` caller. `DKGAgent` wires a `messengerOutboxTimer` periodic tick and piggy-backs `messenger.processOutboxOnConnect` onto its existing `connection:open` handler. `lifecycle.ts` instantiates the SQLite stores against the shared `DashboardDB` and routes `ackTransportFactory.sendP2P` through the Messenger (semantics-identical until `/storage-ack` migrates in PR-11).
- **PR-3 (#544): pilot migration — chat + skill onto `/dkg/10.0.1/message`** (`packages/core/src/constants.ts`, `packages/agent/src/messaging.ts`, `packages/agent/src/dkg-agent.ts`, `packages/node-ui/src/db.ts`, `docs/messenger-add-protocol.md`): `PROTOCOL_MESSAGE` prefix bumped from `/dkg/10.0.0/message` to `/dkg/10.0.1/message`. `MessageHandler.sendChat` / `sendSkillRequest` route through `messenger.sendReliable`; the in-process `MessageOutbox` chat-specific queue + its periodic tick + opportunistic-flush + stale-snapshot guard are **deleted** in favour of the substrate's generic `ProtocolOutbox`. V13 SQLite migration drops the V11 `idx_chat_msgid` partial unique index (receiver-side dedup now owned by the substrate's `message_idempotency` table). MCP `dkg_send_message` queued/attempts/nextAttemptAtMs operator surface preserved end-to-end — sourced from the substrate outbox.
- **PR-4 (#545): `ProtocolRouter.send` parallelPaths option** (`packages/core/src/protocol-router.ts`, `docs/messenger.md`): `send()` accepts a `SendOptions { parallelPaths?: number, timeoutMs?: number }` object (the legacy `timeoutMs: number` arg is still accepted for backward compat). When `parallelPaths > 1`, opens N concurrent `newStream` attempts across enumerated live connections via `Promise.any`; first success wins, losers aborted. Safe only on `/dkg/10.0.1/*` where receiver dedup is mandatory. Defaults to 1 for app-level fan-out protocols (storage-ack / verify-proposal); chat opts in at 2.
- **PR-5 (#546): DHT-walk-on-outbox-stall recovery** (`packages/agent/src/p2p/messenger.ts`, `packages/agent/src/dkg-agent.ts`, `docs/messenger.md`): outbox entries that reach `OUTBOX_STALL_THRESHOLD = 5` attempts of "no valid addresses for peer" trigger a time-bounded (`DHT_WALK_TIMEOUT_MS = 10s`), rate-limited (`DHT_WALK_RATE_LIMIT_MS = 5min/peer`) `libp2p.peerRouting.findPeer()` to refresh the receiver's addresses in the peerStore. Runs in the background so retries continue uninterrupted; the next outbox tick re-enumerates against the refreshed addresses.
- **PR-7 (#548): `--relay-preferred` CLI flag + `preferredRelays` config + relay-setup playbook** (`packages/cli/src/cli.ts`, `packages/cli/src/config.ts`, `packages/cli/src/daemon/lifecycle.ts`, `packages/cli/README.md`, `docs/messenger-operator.md`): operators can prioritise relays they control via `dkg start --relay-preferred /ip4/.../p2p/...` (repeatable) or by writing `preferredRelays: string[]` into `~/.dkg/config.json`. The new `mergePreferredRelays` helper parses both sources, dedupes (first-seen order), and prepends to the network relay list — public testnet relays remain as fallback. Operator playbook for standing up your own relay infrastructure ships in `packages/cli/README.md` § "Operator relays". PR-6 (gossip peer-hints) cancelled per Gate B; DHT walk + inbound-from-receiver are sufficient.
- **PR-8 (#550): migrate `/swm-sender-key` + `/private-access` onto the substrate** (`packages/core/src/constants.ts`, `packages/agent/src/dkg-agent.ts`, `packages/publisher/src/access-client.ts`, `packages/publisher/test/_helpers/substrate.ts`, `docs/messenger.md`): both protocols bumped to `/dkg/10.0.1/*` and routed through `messenger.register` / `messenger.sendReliable`. `AccessClient` is refactored to accept a minimal `AccessSendSurface` interface (defined locally in `access-client.ts`) instead of importing `Messenger` directly, avoiding a `publisher → agent` circular dependency. A test-only `publisher/test/_helpers/substrate.ts` shim provides substrate semantics for publisher tests without the agent package dependency.
- **PR-9 (#551): migrate `/query-remote` onto the substrate with `RESPONSE_GONE` retry recipe** (`packages/core/src/constants.ts`, `packages/agent/src/dkg-agent.ts`, `docs/messenger.md`): protocol bumped to `/dkg/10.0.1/query-remote`. New `sendQueryReliable()` helper wraps `messenger.sendReliable` and re-issues with a fresh `messageId` if the previous attempt returns `RESPONSE_GONE` (cap 2 attempts). SPARQL queries are app-layer idempotent, so the fresh-`messageId` re-issue is safe; the cap prevents infinite loops if every response is over the 256 KiB cache budget.
- **PR-10 (#554): migrate `/join-request` onto the substrate; delete `JoinApprovalRetryQueue`** (`packages/core/src/constants.ts`, `packages/agent/src/dkg-agent.ts`, `packages/agent/src/join-approval-retry-queue.ts`, `docs/messenger.md`): all three `messenger.sendToPeer` call sites for `/dkg/10.0.0/join-request` (private notification, curator-targeted forward, broadcast) replaced with `messenger.sendReliable`; protocol bumped to `/dkg/10.0.1/join-request`. The in-memory `JoinApprovalRetryQueue` + its 30s timer + on-connect handler + processor methods are **deleted** — substrate's SQLite outbox now owns retry persistence across restart. Net `-554 / +136` LOC. `listPendingJoinApprovalRetries` stubbed to return `[]` (operator diagnostic re-built atop substrate outbox in PR-12).
- **PR-11 (#555): migrate `/storage-ack` + `/verify-proposal` onto the substrate** (`packages/core/src/constants.ts`, `packages/agent/src/dkg-agent.ts`, `packages/cli/src/daemon/lifecycle.ts`, `docs/messenger.md`): both protocols bumped to `/dkg/10.0.1/*` and routed via `messenger.register` / `messenger.sendReliable`. `ACKCollector` + `VerifyCollector` quorum logic is unchanged; only the transport rewires. Three `sendP2P` wirings swap `sendToPeer → sendReliable` with `queued`-as-per-peer-throw so the collectors' existing retry loops keep their semantics. `parallelPaths` stays at 1 to avoid 9x fan-out amplification on top of the existing app-layer quorum. `/dkg/10.0.0/verify-approval` intentionally stays bare (not a substrate caller).
- **PR-12 (#557): per-message latency histogram + `/api/slo` endpoint + soak script extension** (`packages/agent/src/p2p/messenger.ts`, `packages/agent/src/dkg-agent.ts`, `packages/cli/src/daemon/routes/agent-chat.ts`, `scripts/libp2p-soak-test.sh`, `docs/messenger.md`, `docs/messenger-operator.md`): in-memory per-protocol histogram of `sendReliable` invoke → `delivered: true` latency across queue + retries (1000-sample sliding window via `DEFAULT_SLO_WINDOW_SAMPLES`). Exposes `{ samples, p50Ms, p95Ms, p99Ms, delivered, queued }` per protocol via `GET /api/slo` (localhost-only by default; same `Authorization: Bearer` requirement as every other `/api/*` route). Soak script extended with per-cycle `/api/slo` snapshot (`slo.jsonl` + human-readable summary line in `main.log`) covering all 8 protocols. Source of truth for the ship-gate SLO measurement.
- **PR-13: docs consolidation pass** (`docs/messenger.md`, `docs/messenger-operator.md`, `AGENTS.md`, `CHANGELOG.md`): sequence diagrams (topology + happy-path + recovery + multi-path) polished into `docs/messenger.md`; per-protocol coverage table marked all-rc.9-shipped; SLO reading guide folded into `docs/messenger-operator.md`; `AGENTS.md` gets a Universal Messenger paragraph + convergence rule (route new short-message protocols via `Messenger.sendReliable`, never `ProtocolRouter.send` directly); this CHANGELOG entry.

### Cancelled

- **PR-6 (gossip peer-hints, conditional)**: skipped per Gate B decision — DHT walk + inbound-from-receiver are sufficient for the rc.9 SLO target. If a post-ship soak surfaces a reliability tail that DHT walk doesn't close, PR-6 lands as a fast follow-up under the original gossip-hints design (signed topic, 5min publish cadence, 10min replay window).

### Carry-over from rc.8 (preserved by the substrate)

The substrate explicitly preserves five rc.8 invariants surfaced by PRs #533 / #534 / #536 / #537 / #538:

1. **Receiver-side dedup contract** (rc.8 #534): the generic `Messenger.register` wrapper performs `(peer, protocol, message_id, 'in')` lookup before invoking the handler — exactly-once application semantics.
2. **Targeted `ON CONFLICT`** (rc.8 #534): `message_idempotency` uses `INSERT ... ON CONFLICT DO NOTHING` only on the dedup case, not as a swallow-all-constraint clause.
3. **Stale-snapshot guard** (rc.8 #538): `ProtocolOutbox.processOutboxOnConnect` and `processOutboxTick` call `hasEntry()` between `tryBeginAttempt()` and the wire send. Contract test pinned in `messenger-substrate.test.ts`.
4. **Peer ID normalisation** (rc.8 #533): every libp2p-boundary code path uses `peerId.toString()` consistently for outbox / idempotency / diagnostics lookups.
5. **Connection reuse logic** (rc.8 #537): PR-4 multi-path enumeration walks `getConnections()` directly rather than peerId-keying.

---

## [10.0.0-rc.8] - 2026-05-15

libp2p reachability hardening (capacity / observability / multi-reservation series), tag-triggered npm publishing, agent-to-agent debug chat, Node UI chat panel revamp, and a Base Sepolia 7-contract redeploy. **Testnet contract redeploy required** — `chainResetMarker` bumped to `v10-profilestorage-paramstore-redeploy-2026-05-14` (already on `main` since #511); first boot on rc.8 wipes per-node chain-derived state for any operator still on the rc.6 marker. Hub at `0xC056e67Da4F51377Ad1B01f50F655fFdcCD809F6` and Token at `0x2A58BdD13176D85906D804cdbFFA0D9119282DC8` retain their addresses.

### Added
- **libp2p reachability hardening PR1/3 — Core Node relay capacity + TTLs + operator knob** (PR #524, `packages/core/src/node.ts`, `packages/core/src/types.ts`, `packages/cli/src/config.ts`, `packages/agent/src/dkg-agent.ts`): Core Nodes now scale `circuitRelayServer` capacity by `RELAY_CAPACITY_MULTIPLIER` so a node advertising itself as a relay actually accepts the reservations it gets dialed for. New `DKGNodeConfig.relayServerCapacity` operator knob (validated by `validateRelayServerCapacity`) lets large-fanout operators scale `maxReservations` / `maxCircuits` / `maxOutboundStreams` / `maxInboundStreams` together; defaults stay safe for small operators. Reservation TTL extended to 2 h with `defaultDurationLimit` of 30 min so long-lived edges aren't churned on every refresh cycle.
- **libp2p reachability hardening PR2/3 — relay observability** (PR #525, new `packages/core/src/relay-metrics-adapter.ts`, new `packages/core/src/relay-internal-shapes.ts`, `packages/core/src/node.ts`, `packages/node-ui/src/db.ts`, `packages/node-ui/src/api.ts`): `RelayMetricsAdapter` plugs into libp2p as a `Metrics` implementation and counts relay-server bytes by direction via `RELAY_V2_HOP_CODEC` / `RELAY_V2_STOP_CODEC` stream tracking (no more guessing from raw connection metadata). New `/api/relay/stats` endpoint surfaces `{ activeReservations, activeCircuits, bytesIn, bytesOut }`; values are persisted as `relay_*` columns on `metric_snapshots` for retention. Compatibility helpers (`readRelayReservations`, `readConnectionStreams`) validate libp2p internal shapes and return `null` on mismatch so the API reports a visible error rather than silently stale zeros if libp2p ever refactors. Pinned by `packages/core/test/relay-internal-shapes.test.ts` (9 cases).
- **libp2p reachability hardening PR3/3 — edge multi-reservation tuning** (PR #526, `packages/core/src/node.ts`, `packages/core/test/relay.test.ts`): edge nodes can now hold multiple parallel relay reservations for redundancy via `DKGNodeConfig.relayReservationCount` (default 3, gated to edge nodes that actually have `relayPeers`). The relay watchdog enforces *per-relay* reservation presence when target > 1, with a per-tick forced-redial budget capped at `missingSlotsAtTickStart` so a single cycle can't burn through every relay. New canonical `usableRelayCandidates` pass deduplicates `relayPeers` by canonical peerId (handles mixed base58btc/base32 encodings), aggregates alternate transports for the same peer (`[relayA-tcp, relayA-ws]` → one `RelayTarget` with both addrs), drops self-references, and warns on malformed entries. AutoNAT gating + `/p2p-circuit` listener fallback now key off `usableRelayCandidates.length` so a node whose every configured relay is unusable falls back cleanly instead of half-configuring itself.
- **Agent-to-agent debug chat** (PRs #510, #521, `packages/mcp-dkg/src/index.ts`, new `packages/agent/src/message-outbox.ts`, `packages/core/src/protocol-router.ts`): MCP exposes `dkg_send_message` / `dkg_check_inbox` / `dkg_get_chat_history` / `dkg_list_agents` so two agents on different nodes can exchange threaded messages with ACL enforcement (sender-on-allowlist, no rate-limit bypass). `MessageOutbox` retries silent-dropped invitee deliveries with per-key inflight guard against duplicate-delivery races; `ProtocolRouter.send` re-runs `peerResolver` per attempt so a stale handle from boot doesn't strand subsequent retries.
- **Node UI chat panel UX/UI revamp** (PRs #503, #504, #505, #516, #528, #529, `packages/node-ui/src/components/chat/**`): full chat-panel rebuild — sticky header, custom Select for project switching, drag-and-drop file composer with chip rendering, markdown + syntax highlighting in assistant turns, full-width assistant bubbles, send-button states, hover-revealed timestamps, inline streaming caret, my-projects picker, and a 404-resilient markdown viewer for imported documents. Replaces the legacy chat surface end-to-end.
- **Tag-triggered npm publishing** (PR #522, `.github/workflows/npm-publish.yml`, `.github/workflows/release.yml`): merging to `main` no longer publishes anything to npm. Publishing now fires only on signed annotated tags matching `v<major>.<minor>.<patch>` (e.g. `v10.0.0-rc.8`), and only when the tag is reachable from `main` or `v10-rc`, ssh- or gpg-signed, and matches every `package.json` version in the workspace. The `npm-publish` GitHub Environment requires a reviewer (5-minute wait, no self-approval) before any package is pushed. Replaces the previous continuous-publish-on-merge flow.

### Fixed
- **`relayServerCapacity` validator silently accepted unsafe-integer values** (PR #530, `packages/core/src/node.ts`, `packages/core/test/relay-capacity.test.ts`): the validator used `Number.isInteger` which accepts values above `Number.MAX_SAFE_INTEGER` (e.g. `9007199254740993`); multiplying by `RELAY_CAPACITY_MULTIPLIER` then silently lost precision and corrupted the libp2p caps. Now uses `Number.isSafeInteger` and a new `MAX_RELAY_SERVER_CAPACITY` ceiling that ensures every derived cap also stays within the safe range. `deriveRelayCaps` got the same belt-and-braces check.
- **Staking-V10: `_convertToNFT` left migrated TRAC stranded in `StakingStorage`** (PR #491, `packages/evm-module/contracts/StakingV10.sol`): the conviction-NFT conversion path read the migrated balance from `StakingStorage` but never transferred it onward to `ConvictionStakingStorage`, so the underlying TRAC was unrecoverable from either side post-conversion. Now transfers the balance in the same call before minting the NFT.
- **Staking-V10: operator-fee lookup ignored historical epoch** (PR #493, `packages/evm-module/contracts/StakingV10.sol`): `_getEffectiveOperatorFee` resolved against the latest fee-change rather than the epoch the lookup was for, so reward calculations against past epochs used today's fee. Now binds the lookup to the requesting epoch's timestamp.
- **Post-approval sync gap hid freshly-joined curated CGs** (PR #517, `packages/agent/src/dkg-agent.ts`): a window between approval-event arrival and `_meta` reconciliation left the just-joined CG invisible to the invitee until the next periodic sync. Approval handling now triggers an immediate targeted sync of the new CG.
- **Curator reject-reason was hidden in `JoinProjectModal`** (PR #508, `packages/node-ui/src/components/.../JoinProjectModal.tsx`): the modal swallowed the `reason` field on `JOIN_REQUEST_REJECTED`, leaving operators with a generic "rejected" UI. Now surfaces the curator's explicit reason string.
- **Axiom-4 trust upgrades** (PR #523): hardened trust-upgrade endpoint inputs, bounded the verify-collection timeout, fail-closed on non-quorum verify, and aligned devnet trust paths with the new gating. Companion devnet sections cover the regression surfaces.
- **`npm-publish` job missed hidden `.publish-artifacts` upload** (PR #515, `.github/workflows/npm-publish.yml`): renamed to non-hidden `publish-artifacts` so `actions/upload-artifact` includes it; preceded #522 (tag-triggered publishing) and unblocked the new flow.

### Changed
- **Base Sepolia (chainId 84532): 7 contracts redeployed off `main@7235e669`** (PR #511, `packages/evm-module/deployments/base_sepolia_v10_contracts.json`): refresh of the `Profile`/`ProfileStorage`/`ParameterStorage` family + the staking-V10 contracts that needed the #491 / #493 fixes deployed. New addresses are committed to `base_sepolia_v10_contracts.json`; agents discover them via `Hub.getContractAddress(...)` at runtime so no `network/testnet.json` edit was needed beyond the `chainResetMarker` bump (`v10-rc6-pca-author-attestation-2026-05-10` → `v10-profilestorage-paramstore-redeploy-2026-05-14`). Hub + Token addresses preserved.

---

## [10.0.0-rc.7] - 2026-05-13

RFC 07 in-process `PeerResolver` consolidation, Relay Registry tri-state on-chain semantics, PCA wiring at registration, and full invite/join e2e coverage in the devnet smoke suite. **No testnet contract redeploy** — `chainResetMarker` unchanged from rc.6 (`v10-rc6-pca-author-attestation-2026-05-10`); rc.7 is a node-only update over the existing on-chain world.

### Added
- **RFC 07 — In-process `PeerResolver` (consolidated PRs #494, #496, #497, #499, #501, #506)**: every outbound libp2p dial now flows through a single resolution chain (live-conn → DHT → RFC 04 registry stub → agents-CG fallback). New `/api/connect` semantics return precise HTTP codes (`504 PEER_NOT_FOUND`, `502 DIAL_FAILED`, `400 INVALID_PEER_ID`) with structured error bodies so callers can distinguish "peer is unknown to us" from "we know it but can't reach it." A CI grep gate (`scripts/audit-dial-protocol.mjs`) prevents new raw `libp2p.dialProtocol(peerId, …)` callers from sneaking in around the resolver.
- **Backend-agnostic `dkgGossipMsgId` primitive** (PR #501): unified gossip message-id derivation behind a single helper so every gossipsub publisher (heartbeat, agent-ad, KA-ad, SWM-write) uses identical message-id logic regardless of transport (real libp2p vs in-memory test bus). Cuts class of duplicate-rebroadcast bugs the `/api/agents` heartbeat exhibited under restart.
- **devnet-test.sh sections 28-31** (PRs #501, #506, #507): the smoke suite now exercises (28) `/api/connect` resolver path + HTTP semantics, (29) cross-node connect matrix, (30) Node1 cold-restart + post-restart resolver wiring, (31) curated CG invite/join end-to-end including silent-NACK regression guards. Knobs `SKIP_RESTART=1`, `SKIP_MATRIX=1`, `SKIP_INVITE_FLOW=1` for fast iteration.
- **PCA wiring at registration** (PR #502, rebase of #423, `packages/cli/src/daemon/routes/context-graph.ts`, `packages/agent/src/dkg-agent.ts`): when an operator registers a CG with `publishPolicy: 'pca'`, the daemon now records the PCA owner address, the chain signer address, and the `getRegistrationTxSignerAddress` separately. PCA agents can publish to the registered CG without 500-error fallthroughs; non-existent PCA accounts return a clean `404` rather than a generic 500; `publishPolicy` is now exposed on `ApiClient.createContextGraph` so dkg-node-ui can surface the choice at create-time.

### Fixed
- **`relayCapable` was sticky on chain** (PR #506, `packages/agent/src/dkg-agent.ts`, `packages/cli/src/daemon/lifecycle.ts`): `publishRelayRegistry` previously treated `false` and `undefined` identically as no-ops, so a node that ever ran with `relayCapable=true` would keep advertising itself as relay-capable on chain forever. Now tri-state: `true` ensures on-chain flag is true, `false` actively clears any stale opt-in, `undefined` leaves the on-chain flag untouched (preserves manual `dkg admin set-relay-capable` flips). 12 new regression tests in `packages/agent/test/publish-relay-registry.test.ts`.
- **Curated CG invite/acceptance regressions caught by SECTION 31** (PR #507): the standalone `scripts/devnet-test-invite-flow.sh` is folded into the main smoke suite, so future runs assert (a) `DKG_CURATOR` triple is present in the curator's `_meta` graph (silent-NACK regression guard for PR #448 round-6), (b) the inbound `PROTOCOL_JOIN_REQUEST` handler logs accept + persist, (c) `denied` catch-up status produces no phantom CG entries, and (d) post-approval catch-up actually completes with `_meta` triples mirrored to the invitee.

---

## [10.0.0-rc.6] - 2026-05-10

RFC-001 sign-at-creation lifecycle, on-chain agent provenance, robust replica sync, devnet release-gate test layer. **Testnet contract redeploy required** — `chainResetMarker` bumped to `v10-rc6-pca-author-attestation-2026-05-10`, which auto-wipes node-side state on first boot. All non-Hub/Token contracts on Base Sepolia have new addresses (Hub `0xC056e67Da4F51377Ad1B01f50F655fFdcCD809F6` and Token `0x2A58BdD13176D85906D804cdbFFA0D9119282DC8` preserved); operators must re-create profiles and stakers must re-stake (TRAC stuck in the orphaned `DKGStakingConvictionNFT` is non-recoverable on testnet by design).

### Added
- **Sign-at-creation assertion lifecycle** (PR #436, `packages/agent/src/dkg-agent.ts`, `packages/cli/src/daemon/routes/assertion.ts`): chained `assertion.create → write → finalize → promote` REST flow plus four standalone routes that each emit `memory_graph_changed` SSE so external lifecycle composers (staking-ui, dkg-node-ui) reflect every step in real time. Seal binds `merkleRoot + rootEntities + EIP-712 author attestation` together; `publishFromFinalizedAssertion` recomputes the root from the same `kaMap` partition the seal used so any drift between finalize-time and publish-time is rejected before a signature is wasted.
- **On-chain agent provenance via EIP-712 author attestation** (PR #436, `packages/evm-module/contracts/KnowledgeAssetsV10.sol`, `packages/agent/src/finalization-handler.ts`, `packages/chain/src/evm-adapter.ts`): unified `publish(PublishParams)` entry point with `_verifyAuthorAttestation` (EIP-712 + EIP-1271 for smart-contract wallets); new `KCCreated(uint256,bytes32,uint96,address)` event indexed-topic carries the recovered author address. Originator path emits `dkg:authoredBy` + `dkg:Publication` triples on the publishing node; replicas now reconstruct the same triples by reading the on-chain `author` topic in `FinalizationHandler.verifyOnChain` and threading it through `promoteSharedMemoryToCanonical`.
- **Robust sync-on-connect retry** (PR #436, `packages/agent/src/dkg-agent.ts`, `packages/agent/src/sync/on-connect/sync-on-connect.ts`): event-driven retry on `peer:update` (covers libp2p `identify` race) plus a 5-minute periodic reconciler that re-pulls from any connected peer whose last successful sync is staler than 10 minutes. Handles the "node joins network at random time, peer protocols arrive late" failure mode that previously stranded fresh nodes without historical KCs.
- **Hardhat interval mining default** (PR #436, `scripts/devnet.sh`): devnet now mines a block every 1000 ms via `evm_setIntervalMining`, so on-chain time keeps ticking without new transactions. Required for RandomSampling proofing periods, operator-fee accrual, and any time-based contract behavior to work on a quiet local devnet.
- **`devnet/v10-core-flows` release-gate test suite** (PR #436): release-gate validation against a live 6-node devnet. Four canonical scenarios (chained sign-at-creation lifecycle + SSE emission per step, edge-node publish surfaces `tentative` correctly, NFT staking withdraw round-trip, operator-fee accrual within 1 % of RFC-26 prediction → request → cooldown → finalize). Run via `pnpm test:devnet:v10-core-flows`. Companion `FINDINGS.md` documents every architectural confirmation and deferred trade-off.
- **Devnet test suites promoted out of `experiments/`** (PR #436): `agent-provenance`, `v10-core-flows`, `v10-end-to-end`, `v10-stress` now live under `devnet/`. Their `package.json` `test` scripts were renamed to `test:devnet` so `turbo run test` no longer pulls them into the default CI fan-out — they require a live devnet and bootstrap state and are run via dedicated `pnpm test:devnet:*` scripts.

### Fixed
- **Sync responder dropped per-cgId `_meta` graphs** (PR #436, `packages/agent/src/sync/responder/sync-handler.ts`): the data-phase SPARQL filter `!STRENDS(STR(?g), "/_meta")` was over-matching and excluded every per-context-graph metadata graph (where KA merkle roots, `kaCount`, and other RandomSampling preconditions live). Replaced with an exact-match exclusion of just the canonical top-level `_meta` graph. Without this fix, every non-publisher core node failed `kc-not-synced` on every prover tick.
- **Standalone `/api/assertion/:name/finalize` did not emit `memory_graph_changed`** (PR #436, `packages/cli/src/daemon/routes/assertion.ts`): a merge-conflict resolution oversight. Clients composing the lifecycle by hand missed the `assertion_finalized` state transition. Pinned by `packages/cli/test/memory-graph-events.test.ts`.
- **Round-5 review: `assertionFinalize` could seal an unrecoverable merkleRoot** (PR #436, `packages/agent/src/dkg-agent.ts`): finalize hashed every quad in the assertion graph but `assertionPromote` and `publishFromFinalizedAssertion` later filtered out unsafe-IRI roots through `isSafeIri`, so the sealed root could not be recomputed at publish time. Now rejects finalize when *any* root entity fails `isSafeIri` rather than silently dropping the unsafe subset.
- **Round-5 review: replicas missed `dkg:authoredBy` triples** (PR #436, `packages/agent/src/finalization-handler.ts`, `packages/chain/src/evm-adapter.ts`): `KCMetadata` was rebuilt on the receive side with no author fields. Threaded the EIP-712-attested `author` from the `KnowledgeAssetCreated` event through `EVMChainAdapter.listenForEvents → FinalizationHandler.verifyOnChain → promoteSharedMemoryToCanonical`. Two regression tests added in `finalization-promote-extra.test.ts`.
- **Publisher no longer auto-mints an ephemeral signing wallet** (`packages/publisher/src/dkg-publisher.ts`): `DKGPublisher` constructed without `publisherPrivateKey` previously generated `ethers.Wallet.createRandom()` whenever chain was enabled and used it to sign on-chain publish digests, ACK self-signatures, and authorship proofs. Signatures were unverifiable (signed by a throw-away key the caller never saw, attributed via `publisherAddress` to a different address). The constructor now leaves `publisherWallet` undefined, rejects zero/mismatched `publisherAddress` values, and publish/update fail before emitting publisher-attributed output unless a real signing key exists.

### Added (carried forward from PR #366 follow-ups)
- **`scripts/audit-create-random.mjs`** + CI gate: bans new `Wallet.createRandom()` use in `packages/*/src/**` outside three explicitly justified call sites (`op-wallets.ts` first-run wallet bootstrap, `agent-keystore.ts` custodial chat-agent registration, `evm-module/utils/helpers.ts` deploy script). Same anti-pattern destroyed nine testnet admin keys in May 2026 via the pre-PR-#366 `ensureProfile` random-and-discard path; audit runs in <300 ms on every CI build.

### Changed
- **Base Sepolia (chainId 84532): full v10.1 contract redeploy.** Hub at `0xC056e67Da4F51377Ad1B01f50F655fFdcCD809F6` and Token at `0x2A58BdD13176D85906D804cdbFFA0D9119282DC8` retain their addresses and Token retains its `50,052,000 v9TRAC` total supply. The 34 deployable non-Hub/Token contracts (including the changed `KnowledgeAssetsV10`, `KnowledgeCollectionStorage`, `ContextGraphs`, `ContextGraphStorage`, `KnowledgeCollection`) were freshly deployed and registered on-chain in a single batched `Hub.setAndReinitializeContracts` call. New addresses are committed to `packages/evm-module/deployments/base_sepolia_v10_contracts.json`; agents discover them via `Hub.getContractAddress(...)` at runtime so `network/testnet.json` was not edited (only `chainResetMarker` was bumped). 11 auxiliary contracts (the legacy `ContextGraph*` registry family superseded by the new `ContextGraphs`/`ContextGraphStorage` and the V6/V8 reward migrators) intentionally skipped redeploy via their existing in-script gates.

### Deferred to follow-up PRs
- **PCA discount/attribution decoupling** (review comment 3215083795): the `DKGPublishingConvictionNFT` discount branch is keyed to `agentToAccountId(msg.sender)` while publishing credit goes to `p.publisherNodeIdentityId` (RFC-001 §3.6 — attribution decoupled from payment by design). Off-chain accounting concern is real but tightening it is a contract API change; will be its own PR.
- **Replica `dkg:authoredBy` via the gossip-publish path** (review comment 3215169754): the rc.6 fix covers the finalization-gossip replica path. Peers that confirm via `GossipPublishHandler` (the full-publish gossip path) still rebuild `kcMeta` without `authorAddress`; same one-line shape of fix as the finalization path, scoped to a separate PR for `GossipPublishHandler`-focused review.

---

## [10.0.0-rc.4] - 2026-05-04

Hotfix on top of `v10.0.0-rc.3`. **Restores random-sampling proof submission for v9-style `<owner>/<slug>` context graphs on testnet** and brings the deployed Base Sepolia `Profile` contract into line with what the post-PR-#366 daemon expects, so `op[1]` / `op[2]` ACK signer auto-registration finally lands on-chain. No `chainResetMarker` change — per-node state (oxigraph store, RS WAL, publish journals) is preserved across the operator update.

### Fixed
- **V9-style `<owner>/<slug>` context graph names no longer fail `extractV10KCFromStore`** (#377): the kc-extractor's `resolveContextGraphNameFromOnChainId` previously rejected any CG name containing `/`, so finalized KCs surfaced as `KCNotFoundError` in `[rs.tick.kc-not-synced]` and silently dropped every random-sampling proof submission for slash-named CGs (which is the v9 namespacing convention `<owner_address>/<slug>`). Validation now uses `assertSafeIri` on the *derived* meta-graph URI — the actual SPARQL injection surface — instead of an over-tight name-level allowlist that was conflating namespacing with safety. Reproduced first as a unit test in `kc-extractor.test.ts` and then end-to-end in `e2e-hardhat-chain.test.ts`; both regression-tested by reverting the resolver fix and confirming the new tests fail.
- **`scripts/devnet.sh` no longer crashes with `SyntaxError: Unexpected end of input` on startup** (#377): three JavaScript comments inside `node -e ...` blocks contained unescaped inner double-quotes that bash interpreted as string delimiters, truncating the script. Comments rephrased to avoid embedded double-quotes — necessary precondition for validating the kc-extractor fix on a local devnet.

### Changed
- **Base Sepolia (chainId 84532): `Profile` upgraded `v1.0.0` → `v1.1.0`, `Identity` re-deployed (`v1.0.0`, source-only zero-address-check tightening) so it stays in lockstep with the Profile that calls into it** (#372). The new Profile carries the `addOperationalWallets(uint72, address[])` external function the post-PR-#366 `ensureProfile` flow expects when registering the operational ACK wallet trio; without this upgrade the daemon failed silently with `Operational wallet auto-registration failed: missing revert data ... to: 0x4Ad9B99C…0493A` and only `op[0]` ever made it on-chain. Both contracts are pure logic (zero per-identity state) so no migration is required — existing identities 1-14, including freshly-recreated beacon-01 with 50,000 v9TRAC stake on identity 14, are unaffected. Hub mappings updated atomically via `Hub.setAndReinitializeContracts` (tx [`0xf7c8dfe2…1914`](https://sepolia.basescan.org/tx/0xf7c8dfe2c242bec2ee64f648757aba5b8d3b567b2129c3ebe89849abe8221914)).

---

## [10.0.0-rc.3] - 2026-05-03

Operator hardening on top of `v10.0.0-rc.2`. No contract redeploy — `chainResetMarker` unchanged, no per-node state wipe is triggered. Ships the admin / operational wallet key split, RandomSampling Hub-rotation self-refresh, prover stale-period detection, and devnet bootstrap fail-fast guards.

### Added
- **Persisted profile admin wallet** (#366): `wallets.json` now contains a distinct admin wallet alongside operational wallets so newly-created profiles do not lose their admin key.
- **Operational wallet self-healing for ACK signing** (#366): core nodes now auto-register configured operational ACK signer wallets on-chain for their profile identity during startup.
- **`Profile.addOperationalWallets(uint72,address[])` facade** (#366): profile admins can add operational-only keys without using the lower-level generic key-management surface.
- **End-to-end ACK signer registration coverage** (#366): a real Hardhat/libp2p test now verifies startup registration, StorageACK signing by the confirmed key, and refusal to sign after the key is removed on-chain.
- **RandomSampling Hub-rotation self-refresh** (#367): `EVMChainAdapter` re-resolves `RandomSampling` + `RandomSamplingStorage` addresses through `HubResolutionCache` on every prover tick instead of caching them at boot, so a Hub rotation no longer strands the prover on dead addresses.
- **`[rs.tick.forcing-rotation]` log + behaviour** (#369): the prover now compares the cached challenge's period against the on-chain block height every tick and forces a fresh `createChallenge` when the cached "unsolved" challenge is past its wall-clock period boundary, instead of stranding on a stale challenge after a Hub rotation.
- **Devnet stake-probe via `getNodeStakeV10`** (#368): `scripts/devnet.sh` checks for an existing identity-scoped V10 stake before issuing `createConviction`, preventing duplicate stakes when re-running `start` on an already-bootstrapped devnet.
- **Devnet hosting-node discovery fail-fast** (#370): `scripts/devnet-test-publish.sh` now distinguishes file-level `wallets.json` failures (refuses to run a partial smoke) from intentional edge-node skips.

### Changed
- **Admin-only operational wallet registration** (#366): `Profile.addOperationalWallets` requires a profile admin key; operational keys can no longer add more operational keys.
- **New profile creation uses the persisted admin wallet** (#366): `EVMChainAdapter.ensureProfile` now uses `chainConfig.adminPrivateKey` for the profile admin address instead of creating an unrecoverable random admin wallet.
- **StorageACK signing is gated by on-chain confirmation** (#366): core nodes only register/sign with ACK keys confirmed as `OPERATIONAL_KEY` for the node identity.
- **Devnet bootstrap fail-fast** (#370): `scripts/devnet.sh` now exits non-zero when `staked < coreCount`, refusing to declare devnet ready when only some cores ended up staked.
- **Single-flight RPC duration probe** (#369): `EVMChainAdapter` uses Contract-instance identity (rather than `HubResolutionCache.generation`) to scope the duration probe slot, so a TTL refresh that mints a new Contract handle correctly invalidates an in-flight probe without breaking `resolveAndAssignRandomSamplingPair`'s concurrent-invalidation detection.

### Fixed
- **Invalid ACKs from unregistered wallets** (#366): ACK handlers no longer produce signatures from local keys that are not confirmed operational wallets on-chain.
- **`HubResolutionCache` invalidate-vs-await race** (#367): a `generation` counter bumped only on `invalidate()` ensures an in-flight resolve started under generation N cannot write back its value if `invalidate()` was called while it was suspended.
- **Devnet single-wallet `wallets.json` file shape** (#368): `scripts/devnet.sh` now writes the `{ adminWallet, wallets }` shape expected by post-#366 daemons (graceful fallback for legacy single-wallet files retained for testnet operators).

---

## [10.0.0-rc.2] - 2026-05-01

V10 RandomSampling + V8/V10 staking consolidation. Testnet reset required (Base Sepolia) — see `docs/TESTNET_RESET.md`.

### Added
- **V10 RandomSampling end-to-end** (`packages/random-sampling`): per-node challenge/proof loop driven by `RandomSampling.sol`; chunk selection reads `merkleLeafCount` from on-chain V10 storage; non-zero `getNodeEpochProofPeriodScore` once a node holds V10 stake.
- **Auto chain-reset wipe** (`packages/cli/src/daemon/chain-reset-wipe.ts`): on boot, the daemon compares the bundled `network.chainResetMarker` against the persisted marker and one-shot wipes `store.nq{,.tmp}` + `publish-journal.*` + `random-sampling.wal` when they differ. Operators no longer need a manual wipe procedure on a testnet reset.
- **`ensureProfile` auto-stake via V10 path** (`packages/chain/src/evm-adapter.ts`): on a clean chain, agents auto-create their on-chain identity and stake 50k TRAC into a V10 NFT position via `DKGStakingConvictionNFT.createConviction(identityId, amount, lockTier=1)` so the V10 stake vault (`ConvictionStakingStorage.nodeStakeV10`) is non-zero from the first proof period.
- **Required `merkleLeafCount` on the V9→V10 publish bridge** (`packages/chain/src/chain-adapter.ts`, `evm-adapter.ts`): `PublishToContextGraphParams.merkleLeafCount` is now required; the bridge throws on missing/invalid input instead of silently defaulting to 1 (which would corrupt RandomSampling chunk selection for any KA with more than one leaf).
- **Stale-proof-period detection in the prover** (`packages/random-sampling/src/prover.ts`): tick now checks the actual chain block height against the cached period's expiry and forces `createChallenge` to rotate when the period has elapsed, instead of stranding on a stale "already-solved" cache view.
- **Testnet reset runbook** (`docs/TESTNET_RESET.md`): full procedure for the V10 cutover covering maintainer release (npm publish + git merge), contracts deploy, automatic per-node state wipe, and smoke verification.
- **Operator-supplied `randomSampling.walPath`** (`packages/cli/src/daemon/chain-reset-wipe.ts`, `daemon/lifecycle.ts`): chain-reset wipe now honors a custom WAL path from config instead of only the default location.
- **Codex PR review workflow** (`.github/workflows/codex-review.yml`): `pull_request_target` + SHA-pinned for review on every PR.

### Changed
- **Consolidated V8 `StakingStorage` into V10 `ConvictionStakingStorage`**: the dual-store coupling between V8 `Staking` / `DelegatorsInfo` and V10 storage is dropped. V10 contracts (`StakingV10`, `DKGStakingConvictionNFT`, `ConvictionStakingStorage`, `RandomSampling`, `RandomSamplingStorage`) are the canonical staking surface; V8 staking is unregistered from the Hub on the testnet reset.
- **Test helpers updated to V10 staking** (`hardhat-harness.ts:stakeAndSetAsk`): switches from V8 `Staking.stake` to `DKGStakingConvictionNFT.createConviction` so E2E flows match the agent's actual auto-stake path.
- **`enrichEvmError` regex generalised** (`packages/chain`): now decodes EVM revert reasons across Hardhat-style `data="0x..."` and the broader provider variants.

### Fixed
- **Zero RandomSampling node scores** caused by V8/V10 stake-vault split — `RandomSampling.calculateNodeScore` reads `ConvictionStakingStorage.getNodeStakeV10` exclusively, but legacy `Staking.stake` only updated V8 `StakingStorage`. Resolved by routing all stake through V10 (`ensureProfile`, `stakeAndSetAsk`).
- **`chainResetWipe` daemon crash on FS errors** (`packages/cli/src/daemon/chain-reset-wipe.ts`): wipe + `saveState` are now wrapped in `try/catch`; FS errors log a warning and boot continues instead of crashing the daemon.
- **`ensureProfile` profile-without-stake on partial failure**: profile creation and staking are now in separate `try/catch` blocks so a failed stake leaves the on-chain identity intact for retry instead of leaving the operator without either.
- **ABI pinning test drift** for V10 publish/update functions after `merkleLeafCount` was added (`abi-pinning.test.ts`): pin digests refreshed.

[Unreleased]: https://github.com/OriginTrail/dkg/compare/v10.0.0-rc.12...HEAD
[10.0.0-rc.12]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.12
[10.0.0-rc.11]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.11
[10.0.0-rc.10]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.10
[10.0.0-rc.9]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.9
[10.0.0-rc.8]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.8
[10.0.0-rc.7]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.7
[10.0.0-rc.6]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.6
[10.0.0-rc.4]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.4
[10.0.0-rc.3]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.3
[10.0.0-rc.2]: https://github.com/OriginTrail/dkg/releases/tag/v10.0.0-rc.2
[9.0.0]: https://github.com/OriginTrail/dkg-v9/releases/tag/v9.0.0

---

## [9.0.0] - 2026-02-26

First tracked release (DKG V9). Includes:

### Added
- **Cross-agent query protocol** (`/dkg/query/2.0.0`): query another node's knowledge store over libp2p (ENTITY_BY_UAL, ENTITIES_BY_TYPE, ENTITY_TRIPLES, SPARQL_QUERY) with access policies and rate limiting.
- **Node dashboard UI** (`@origintrail-official/dkg-node-ui`): web UI served by the daemon — dashboard, Knowledge Explorer (SPARQL + graph viz), Operations log, Network, Wallet, Integrations, chat assistant (rule-based + optional LLM).
- **Oxigraph persistence and sync**: triple store persists to disk; sync protocol for catch-up on connect.
- **On-chain publishing**: Base Sepolia testnet integration, TRAC staking, ask setting, knowledge asset minting.
- **CLI**: `dkg init`, `start`, `stop`, `status`, `peers`, `publish`, `query`, `query-remote`, `subscribe`, `contextGraph create/list/info`, `set-ask`, `wallet`, `logs`.
- **GitHub Actions CI**: build and test on push/PR; Solidity compile and tests.

### Changed
- Broadcast publish uses result's `publicQuads` so private triples are not re-sent over gossip.
- Agent supports `listenHost` for binding to a specific interface (e.g. 127.0.0.1 in tests).
