Skip to content

Fix the caught bugs by the tests#229

Open
Bojan131 wants to merge 110 commits intomainfrom
fix/caught_bugs
Open

Fix the caught bugs by the tests#229
Bojan131 wants to merge 110 commits intomainfrom
fix/caught_bugs

Conversation

@Bojan131
Copy link
Copy Markdown
Contributor

@Bojan131 Bojan131 commented Apr 21, 2026

Fix bugs that tests have caught

Niks988 and others added 3 commits April 6, 2026 17:39
Relay-03 (beacon-03) switched to V10-rc. DO relay (167.71.33.105)
decommissioned. V9 testnet now runs 2 relays: relay-01 + relay-02.

Made-with: Cursor
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review completed — no issues found.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review completed — no issues found.

@Bojan131 Bojan131 changed the title update Fix the caught bugs by the tests Apr 21, 2026
…call

- core: computeUpdateACKDigest allocated 340 bytes but only wrote 308,
  causing digest mismatches against the Solidity contract layout (C-1).
  Resize buffer to 308 bytes to match abi.encodePacked layout.
- origin-trail-game: coordinator called agent.createContextGraph (returns
  void) but treated the result as { success, contextGraphId }, so
  swarm.contextGraphId was never populated (G-1). Switch to
  registerContextGraphOnChain and gate on contextGraphId being present.
- origin-trail-game: record lineage from snapshot on the successful
  publish paths in checkProposalThreshold and forceResolveTurn so
  workspace lineage is always written.
- Update in-test mock agents to expose registerContextGraphOnChain with
  the correct return shape.

Made-with: Cursor
- E-7: Hub._setContractAddress no longer double-emits NewContract
- E-20: _isMultiSigOwner is EOA-safe across RandomSampling, RandomSamplingStorage, and all Migrator contracts
- Q-1: DKGQueryEngine now injects minTrust SPARQL filter for verified-memory views
- P-13: resolveViewGraphs honors minTrust by excluding root data graph for verified-memory
- K-4/K-5: network-sim exposes seeded RNG (mulberry32) and deterministic scenario runner + libp2p parity harness

Made-with: Cursor
- CH-10: enrichEvmError now decodes revert data from multiple RPC shapes
  (Hardhat `data="0x…"`, Geth `data: "0x…"` / `data=0x…`, Infura/Alchemy
  `errorData="0x…"`, JSON-embedded `"data":"0x…"`). Prevents raw selector
  leakage to operators (issue #159 class).
- ST-7: PrivateContentStore.storePrivateTriples and deletePrivateTriples
  now run assertSafeIri on rootEntity to block SPARQL-injection at the
  entry points; aligns defence-in-depth with getPrivateTriples.
- A-7: buildEndorsementQuads now emits a random 128-bit nonce and a
  canonical-digest proof quad alongside ENDORSES/ENDORSED_AT, making
  endorsements replay-resistant and cryptographically bindable (spec §10).
- A-13: new workspace-config loader module in packages/agent/src that
  resolves .dkg/config.yaml → .dkg/config.json → AGENTS.md frontmatter
  per spec §22 and validates the schema.

Made-with: Cursor
- agent-audit-extra.test.ts: update A-7 expectation to match the fixed
  buildEndorsementQuads (4 quads with inline signature + nonce) and
  allow-list intentional negative DID-format fixtures.
- agent.test.ts / gossip-validation.test.ts / gossip-publish-handler.test.ts:
  migrate hard-coded did:dkg:agent:Qm… / :owner / :attacker fixtures to
  the spec-mandated 0x-address form (A-12).
- did-format-extra.test.ts: skip agent-audit-extra.test.ts in the drift
  scan; it documents peer-ID form as a negative test.

Made-with: Cursor
- origin-trail-game/coordinator: allow launchExpedition to proceed when
  CCL policy installation fails specifically because the paranet has no
  registered on-chain owner (identity not provisioned). Previously this
  bricked every E2E game launch in no-chain/dev mode with "Expedition
  startup aborted", leaving swarm.status unset. Other CCL failures still
  abort the launch so governance drift is still fatal on a real chain.

- storage/oxigraph-worker: resolveWorkerImplPath() now falls back from
  the sibling src/ path to dist/adapters/oxigraph-worker-impl.js when the
  sibling JS artifact is missing (i.e. when vitest executes src/*.ts
  directly). Fixes the "Cannot find module … oxigraph-worker-impl.js"
  failure in storage.test.ts > createTripleStore factory.

Made-with: Cursor
Comment thread packages/origin-trail-game/src/dkg/coordinator.ts Outdated
Comment thread packages/origin-trail-game/src/dkg/coordinator.ts Outdated
Comment thread packages/agent/src/endorse.ts Outdated
Comment thread packages/query/src/dkg-query-engine.ts Outdated
Comment thread packages/query/src/dkg-query-engine.ts Outdated
Comment thread packages/network-sim/src/server/sim-engine.ts
…nable

- A-15: wrap WorkspacePublishRequest in signed GossipEnvelope on share/
  publish/conditional-share/ontology paths; helper module signed-gossip.ts
  returns zero-byte placeholders when no signer is available.
- A-5: thread per-CG requiredSignatures from chain adapter through publisher;
  block on-chain submission and force tentative status when ack count <
  requiredSignatures.
- K-11: add DKG_PERSIST_CHAT_TURN action + chat-persistence hook surface so
  ElizaOS adapters route turns through the DKG node.
- Hub _checkOwnerOrMultiSigOwner now reverts with OwnableUnauthorizedAccount
  to match OZ Ownable v5 indexer expectations.
- e2e-publish-protocol: assertions updated to verify per-CG quorum gating
  (tentative status + SWM data presence) instead of documenting the bug.

Made-with: Cursor
Comment thread packages/adapter-elizaos/src/actions.ts Outdated
Comment thread packages/agent/src/dkg-agent.ts
Comment thread packages/query/src/dkg-query-engine.ts Outdated
Comment thread packages/agent/src/endorse.ts Outdated
Comment thread packages/evm-module/contracts/storage/Hub.sol
KAV10._executePublishCore now emits two events alongside the existing
KCS.KnowledgeCollectionCreated:

1. KAV10.KnowledgeBatchCreated (new spec event) — CG-aware projection
   so v10 indexers don't have to join CG bind events.
2. KASStorage.KnowledgeBatchCreated (legacy V8/V9 event) via the
   emit-only emitV10KnowledgeBatchCreated entry point so pre-V10
   indexers keep working without changes.

The legacy emit is best-effort — if KASStorage is not Hub-registered
(V10-only stack) the dual-emit silently degrades. KAV10's initialize
resolves it via try/catch.

Made-with: Cursor
Comment thread packages/agent/src/dkg-agent.ts Outdated
Comment thread packages/adapter-elizaos/src/actions.ts Outdated
Comment thread packages/evm-module/contracts/KnowledgeAssetsV10.sol Outdated
Comment thread packages/agent/src/endorse.ts Outdated
Comment thread packages/query/src/dkg-query-engine.ts Outdated
Comment thread packages/evm-module/contracts/storage/Hub.sol
…rizedAccess ABI

- ST-2: PrivateContentStore now seals each literal `object` with AES-256-GCM
  before handing the quad to the underlying TripleStore. The on-disk N-Quads
  dump and any unauthorised SPARQL caller see only an `enc:gcm:v1:<base64>`
  envelope; getPrivateTriples reverses the seal. Encryption key resolution
  prefers an explicit constructor key, then DKG_PRIVATE_STORE_KEY, then a
  process-local random key (still satisfies at-rest confidentiality).

- ST-12: Oxigraph numeric-subtype canonicalization (e.g. xsd:long → xsd:integer)
  is reversed by capturing the publisher-declared datatype on insert and
  re-applying it during query result serialization for both bindings and
  CONSTRUCT outputs.

- Hub: re-declare `error UnauthorizedAccess(string msg)` on the contract so
  hardhat-chai-matchers can resolve the selector when tests pin
  revertedWithCustomError(HubContract, 'UnauthorizedAccess'). Hub itself
  continues to revert with `OwnableUnauthorizedAccount(msg.sender)` (OZ
  Ownable v5 alignment) — the declaration is purely an ABI-surface fix to
  match how HubDependent contracts already raise the library error.

- Hub-extra / v10-hub-audit: update the two `_checkOwnerOrMultiSigOwner`
  tests to expect `OwnableUnauthorizedAccount(msg.sender)` to match the
  new on-chain revert. Other gates (HubDependent.onlyContracts /
  onlyHubOwner / onlyHub) keep raising HubLib.UnauthorizedAccess and
  remain pinned via the Hub ABI.

- adapter-elizaos: relax the persistChatTurnImpl agent contract to accept
  `kcId: bigint | string` so DKGAgent.publish (which returns bigint) is
  type-compatible; coerce to string before returning. Unblocks build of
  agent / chain / publisher integration jobs.

E-9 ABI artefacts (KnowledgeAssetsStorage, KnowledgeAssetsV10) regenerated
to expose the new dual-emit KnowledgeBatchCreated event signature.

Made-with: Cursor
Comment thread packages/agent/src/dkg-agent.ts Outdated
Comment thread packages/query/src/dkg-query-engine.ts Outdated
Comment thread packages/adapter-elizaos/src/actions.ts Outdated
Comment thread packages/storage/src/private-store.ts Outdated
Comment thread packages/agent/src/endorse.ts Outdated
Comment thread packages/evm-module/contracts/KnowledgeAssetsV10.sol Outdated
Comment thread packages/storage/src/adapters/oxigraph.ts Outdated
…-SIV for at-rest privacy

- evm-module/E-11: ship MigratorV10Staking.sol — zero-token V8→V10 delegator
  state migrator (no Token.transfer; replays snapshot stake-bases via
  StakingStorage.setDelegatorStakeBase + integrity gate
  markNodeMigrated). Idempotent per (identityId, delegator), gated by
  Hub-owner / multisig-owner. Audit test now flips green; typechain
  path corrected to mirror generator's `typechain/contracts/migrations/`
  layout.
- evm-module/E-17: re-frame Profile-extra audit. The trust-layer spec
  ("50K TRAC to participate in the network") is satisfied at the
  Staking._addNodeToShardingTable gate, NOT at profile creation. Pin
  the actual enforcement point: Profile and Staking share one
  ParametersStorage; minimumStake = 50K; createProfile alone leaves
  the node out of the active sharding table.
- storage/ST-2: switch PrivateContentStore to deterministic AES-GCM-SIV
  (IV = HMAC-SHA256(key, plaintext)[:12]) and a stable default key
  domain. Same plaintext → same ciphertext, so equality-based
  pipelines (publisher async-lift `subtractFinalizedExactQuads`) work
  without decryption while at-rest plaintext is still hidden.
  Authorised round-trips via `getPrivateTriples` continue to decrypt.
- agent/e2e-security: assert raw SPARQL sees the
  `enc:gcm:v1:` envelope and authorised PrivateContentStore
  round-trips the original literal.
- adapter-openclaw/K-9: align openclaw.plugin.json id with package.json
  name (@origintrail-official/dkg-adapter-openclaw).

Made-with: Cursor
Comment thread packages/adapter-openclaw/openclaw.plugin.json Outdated
Comment thread packages/agent/src/signed-gossip.ts
Comment thread packages/query/src/dkg-query-engine.ts Outdated
Comment thread packages/storage/src/adapters/oxigraph.ts Outdated
Comment thread packages/storage/src/private-store.ts Outdated
… MigratorV10 audit

CLI hardening (BUGS_FOUND.md BURA):
- CLI-1: enforce scrypt KDF parameter floor (N ≥ 2^15, r ≥ 8, p ≥ 1,
  dklen == 32, salt ≥ 16 bytes) in `decryptKeystore`; also fix the
  parallel bug where the loader ignored kdfparams and always used
  the global SCRYPT_N. Bumped existing keystore.test.ts to 2^15.
- CLI-7: classify libp2p / multiformats client-side errors
  (Non-base58btc, ERR_INVALID_PEER, dial timeout, etc.) as 4xx
  instead of 500 in /api/query-remote.
- CLI-9: pre-validate batchId BigInt parsing (400 on garbage),
  map "not found" / chain-revert errors from /api/verify to 4xx,
  and scrub raw `data="0x…"` / `unknown custom error` payloads
  from every 500 body via sanitizeRevertMessage.
- CLI-10: implement spec §18 verifySignedRequest (HMAC-SHA256 over
  ts+body, ±5 min freshness, single-use nonce store) and add a
  Bearer-only replay guard (body-less mutating requests are
  fingerprinted on token+method+path, identical replays within 60 s
  → 401). Stale x-dkg-timestamp → 401 even on Bearer-only requests.
- CLI-11: add `rotateToken` / `revokeToken`; make `verifyToken`
  reconcile against the on-disk auth.token mtime so `dkg auth
  rotate` invalidates the old credential without a daemon restart.
  loadTokens snapshots the file set so reconciler subtracts stale
  file tokens without touching config-pinned ones.
- CLI-16: tighten `isValidContextGraphId` to refuse `..` substrings
  and `.` / `..` path segments — closes the path-traversal in
  /api/context-graph/create.

EVM (BUGS_FOUND.md TORNADO):
- E-11: assert against the artifact JSON instead of the typechain
  binding so the lean `hardhat.node.config.ts` (no @typechain/hardhat)
  passes; keep the typechain check as a bonus when the binding does
  exist locally.

CI (BUGS_FOUND.md TORNADO):
- ST-1: boot a real `lyrasis/blazegraph:2.1.5` service container
  for the Tornado: core lane and wire `BLAZEGRAPH_URL` so
  adapter-parity-extra.test.ts exercises both engines instead of
  the canned http stub. Maps :8080 (where Blazegraph actually
  listens) to host :9999 and waits up to 60 s for readiness.

Adapter (BUGS_FOUND.md KOSAVA):
- K-9: openclaw setup test now asserts manifest.id === pkg.name
  (the canonical scoped npm name) so plugin discovery can resolve
  the adapter by either field interchangeably.

Made-with: Cursor
Comment thread packages/agent/src/dkg-agent.ts Outdated
Comment thread packages/cli/src/auth.ts
Comment thread packages/adapter-openclaw/openclaw.plugin.json Outdated
Comment thread packages/cli/src/auth.ts Outdated
Comment thread packages/agent/src/endorse.ts Outdated
… Blazegraph SPARQL

- publisher P-1: write-ahead pre-broadcast journal entry + journal:writeahead phase
  events emitted before chain.createKnowledgeAssetsV10. Update phase-sequences.test
  golden sequence accordingly.
- publisher P-2: enforce wallet-lock fence inside update() for forward-progress
  transitions (claimed/validated/broadcast/included). Terminal states bypass.
  Stop syncWalletLockForJob from silently recreating cleared locks. Update
  fencing-and-kc-anchor-extra.test to match the corrected behavior.
- publisher ST-2 subtraction: pass decryptObjects=true when loading authoritative
  private quad keys so subtractFinalizedExactQuads can match plaintext input
  against on-disk AES-GCM-SIV envelopes.
- storage ST-2: export decryptPrivateLiteral so the publisher (and any other
  module) can decrypt private literals without instantiating PrivateContentStore.
- storage Blazegraph: fix DELETE template to include GRAPH keyword when using a
  graph variable (deleteByPattern). Drop ALL between conformance suite tests so
  the external store is hermetic across re-runs.
- mcp-server K-2: register mcp_auth tool (status/set/whoami) for credential
  introspection and connection probing. K-1: update tool-count and
  namespace-prefix tests to whitelist mcp_auth.

Made-with: Cursor
Comment thread packages/agent/src/dkg-agent.ts Outdated
Comment thread packages/agent/src/endorse.ts Outdated
Comment thread packages/agent/src/dkg-agent.ts Outdated
Comment thread packages/adapter-elizaos/src/actions.ts Outdated
Comment thread packages/cli/src/auth.ts
… (bigint serialization)

publicByteSize was typed as number but assigned a bigint at runtime, causing
TS2322 in CI. Mirror the tokenAmount approach: keep the field stringified
to remain JSON-serializable across journal persistence.

Made-with: Cursor
Comment thread packages/agent/src/endorse.ts Outdated
Comment thread packages/query/src/dkg-query-engine.ts Outdated
Comment thread packages/cli/src/auth.ts Outdated
Comment thread packages/storage/src/adapters/oxigraph.ts Outdated
Comment thread packages/storage/src/private-store.ts Outdated
…e graphs

The SPARQL-1.1 DELETE/WHERE form with a graph variable parses on Blazegraph
2.1.5 but does not actually remove matching quads through the REST endpoint
(returns 200 OK; subsequent SELECT still returns the quads). Materialize
matching graphs first via SELECT DISTINCT ?g, then issue an explicit
DELETE WHERE { GRAPH <g> { ... } } per graph plus one default-graph
delete. Verified by storage.test.ts conformance suite (deleteByPattern
removes matching quads, deleteBySubjectPrefix).

Made-with: Cursor
Comment thread packages/adapter-openclaw/openclaw.plugin.json Outdated
Comment thread packages/cli/src/auth.ts Outdated
Comment thread packages/cli/src/auth.ts
Comment thread packages/agent/src/dkg-agent.ts Outdated
Comment thread packages/agent/src/endorse.ts Outdated
…edupe + assistant-cache payload comparison

Two bot review threads from PR #229 round 31-5:

- FeDS (packages/adapter-elizaos/src/actions.ts:1173) — the headless
  branch reused buildAssistantMessageQuads() verbatim, including the
  schema:isPartOf <session> edge that ChatMemoryManager.getSession()
  walks to enumerate messages. When a canonical user-first turn
  later replayed for the same turnKey, the user-turn path wrote a
  SECOND assistant message at msg:agent:K (also session-scoped) and
  getSession() returned BOTH because the URIs differ. Tag the
  headless assistant message with dkg:headlessAssistantMessage
  "true" (mirrors the existing dkg:headlessUserMessage marker on
  the stub) and dedupe in getSession() by canonical turn key
  (strip "headless:" literal prefix off dkg:turnId). When BOTH
  variants exist for the same canonical key, drop the headless
  one. Headless replies that have NO canonical counterpart (the
  proactive-agent / recovery-path case) are kept untouched —
  dedupe activates only when both are present. The schema:isPartOf
  edge stays on the headless assistant message so that flow keeps
  surfacing in the standard enumeration.

- FeDT (packages/adapter-elizaos/src/index.ts:555) — the
  persistedAssistantMessages cache stored a bare `true` per
  (roomId, userMsgId, dest) key, treating ANY non-empty
  assistantText / assistantReply.text / state.lastAssistantReply
  through onChatTurn as proof the assistant leg was final. Hosts
  that pipelined a PROVISIONAL or STALE assistant string (e.g. an
  in-flight LLM partial parked on state.lastAssistantReply before
  the streaming completion fires) marked the cache → the later
  real onAssistantReply read assistantAlreadyPersisted=true and
  short-circuited, leaving the stored reply stuck on the
  partial/wrong text. Switch the cache to store the FULL assistant
  text the user-turn write actually persisted, and only set
  assistantAlreadyPersisted=true when the incoming reply text
  matches the cached value byte-for-byte (idempotent retry).
  Mismatches mean the host pipelined a provisional string and the
  FINAL reply landed later — leave the flag unset so the impl
  emits the new (final) assistant message. Empty strings are
  refused inside markAssistantPersisted as defence-in-depth.

Test plan: extends test/actions-behavioral.test.ts with a new r31-5
describe block pinning the headlessAssistantMessage marker and the
schema:isPartOf preservation; extends test/plugin.test.ts with a
new r31-5 describe block exercising the payload comparison
(provisional → no suppression, identical text → suppression,
options.assistantText / options.assistantReply.text resolution
parity, empty-text refusal, explicit caller signal precedence);
extends node-ui/test/chat-memory.test.ts with the dedupe
regressions (canonical wins, headless-only kept, SPARQL projection
includes the marker, cross-key dedupe is rejected). Updates the
previously failing parity tests to exercise byte-identical text on
both legs (the parity invariant survives the new payload
comparison).

Made-with: Cursor
Comment thread packages/agent/src/workspace-config.ts Outdated
Comment thread packages/adapter-elizaos/src/index.ts
Comment thread packages/adapter-elizaos/src/index.ts
…ema + userMessageId cache mismatch + assistant text supersede)

Three issues surfaced after the r31-5 push:

  1. agent/src/workspace-config.ts (F08s)
     `parseWorkspaceConfig` expected `node:` to be a string, but the
     canonical `.dkg/config.yaml.example` (and other loaders in the
     monorepo) define `node:` as `{ api, tokenFile?, token? }`. Real
     workspaces threw at startup. Fix: `WorkspaceConfig.node` is now
     `WorkspaceConfigNode` (object); `parseNodeField` accepts either a
     bare string (normalised to `{ api: <string> }`) or the canonical
     object shape and validates the required `api` field. Tests updated
     to assert `cfg.node.api` and to cover both input shapes + error
     paths (missing/empty `api`).

  2. adapter-elizaos (F080 — index.ts:596 / actions.ts:941)
     `onChatTurnHandler` cached the persisted-turn marker under
     `optsAny.userMessageId ?? message.id` (r29-2), but
     `persistChatTurnImpl` only honoured `optsAny.userMessageId` on
     the assistant-reply path. In user-turn mode the impl silently
     dropped the pre-minted id and keyed `turnSourceId` off
     `message.id`. Hosts that pre-mint a user-turn id ended up with a
     cache key that disagreed with the on-disk turn URI: the matching
     `onAssistantReply` saw the cache hit, took the cheap append-only
     branch, and wrote `hasAssistantMessage` onto a turn URI that
     didn't exist — replies became unreadable. Fix: honour
     `optsAny.userMessageId` on BOTH paths so the cache key and the
     RDF turn URI converge.

  3. adapter-elizaos (F082 — index.ts:521)
     When the user-turn write embedded a PROVISIONAL assistant string
     (e.g. partial-streaming completion the host parked on
     `state.lastAssistantReply` before the final reply landed) and the
     follow-up `onAssistantReply` brought DIFFERENT final text, the
     wrapper only suppressed on byte-for-byte match (r31-5) and
     otherwise fell through to `_dkgServiceLoose.persistChatTurn` with
     `userTurnPersisted: true` still in place. The impl took the
     append-only branch and stamped a SECOND
     `schema:text` / `schema:dateCreated` / `schema:author` triple on
     the same `msg:agent:${turnKey}` URI — multi-valued predicates
     made `ChatMemoryManager.getSession()` LIMIT 1 readback
     nondeterministic, freezing or flickering the displayed text.
     Fix (writer + reader, modelled in the graph): on text mismatch
     with non-empty incoming, the wrapper sets
     `userTurnPersisted=false` AND `assistantSupersedesCanonical=true`,
     which routes the write through the headless branch onto the
     distinct `msg:agent-headless:${turnKey}` URI and tags it with
     `dkg:supersedesCanonicalAssistant "true"`. The reader's r31-5
     dedupe pass inverts its canonical-wins preference for that turn
     key only, so the FRESH headless message surfaces and the stale
     provisional canonical is filtered out. Empty incoming text
     deliberately keeps the canonical (better than blanking the
     reply). Inversion is scoped to assistant-author messages on the
     matching canonical turn key — user messages and unrelated keys
     are unaffected.

Tests:
  * adapter-elizaos: 5 new tests in plugin.test.ts (supersede flag set
    on mismatch + non-empty; empty-incoming guard; idempotent-match
    still wins; cache-miss falls through; user-turn userMessageId
    pre-mint aligns with cache key) + 5 new tests in
    actions-behavioral.test.ts (supersede marker on headless write;
    headless without flag stays clean; canonical writes never carry
    the marker; user-turn write with explicit userMessageId mints
    the canonical user URI under userMessageId; missing userMessageId
    falls back to message.id).
  * node-ui: 4 new tests in chat-memory.test.ts (inversion drops
    provisional canonical and surfaces fresh headless; cross-key
    scope is preserved; user messages never dropped; SPARQL query
    fetches the new flag).
  * agent: workspace-config + workspace-config-extra updated for the
    new object-shape contract and exercise both input forms.

Verified: all 210 adapter-elizaos tests, all 369 node-ui tests, all
508 agent tests pass; tsc --noEmit clean across all three packages
plus their test configs.

Made-with: Cursor
Comment thread packages/cli/src/auth.ts Outdated
Comment thread packages/storage/src/adapters/blazegraph.ts Outdated
Comment thread packages/publisher/src/chain-event-poller.ts Outdated
…erify + blazegraph blank-node round-trip + idle-poller short-circuit)

Bug PRRT_kwDORwbl8c5-Grq8 (cli/src/auth.ts:947): for body-carrying
signed requests, drain the body and verify the HMAC BEFORE returning
from `httpAuthGuard`. Previously the response-time guard rewrote the
handler's response to 401 if HMAC was never verified, but the handler
had already run by then — any state-mutating side effect on a forged
signature went through and only the response payload was suppressed.
Fix: the body-carrying signed-request branch now returns
`Promise<boolean>`; `installSignedRequestResponseGuard` drives the
eager drain + verify upfront, stashes the buffered body on
`req.__dkgPrebufferedBody`, and only resolves true after the HMAC
binds against the actual wire bytes. Daemon body readers
(`readBody` / `readBodyBuffer`) prefer the pre-buffered body so they
don't re-attach listeners on an exhausted stream. Lifecycle handler
loop now `await`s the guard.

Bug PRRT_kwDORwbl8c5-Grq- (storage/adapters/blazegraph.ts:94/131/164):
the SELECT-then-DELETE branches of `deleteByPattern` materialise rows
that may include blank-node subjects (`_:b0`). Re-serialising as
`<${escapeUri(sx)}>` produced the syntactically invalid IRI `<_:b0>`
and the resulting `DELETE DATA` either errored or silently no-op'd,
leaving blank-node-subject quads pinned in storage. Route every RDF
position through `formatTerm` so blank nodes stay `_:b0` (predicates
remain angle-bracketed because RDF predicates can only be IRIs).

Issue PRRT_kwDORwbl8c5-GrrD (publisher/chain-event-poller.ts:305):
the early-return gate keyed on `!!this.onUnmatchedBatchCreated`, but
DKGAgent now wires that callback unconditionally for every node — so
the flag was effectively `true` everywhere and fresh nodes with empty
WALs polled `KnowledgeBatchCreated` / `KCCreated` every tick.
Switched to `!walRecoveryActive` (which honours `hasRecoverableWal`
when provided) so idle nodes correctly short-circuit. Legacy callers
that don't supply `hasRecoverableWal` still get the old "callback
implies WAL is live" semantics.

Regression tests:
  - `packages/storage/test/blazegraph.unit.test.ts` — 4 new tests in
    `deleteByPattern — blank-node round-trip (r31-7 regression)`
    covering single-graph, no-graph named, no-graph default, and
    pattern.subject blank-node paths.
  - `packages/publisher/test/chain-event-poller-r24-4.test.ts` — 4
    new tests in `r31-7: tick early-return must key on
    walRecoveryActive (NOT bare callback presence)`. Also adjusted
    the prior `does SEED near tip when callback is wired but
    hasRecoverableWal returns false` test to add an
    `onContextGraphCreated` watcher so the seed-near-tip path is
    still exercised — without an external reason to scan the new
    early-return correctly skips the tick (which is exactly the
    behaviour the new dedicated regression tests pin).

Verified: - tsc --build clean across cli + storage + publisher.
  - publisher 859/859 pass.
  - cli auth + daemon 391/391 pass.
  - storage 185/185 pass (the BLAZEGRAPH_URL parity test only
    runs in CI with the env var set).
Made-with: Cursor
Comment thread packages/storage/src/adapters/oxigraph.ts
Comment thread packages/agent/src/ccl-fact-resolution.ts
Comment thread packages/query/src/dkg-query-engine.ts Outdated
…ion dtype conflict + ccl legacy endorsement back-compat + sparql WHERE-locator token-aware)

- HRXf (storage/oxigraph.ts): two writes at the same (s,p,value,g) with
  DIFFERENT declared numeric subtypes (e.g. xsd:int + xsd:positiveInteger
  for lexeme `1`) silently overwrote the side-table entry, so SELECT/
  CONSTRUCT readbacks reported the latest-write-wins subtype for both
  logical sources. Track per-position conflicts in
  conflictedNumericDatatypeKeys and the conflicted lexemes in
  conflictedNumericDatatypeLexemes; restoreOriginalDatatype short-circuits
  to canonical xsd:integer for conflicted positions, and
  restoreOriginalDatatypeForSelectBinding refuses to lexically restore
  any binding row carrying a conflicted lexeme. Sidecar bumped to v2 to
  persist the conflict markers across restarts; eviction paths now drop
  conflict keys for the affected graph too.

- HRXj (agent/ccl-fact-resolution.ts): resolveEndorsementFacts only
  matched the new per-event endorsement-resource shape introduced in
  r19-3 (?endorsement dkg:endorses ?ual + ?endorsement dkg:endorsedBy
  ?endorser). Every endorsement quad published BEFORE r19-3 lives as
  the legacy direct shape (<agent> dkg:endorses <ual>); without back-
  compat those vanish on deploy and CCL endorsement_count silently
  flips to 0, denying access to genuinely-endorsed UALs. Now UNION both
  shapes (with FILTER NOT EXISTS to avoid double-counting a quad that
  matches both) and dedupe (endorser, ual) pairs in JS so the policy
  semantic stays "distinct endorsers".

- HRXo (query/dkg-query-engine.ts): findWhereBraceStart's fast path was
  a raw regex /\bWHERE\s*\{/i that matched ANY occurrence — including
  inside string literals, # comments, and IRI fragments. Adversarial /
  obfuscated input like `SELECT ("WHERE {" AS ?x) WHERE { ... }` made
  the rewriters wrap the WRONG block, producing invalid queries or
  silently filtering against the literal. Replace with a token-aware
  scanner (findExplicitWhereTokenIdx + nextSignificantBraceAfter) that
  mirrors the lex rules already used by the rest of the helpers (skips
  comments, single/double/triple-quoted literals, IRIREFs, and enforces
  word boundaries on the WHERE keyword).

Tests:
  - storage/test/oxigraph-extra.test.ts: +4 cases pinning the per-
    position + lexeme conflict refusal contract (SELECT, CONSTRUCT,
    cross-graph SELECT, idempotent same-dtype path).
  - agent/test/ccl-fact-resolution-r31-8.test.ts (new): +5 cases
    covering legacy-only quads, mixed-shape de-dup, recall preservation,
    and the FILTER NOT EXISTS double-count guard.
  - query/test/query-extra.test.ts: +5 cases for the WHERE locator —
    string-literal alias decoy, # comment decoy, IRI containing WHERE,
    triple-quoted decoy, and word-boundary check (?WHEREVER alias).

All affected packages typecheck clean; package-level vitest suites pass
(only pre-existing real-Blazegraph parity test stays red without
BLAZEGRAPH_URL — unchanged).

Made-with: Cursor
Comment thread packages/adapter-elizaos/src/service.ts
Comment thread packages/adapter-elizaos/src/service.ts Outdated
…e flag + userMessageId on user-turn options)

Two bot threads on PR #229 (r31-9) flagged that the public typed
DKGService surface had drifted from the runtime contract that r31-6
established:

H2fh — service.ts:70 (assistantSupersedesCanonical)
  r31-6 added a runtime "supersedes canonical" branch in
  persistChatTurnImpl that emits a `dkg:supersedesCanonicalAssistant`
  marker so the reader prefers the headless assistant message over the
  stale provisional text embedded in the user-turn body. The wrapper
  in src/index.ts opts into it, but the public
  AssistantReplyChatTurnOptions interface did not expose the flag, so
  any direct integration calling `dkgService.persistChatTurn(…)` had
  to drop to `as any` (or the deprecated dkgServiceLegacy) to access
  it. The result for those callers was stacked `schema:text` triples
  on the canonical assistant URI — the original H2fh repro.

  Add `readonly assistantSupersedesCanonical?: boolean` to
  AssistantReplyChatTurnOptions (src/service.ts) AND to the underlying
  ChatTurnPersistOptions (src/types.ts) so the field is discoverable
  from both the typed surface and the legacy options bag. Optional —
  legacy callers compile unchanged. Doc comment pins the contract:
  ignored unless `userTurnPersisted: false` (the headless branch).

H2fk — service.ts:86 (userMessageId on user-turn options)
  r31-6 also plumbed `options.userMessageId` through the user-turn
  write path so a host can pre-mint an id and have the persisted-turn
  cache key + the on-disk turn URI converge against the assistant's
  later `onAssistantReply` call. The runtime accepts it, but
  UserTurnChatTurnOptions declared `userMessageId?: never`, forcing
  TS callers using the pre-mint flow to drop to `as any` /
  dkgServiceLegacy and defeating the typed surface.

  Change to `userMessageId?: string` to match the runtime: when
  present and non-empty, persistChatTurnImpl keys the canonical turn
  URI off it; when absent, it falls back to message.id. Doc comment
  pins the contract.

Regression coverage (test/dkg-service-overloads.test.ts):
  - UserTurnChatTurnOptions ACCEPTS userMessageId (pre-mint flow type-checks)
  - UserTurnChatTurnOptions allows OMITTING userMessageId (anti-mandatory)
  - AssistantReplyChatTurnOptions EXPOSES assistantSupersedesCanonical
  - AssistantReplyChatTurnOptions assistantSupersedesCanonical is OPTIONAL
  - source-level pin: `userMessageId?: never` is GONE (anti-drift guard)
  - source-level pin: `assistantSupersedesCanonical` declared on the type

Source-level pins read service.ts back at runtime so a future
re-narrowing of either field surfaces here even if the literal-call
test happens to be deleted in a refactor.

Type-check: pnpm exec tsc --build --force packages/adapter-elizaos → clean.
Tests: 216/216 pass in the adapter-elizaos suite.
Made-with: Cursor
Comment thread packages/node-ui/src/chat-memory.ts Outdated
Comment thread packages/publisher/src/dkg-publisher.ts Outdated
Comment thread packages/cli/src/daemon/http-utils.ts Outdated
Comment thread .github/workflows/ci.yml
Comment thread packages/publisher/src/dkg-publisher.ts
… + WAL backcompat + WAL dir-fsync + CodeQL regex scope + Blazegraph parity ns)

ILd- (node-ui/chat-memory.ts:971): the r31-5 dedupe predicate
`if (!m.isHeadlessAssistant) groupHasNonHeadless.set(...)` falsely
counted the always-non-headless USER turn as proof that a canonical
ASSISTANT message also exists for the same turn key, so a session
with [user-turn, headless-assistant-reply] dropped the headless
reply and rendered with no agent message at all. Narrow the
predicate to `(!isHeadlessAssistant && author === 'agent')` so only
real canonical assistant messages influence the dedupe.

ILeC (publisher/dkg-publisher.ts:87): `isValidJournalEntry`
unconditionally required `v10ContextGraphId` and `publishDigest`,
silently discarding pre-r30 WAL rows on upgrade. Relax both fields
to `string | undefined` on read and hydrate to '' in
`readWalEntriesSync` so legacy entries recover and feed the merkle-
root → op lookup that the chain-event poller depends on.

ILeL (cli/daemon/http-utils.ts:343): the last-mile
js/stack-trace-exposure pacifier regex chain ran on EVERY response
body, so a successful 2xx payload that legitimately contained
v8-frame-shaped substrings (issue title with copy-pasted trace,
SPARQL literal embedding source-position metadata, etc.) was
silently corrupted. Gate the chain on `status >= 400` — CodeQL's
data-flow concern is exclusively err.message → res.end(body), an
error-path concern.

ILeR (.github/workflows/ci.yml:256): storage tests run two suites
in parallel against the SAME Blazegraph namespace, and
`storage.test.ts`'s `DROP ALL` races `adapter-parity-extra.test.ts`'s
fixture. Provision two distinct namespaces (`dkgq` and
`dkgq-parity`) and route the parity suite at
`BLAZEGRAPH_PARITY_URL` so the two suites no longer share state.

ILeV (publisher/dkg-publisher.ts:141): `rewriteWalSync` fsynced
only the temp file before `renameSync`/`unlinkSync`; on POSIX a
crash between the rename and the parent-dir flush leaves the
directory entry dangling and the WAL undefined. Add `fsyncDirSync`
and call it after every rename/unlink so the WAL is fully crash-
durable.

Regression tests pin every behaviour:
- node-ui/test/chat-memory.test.ts (4 new cases): the ILd- repro,
  the r31-5 anti-regression (canonical wins when present),
  headless-only sessions, user-only sessions
- cli/test/daemon-http-utils-helpers.test.ts (6 new cases): success
  bodies round-trip verbatim across 1xx/2xx/3xx, error bodies still
  scrub at the 400 boundary, real Node ENOENT stack still gets
  scrubbed at 500
- publisher/test/wal-recovery.test.ts (extensive): legacy entries
  recover, merkle-root lookup works, mixed legacy/new ordering,
  required-field strictness preserved, fsyncDirSync defined,
  rename + unlink dir-fsync paths exercised

Made-with: Cursor
Comment thread packages/node-ui/src/chat-memory.ts
Comment thread packages/adapter-elizaos/src/index.ts Outdated
Comment thread packages/adapter-elizaos/src/actions.ts Outdated
…rbitration + empty-reply guard + session-root reservation race)

IoNL (node-ui/chat-memory.ts:1213): getSessionGraphDelta() bare-literal
canonical lookup ALWAYS won when a canonical user-first turn existed,
even when r31-6 had ALSO written a SUPERSEDING headless variant marked
`dkg:supersedesCanonicalAssistant "true"`. getSession() (which honours
the marker via the r31-6 dedupe) and getSessionGraphDelta() (which did
not) ended up on different branches: full-refresh callers saw the
FRESH headless text, delta callers saw the STALE canonical text. Fix
adds a SPARQL probe after a canonical hit — if a headless twin
carries the supersede marker, the impl re-resolves to the headless
URI and the CONSTRUCT projection pulls the FRESH triples. Watermark
logic stays on the canonical id so subsequent deltas chain
correctly. Probe is gated to canonical-hit cases only (`!turnId.
startsWith('headless:')`) so it doesn't over-fire on already-headless
callers or burn round trips on misses.

IoNQ (adapter-elizaos/src/index.ts:527): the empty-incoming follow-up
case used to be a fall-through. The wrapper handled equality (set
`assistantAlreadyPersisted=true`) and mismatch (route to headless
URI w/ supersede marker), but `incomingReplyText.length === 0` fell
into the mismatch branch, so the wrapper would route the EMPTY text
to a headless URI marked `supersedesCanonicalAssistant`, and the
reader's r31-6 dedupe would surface the EMPTY headless reply
instead of the cached non-empty canonical reply — chat history
silently flipped to a blank assistant message. Fix adds an explicit
`else if (incomingReplyText.length === 0)` branch that marks
`assistantAlreadyPersisted=true` (suppression, not supersede) when
an empty follow-up arrives with cached non-empty canonical text. The
existing canonical text is strictly better than blank.

IoNR (adapter-elizaos/src/actions.ts:460): the previous "peek-then-
mark-after-success" session-root cache pattern split the gate into a
peek (`wouldEmitSessionRoot`) + a `markSessionRootEmitted` AFTER the
`await agent.assertion.write(...)` resolved. JavaScript's single-
threaded model only protects synchronous code — concurrent
`persistChatTurn` calls for the same `(runtime, sessionUri,
contextGraphId, assertionName)` tuple could BOTH "peek" before
either marked, BOTH include the `?session a schema:Conversation`
root, and the second write would trip the WM Rule-4 entity-
exclusivity guard with a duplicate-root failure. Fix replaces the
peek-then-mark pattern with `reserveSessionRoot()` (synchronous
atomic CAS — at most one caller wins per key per process) plus
`rollbackSessionRoot()` released from the failure path of
`agent.assertion.write()`/`ensureContextGraphLocal()` so retries can
re-emit (preserves r3131820483 crash-safety).

Tests:
- chat-memory.test.ts: 3 new IoNL regression tests covering canonical
  hit + superseding headless twin → delta projects headless URI;
  canonical hit + non-superseding headless twin → keeps canonical;
  caller passes `headless:` turnId → supersede probe SKIPPED.
- chat-memory.test.ts: 6 existing tests updated to account for the
  new supersede-twin probe SPARQL call (one extra mock + bumped
  `toHaveLength` assertions).
- plugin.test.ts: 3 new IoNQ regression tests covering empty
  incoming reply via message.content + via options.assistantText/
  options.assistantReply.text + non-empty mismatch (still routes
  through SUPERSEDE branch, IoNQ guard does not over-fire).
- actions-behavioral.test.ts: 4 new IoNR regression tests covering
  concurrent persists (exactly ONE write carries the root); write
  FAILURE → rollback → retry RE-EMITS the root; write SUCCESS keeps
  the reservation (no over-rollback); ensureContextGraphLocal
  failure also rolls back (the rollback covers the FULL try block).

Made-with: Cursor
Comment thread packages/node-ui/src/chat-memory.ts Outdated
Comment thread packages/node-ui/src/chat-memory.ts Outdated
…p + supersede dedupe shared across all readers)

JNLF (chat-memory.ts:1419) — getSessionGraphDelta SELECT was using a
single `effectiveTurnUri` for both `?user` and `?assistant` after the
r31-11 IoNL fix re-routed to the headless turn URI on supersede. The
headless writer emits a synthetic `msg:user-stub:K` (typed
`dkg:HeadlessUserStub`, empty `schema:text`) on its `dkg:hasUserMessage`
edge so the reader's "both edges resolve" contract stays satisfied. The
delta therefore dropped the user's actual text and could regress
incremental consumers to a blank/system-authored user node. Fix splits
the lookup: `?user` always resolves from the canonical `turnUri`,
`?assistant` resolves from the headless URI only when supersede fires.
The related-subjects seed + CONSTRUCT subjectSet now include both turn
URIs so envelope quads on either side are projected together.

JNLL (chat-memory.ts:1003) — the r31-5 / r31-6 canonical-vs-headless
arbitration only ran inside `getSession()`. `getRecentChats()` and
`getStats()` read raw `?m a schema:Message` rows, so duplicates surfaced
twice in the recents panel and inflated `messageCount`. Extracted the
arbitration into the `applySupersedeDedupe()` helper at module top and
applied it to all three readers:
  - `getSession()` now calls the helper instead of inlining the logic.
  - `getRecentChats()` projection picks up `?turnId / ?headless /
    ?supersedes` (OPTIONAL — backwards-compat with legacy messages) so
    the helper has the inputs it needs.
  - `getStats()` issues a corrective SPARQL COUNT for canonical/headless
    duplicate pairs and subtracts that from `messageCount` (clamped to
    0). `knowledgeTriples` deliberately untouched — see helper docstring
    for why the chat-related triple math is unaffected.

Tests: 9 new + 1 updated regression in chat-memory.test.ts (3 for JNLF
SPARQL split + 6 for JNLL multi-reader parity / stats clamping). All
386 node-ui tests + tsc clean.

Made-with: Cursor
Comment thread packages/chain/src/mock-adapter.ts Outdated
…ed mock now projects publicByteSize + tokenAmount from params (not hardcoded zero)

Bot review (J8hn @ packages/chain/src/mock-adapter.ts:200): the
V10KnowledgeBatchEmitted shim emitted by both publishKnowledgeAssets
and publishKnowledgeAssetsPermanent hardcoded `publicByteSize: '0'`
and `tokenAmount: '0'` even though both fields are first-class on
PublishParams / PermanentPublishParams. The real EVM adapter
(evm-adapter.ts:890 / :896) decodes them off the on-chain log, so any
mock-backed fixture or consumer asserting on byte-size or token-cost
accounting silently passed against the mock while regressing against
the real chain — the exact mock-vs-real desync the V10 emission shim
was added to prevent.

Fix: pull both values straight from `params.publicByteSize.toString()`
and `params.tokenAmount.toString()` on both publish paths.

Epoch fields stay zero — the mock doesn't model the on-chain epoch
counter (real KASStorage computes startEpoch/endEpoch from
`block.timestamp` at write time, and `params.epochs` is the COUNT, not
the start/end window). Documented why on the call-site comment so a
future contributor doesn't try to "also fix" them with a wrong
projection.

Tests: 3 new in mock-adapter-behavioral.test.ts pin the projection on
both publish paths AND a third test that publishes two distinct
fixtures to prove the events differentiate (no constant-zero collapse).
All 228 chain tests + tsc clean.

Made-with: Cursor
Comment thread packages/adapter-elizaos/src/actions.ts Outdated
Comment thread packages/storage/src/adapters/oxigraph.ts
…` short-circuit on '' + lexeme-marker GC after conflict-key eviction)

KK3X — packages/adapter-elizaos/src/actions.ts:1172
  Both the assistant-reply branch and the user-turn branch resolved
  `assistantText` via a `??`-chain, which only bridges null/undefined
  and SHORT-CIRCUITS on `''`. Real-world ElizaOS assistant memories
  often surface as `message.content.text === ''` with the actual
  reply riding on `options.assistantText` / `options.assistantReply.text`
  / `state.lastAssistantReply` — and the user-turn path also breaks
  when a caller passes an explicit empty `optsAny.assistantText`.
  Symptoms: assistant-reply persisted blank `schema:text` on the
  canonical `msg:agent:K` subject; user-turn silently dropped the
  entire assistant leg even though the real reply was in state.
  Fix: replace both chains with a `pickNonEmptyText(...)` helper
  that selects the first non-empty (and non-whitespace-only)
  candidate, mirroring the wrapper boundary in `src/index.ts`. All
  candidates absent → collapses to `''` exactly as before, preserving
  the all-empty contract.

KK3b — packages/storage/src/adapters/oxigraph.ts:169
  `forgetNumericDatatype` and `evictNumericDatatypeForGraph` dropped
  the per-key conflict marker but kept the companion lexeme-level
  marker "pessimistically" forever. Once `"V"` had a transient
  per-position conflict at any key, EVERY future write of any
  `"V"^^xsd:<numeric>` literal across the entire store fell back
  to canonical `xsd:integer` even after the contributing quad /
  graph / subject prefix was gone — permanent subtype loss for
  that lexeme. Fix: introduce `maybeReleaseLexemeMarkers()` that
  re-evaluates the lexeme set against ground truth: a lexeme
  marker is released if and only if NO remaining conflict-key
  still references that lexeme. Both `forgetNumericDatatype`
  and `evictNumericDatatypeForGraph` collect the affected
  lexemes after evicting per-key entries and call the helper.
  Also adds `parseLexemeFromNumericDatatypeKey` (reverse of the
  4-segment NUL-keyed shape) so the GC can walk the conflict-key
  set without re-deriving keys from quads.

Regression coverage:
  - 11 new tests in adapter-elizaos/test/actions-behavioral.test.ts
    pinning both branches: assistant-reply (5 tests for empty
    content, assistantText, assistantReply, state, all-empty,
    whitespace-only, first-wins) and user-turn (5 tests for the
    same matrix plus the user-only no-assistant-leg shape).
  - 5 new tests in storage/test/oxigraph-extra.test.ts pinning
    delete()/dropGraph()/deleteBySubjectPrefix() lexeme-marker
    GC and the partial-eviction guard (lexeme stays while another
    key still conflicts), plus a sanity test that
    forgetNumericDatatype on a non-numeric quad doesn't release
    unrelated markers.

233/233 adapter-elizaos tests pass; 191/191 storage tests pass.

Made-with: Cursor
Comment thread packages/storage/src/private-store.ts Outdated
Comment thread packages/cli/src/auth.ts
…scade-rejection + auth config-pinned token revocation)

KwH_ (private-store.ts:491): the per-graph write lock built `chained` off
`prev.then(() => next)` and `await prev` lived OUTSIDE the try/finally
that calls `release()`. Any rejected predecessor cascaded into the queue:
`await prev` threw before entering the try{}, `release()` never ran, the
lock entry stayed permanently pending, and every later writer for that
graph was bricked until process restart. Fix: build/await the chain off
`safePrev = prev.catch(() => undefined)` so a predecessor's rejection
cannot poison the queue. Belt-and-suspenders: the chained promise also
rebases onto safePrev so direct adversarial poisoning of the lock map
(white-box future-proofing) keeps draining cleanly. New regression suite:
4 tests in private-store-extra.test.ts, including a directly-poisoned
predecessor scenario that hangs/fails the buggy code under a 5s deadline.

KwIE (auth.ts:203): `loadTokens()` merges file-derived AND
`config.auth.tokens` into the same `Set`, but `lastFileSnapshot` only
tracked `fileTokens`. When a token was pinned BOTH in `auth.token` and
in config (a real-world rollout shape — operators sync the same admin
secret across both control surfaces during config rollouts), every path
that compared "what's now on disk" against "what was on disk last time"
revoked the in-memory entry as soon as the file rotation removed it,
silently invalidating a configured admin credential until restart. Fix:
extend the snapshot with a separate `configTokens: Set<string>`,
populate it from `AuthConfig.tokens` at load, re-seed it across
rotations, and gate every revoke loop (reconcileFileTokens, rotateToken,
revokeToken's ENOENT branch) on `if (configTokens.has(t)) continue`.
Operator-explicit revoke still wins (the named target is removed
regardless of provenance). New regression suite: 5 tests in
auth-behavioral.test.ts that exercise the overlap shape across all
three deletion paths (reconcile, rotate, ENOENT cascade) and pin the
explicit-target override.

All new tests verified to FAIL against the buggy code and PASS with the
fix in place — no false-positive coverage. Tests:
  packages/storage/test/private-store-extra.test.ts: 40 passed (+4)
  packages/cli/test/auth-behavioral.test.ts:           88 passed (+5)

Made-with: Cursor
if (cachedAssistantText !== undefined) {
const replyOpt = (options as any)?.assistantReply as { text?: unknown } | undefined;
const incomingReplyText =
(typeof (message as any)?.content?.text === 'string' && (message as any).content.text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: this selector still uses raw || semantics, while persistChatTurnImpl now ignores whitespace-only candidates. If message.content.text is ' ' but options.assistantText carries the real reply, the wrapper treats it as a content change and forces assistantSupersedesCanonical, creating a spurious headless override for identical content. Reuse the same trimmed “first non-empty” selection here (and in the cache-write path below) so the wrapper and writer classify replies the same way.

// path when (b) is missing — fall through to the safe
// headless full-envelope path that emits both edges on a
// distinct headless turn URI (also fixed in r21-1).
const userTurnPersisted =
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: assistantSupersedesCanonical is part of the new public options surface, but this branch silently ignores it whenever userTurnPersisted stays true and falls through to append-only. A direct caller can then append a second canonical assistant schema:text instead of getting the promised headless supersede, which recreates the multi-valued assistant-text bug this flag is meant to avoid. Please validate this combination and either throw or force the headless path when assistantSupersedesCanonical is set.

// breaking change for downstream `as any` callers — they had no
// in-package surface to switch onto. See `service.ts` for the rest
// of the rationale.
export type { DKGServiceLoose } from './service.js';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: the package export map only exposes the root entrypoint, but this file doesn’t re-export the new public service types (DKGService, ChatTurnPersistResult, UserTurnChatTurnOptions, AssistantReplyChatTurnOptions). Downstream TypeScript consumers can’t import the typed hook/service contracts added in this PR without an unsupported deep import, so the new API surface is effectively trapped inside the package. Consider re-exporting those types here.

Strips comments, JSDoc preambles, and test-name labels that referenced
the internal review process (PR #229 round X / rXX-Y / BUGS_FOUND.md
citations) from production code, tests, Solidity contracts, and CI
workflows. The technical "why" content next to each citation is
preserved; only the meta-narration about the review process is removed.

Net: 130 files, -96 lines. Builds clean across all 14 packages; all
local test suites pass that were not already failing on origin/main
(pre-existing chain workspace shim, cli catchup-runner worker_threads,
and storage ST-1 BLAZEGRAPH_URL gate). The previously failing r31-4
anti-drift test in agent/test/per-cg-quorum-rpc-failure-extra was
updated to assert the structural property directly (parsedId
discriminator presence) instead of source-grepping for the marker
comment that no longer exists.

Made-with: Cursor
?? (typeof rawMemoryId === 'string' && rawMemoryId.length > 0 ? rawMemoryId : undefined);
if (!turnSourceId) {
throw new Error(
'persistChatTurnImpl: missing stable message identifier — ' +
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: this makes message.id a hard runtime requirement on the user-turn path, but the plugin/hook entrypoints still accept framework Memory objects and this PR's own Memory type keeps id optional. Any existing Eliza integration that forwards plain onChatTurn payloads without an id will now fail every persistence attempt instead of degrading gracefully. Keep the stricter typed overload if you want, but the runtime path needs a deterministic fallback/override instead of an unconditional throw.

const assistantReplyOpt = optsForAssistant.assistantReply as { text?: unknown } | undefined;
const stateForAssistant = (state ?? {}) as { lastAssistantReply?: unknown };
const assistantText =
(typeof optsForAssistant.assistantText === 'string' && optsForAssistant.assistantText)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: this cache now records any truthy string as an already-persisted assistant reply, but persistChatTurnImpl uses a trimmed/non-empty selector and will skip whitespace-only assistantText/assistantReply.text. That means a placeholder like ' ' poisons the cache even though no assistant message was written, and the later real onAssistantReply gets misrouted through the headless/supersede path. Use the same trimmed/non-empty selector here (and in the incomingReplyText comparison above) so the wrapper mirrors the writer.

// breaking change for downstream `as any` callers — they had no
// in-package surface to switch onto. See `service.ts` for the rest
// of the rationale.
export type { DKGServiceLoose } from './service.js';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: the PR introduces DKGService, UserTurnChatTurnOptions, AssistantReplyChatTurnOptions, and ChatTurnPersistResult as the new typed migration surface, but the barrel only re-exports DKGServiceLoose. Because this package only exports . from package.json, downstream consumers cannot import the strict types from the published package without unsupported deep imports. Re-export the strict service types here as well, otherwise the typed migration path only works inside this repo.

The `usedFullBuildFallback` skip silently bypassed `shouldRebuildContracts()`
whenever the slot's `package.json` lacked a `build:runtime` script (the
"legacy `pnpm build`" path). That made three of the autoupdater-hardening
tests on `main` fail — they assert that:

  1. `autoUpdate.buildTimeoutMs.contracts` is honoured during the contract
     rebuild,
  2. `pnpm --filter dkg-evm-module clean` runs immediately before
     `pnpm --filter dkg-evm-module build`, and
  3. a failed contract diff retries via `git fetch --depth=1 <fetchUrl>
     <currentCommit>` before giving up.

These tests never reached the contract branch under the fallback, and
production was actually broken in the same way: workspace `pnpm build`
runs `hardhat compile` but never `hardhat clean`, so renamed/deleted
contracts' stale ABI/typechain output would survive into the inactive
slot and get swapped live.

Drop the fallback-only skip and gate the contract path solely on
`shouldRebuildContracts()`. The no-source-change branch is unchanged
(Hardhat compile cache stays warm to avoid the cold-solc/ARM64 timeout).
The log line distinguishes the two reach-paths so post-mortems can still
tell whether the runtime came from `build:runtime` or the workspace build.

Made-with: Cursor
return '';
};
const assistantText = pickNonEmptyText(
(message as any)?.content?.text,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: This now treats message.content.text as authoritative over options.assistantText / options.assistantReply.text. In the partial-streaming case the runtime can leave a stale non-empty snapshot in message.content while the final reply arrives in the options bag, so we persist the stale text and the supersede path never converges. Prefer the explicit assistant-text options before the memory snapshot here (and in the matching cache-comparison logic in onAssistantReplyHandler).

runtime: IAgentRuntime,
message: Memory,
state: State | undefined,
options: AssistantReplyChatTurnOptions,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Tightening onAssistantReply to require AssistantReplyChatTurnOptions is a source-breaking API change for existing TS consumers. The runtime handler still infers userMessageId from replyTo / parentId / inReplyTo and can synthesize the missing flags, so callers that compiled before will now fail type-checking for no runtime benefit. Please keep a compatibility overload (or optional options) on the public hook surface and narrow internally.

export interface DkgUserTurnHook {
(
runtime: IAgentRuntime,
message: PersistableMemory,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Requiring PersistableMemory on the public user-turn hooks is another breaking contract change. Upstream ElizaOS's published Memory type does not guarantee an id, so existing integrations that simply forward framework Memory objects will now stop compiling or hit the new runtime throw after upgrade. Preserve a compatibility Memory overload on the public hook surface, or generate a deterministic adapter-side id before tightening this signature.

Bojan131 added a commit that referenced this pull request Apr 29, 2026
Brings PR #229's CI-validated production fixes to a clean branch off
main. Each failing test category on main's CI is addressed:

  - autoupdater hardening (3 tests): always run hardhat clean+rebuild
    when contract sources change so deleted/renamed contracts'
    artifacts don't survive into the inactive slot
  - CLI-1 scrypt KDF parameter floor (5 tests)
  - CLI-7/9/16 SPARQL endpoint 4xx, /api/verify error mapping, path
    traversal rejection in context-graph IDs
  - CLI-10/11 signed-request auth, nonce store, freshness window,
    token rotation/revocation
  - SKILL.md size cap
  - Q-1 DKGQueryEngine minTrust within verified-memory sub-graph
  - ST-2 PrivateContentStore at-rest confidentiality (AES-GCM-SIV)
  - ST-12 Oxigraph typed-literal round-trip
  - K-4 deterministic seeded RNG sim engine
  - K-5 libp2p parity harness
  - P-2 fencing token (stale worker after wallet lock reset)
  - A-5 per-CG requiredSignatures gates publish
  - A-7 buildEndorsementQuads emits signature + nonce
  - A-12 DID format scan (no peer-id form, accepts ETH-address)
  - A-13 workspace config loader
  - A-15 DKGAgent.share wraps in signed GossipEnvelope
  - e2e-flows SPARQL guard
  - e2e-bulletproof SYNC + INVITE contracts (5 tests)
  - e2e-privacy late-join sync

Test files modified to match the corrected production behaviour;
new test files added for additional coverage (per-CG quorum state,
WAL recovery, async-lift bound, transient classifier, etc.).

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants