Skip to content

feat(storage): add default-unused atomic system-record apply - #2124

Open
Jurij89 wants to merge 2 commits into
feat/2052-system-record-materializerfrom
feat/2052-system-record-atomic-apply
Open

feat(storage): add default-unused atomic system-record apply#2124
Jurij89 wants to merge 2 commits into
feat/2052-system-record-materializerfrom
feat/2052-system-record-atomic-apply

Conversation

@Jurij89

@Jurij89 Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add the default-unused atomic system-record apply boundary planned for issue Reconnect-triggered background sync amplification can starve foreground catch-up #2052: exact RDF/state inspection, immutable next-state derivation, one bounded conditional SPARQL update, and exact post-read/recovery classification. Failed or non-2xx writes cannot settle from an immediate prior-state read; they transfer ownership to physical recovery.
  • Bind every internal apply to the private verified-replacement capability, the 12 MiB request lease, activation/session generation, owned Oxigraph child generation, and materialization epoch. Opaque raw updates are rejected only while system-record admission is active; managed writes queued before activation are revalidated at dispatch, while unowned and ordinary default-off paths remain unchanged.
  • Extend managed HTTP and Oxigraph lifecycle ownership so indeterminate writes retain their accounting and settle against the original child before any replacement is admitted. Production still discards the proof issuer, so this PR does not activate system-record synchronization or change live sync behavior by itself.

Related

Diagrams

Verified atomic apply boundary

Before:

sequenceDiagram
    participant Verifier
    participant Registry
    participant Lane
    participant Scheduler
    participant Oxigraph
    Verifier->>Registry: Verified replacement
    Registry-->>Verifier: No atomic apply route
    Verifier-->>Lane: Not callable
    Lane-->>Scheduler: No system-record transaction
    Scheduler-->>Oxigraph: No system-record write
Loading

After:

sequenceDiagram
    participant Verifier
    participant Registry
    participant Lane
    participant Scheduler
    participant Oxigraph
    Verifier->>Registry: consumeCandidate(signed closure, bundle, slice)
    Registry->>Lane: One-shot proof and 12 MiB lease
    Lane->>Scheduler: Generation-bound exclusive agents admission
    Scheduler->>Oxigraph: Bounded exact inspection
    Oxigraph-->>Scheduler: State, claims, capacity, projection
    Scheduler->>Oxigraph: One conditional SPARQL update
    Oxigraph-->>Scheduler: Response or uncertain transport outcome
    Scheduler->>Oxigraph: Exact post-read
    Scheduler-->>Lane: Applied, deferred, or indeterminate
    Lane-->>Verifier: Typed terminal outcome
Loading

Default production path

Before:

sequenceDiagram
    participant Caller
    participant Registry
    participant Store
    participant Oxigraph
    Caller->>Store: Ordinary query or mutation
    Store->>Oxigraph: Legacy dispatch
    Oxigraph-->>Caller: Result
    Registry-->>Caller: No production proof issuer
Loading

After:

sequenceDiagram
    participant Caller
    participant Registry
    participant Store
    participant Oxigraph
    Caller->>Store: Ordinary query or mutation
    Store->>Oxigraph: Same legacy dispatch while admission is disabled
    Oxigraph-->>Caller: Result
    Registry-->>Caller: No production proof issuer
Loading

Files changed

File What
packages/core/src/system-record-*.ts, packages/core/src/ka-bundle-v1.ts Add canonical apply limits, empty-projection invariant, minted authority proof assertion, and bounded byte ownership used by the storage boundary.
packages/storage/src/system-record-*-v1-internal.ts Add private verified-replacement registry, exact RDF/state decoders, next-state model, command encoder, materialization epoch binding, and atomic apply executor.
packages/storage/src/system-record-materializer-v1.ts Extend the passive lane with generation-bound inspection/apply/recovery behavior while preserving the legacy B2 handoff contract.
packages/storage/src/adapters/managed-http-client.ts, packages/storage/src/adapters/sparql-http.ts Bound request/response ownership, account inspection buffers, guard active raw updates, and bind dispatch to the managed child.
packages/storage/src/managed-oxigraph-ownership-v1-internal.ts, packages/cli/src/daemon/oxigraph-server.ts Bind capabilities to exact endpoints/generations and preserve fail-closed physical settlement across recovery, disable, and shutdown.
packages/{core,storage,cli}/test/** Cover canonical state, exact projection parity, CAS/recovery outcomes, lifecycle races, aborts, mutation admission, legacy compatibility, and default-unused behavior.
devnet/issue-2052-managed-ownership/run.ts, .github/workflows/system-record-managed-ownership.yml Extend lifecycle evidence and the focused conformance suite; the live gate deliberately does not claim an atomic apply benchmark.
docs/adr/0002-system-record-sync-v1.md Record current default-unused scope, merge boundaries, and the remaining activation blockers.

Activation gates

This PR is mergeable only as default-unused infrastructure. It is not evidence that the system-record lane can be activated. Before any production proof issuer/opener is wired, follow-up work must prove all of the following:

  • Same-version verified forks and root collisions atomically quarantine the incumbent with exact post-read/recovery semantics instead of returning a zero-write terminal result.
  • A prior durable materialization epoch can be recovered and replaced; it must not remain a permanent generation mismatch.
  • The 64 MiB accountant becomes one process/runtime-owned aggregate. The registry-local implementation here permits one 12 MiB lease but is not the final aggregate owner.
  • Maximum legal records fit the 12 MiB end-to-end lease, 8 MiB prepared-state cap, and 4 MiB request bound, or the protocol limits are reduced. The current conservative 10,000-row response estimate is about 31.4 MiB, and the three-copy prepared-body accounting makes the effective request limit about 2.67 MiB.
  • Live Oxigraph evidence covers caller abort, lost response, maximum-size p99/RSS, and zero deadline-induced recovery on healthy applies.
  • feat(storage): add the live managed materialization boundary (#2052 Stack B2) #2110 is rebased onto current feat(core): add bounded system-record V1 contracts #2103 and this branch is rebased onto the resulting head without losing the B1 hardening on which the verified capability depends.

Until those gates pass, no production issuer/opener may be added and legacy synchronization remains authoritative.

Test plan

  • pnpm --filter @origintrail-official/dkg-core build
  • pnpm --filter @origintrail-official/dkg-core test:system-record - 60 passed, 2 skipped exhaustive cases.
  • pnpm --filter @origintrail-official/dkg-storage build
  • Storage CI conformance set from .github/workflows/system-record-managed-ownership.yml - 310 passed.
  • vitest for managed HTTP, materialization epoch adapter, and SPARQL HTTP/blank-node compatibility - 71 passed.
  • pnpm --filter @origintrail-official/dkg build
  • vitest for oxigraph-managed and oxigraph-server - 63 passed.
  • pnpm test:live:system-record-managed-ownership - PASS, 27 checks across 3 predecessor entries.
  • git diff --check
  • Independent adversarial reviews across transaction/recovery, resource bounds, compatibility/stack topology, and default-unused behavior.

The live workflow proves owned-child lifecycle and predecessor compatibility only. It does not dispatch a verified atomic apply and must not be cited as the missing maximum-size/lost-response activation evidence.

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@Jurij89

Jurij89 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@otReviewAgent please retry the review on head 19563e556. The previous attempt failed before code analysis with a temporary-directory permission error. The branch is unchanged; two independent post-transplant reviews found no blocker/high findings.

@otReviewAgent otReviewAgent left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Operational Notice: Review Agent could not complete this review.

Business logic reviewer failed: WARNING: failed to clean up stale arg0 temp dirs: Permission denied (os error 13)

@Jurij89

Jurij89 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Final adversarial review: converged

I ran independent transaction/recovery, concurrency/liveness/resource, and architecture/stack/evidence passes against head 19563e556, then repeated the relevant lenses against the fixes now pushed as 8eb7e9ef1.

Findings resolved

  1. High: queued pre-activation write escaped active admission. A managed mutation could queue untagged under saturation, the control barrier could enable the lane, and the stale write could dispatch afterward. The adapter now revalidates ownership-backed unbound writes synchronously at dispatch. Saturated-queue regressions cover both an agents-scoped insert and opaque update with zero I/O.
  2. Blocker: failed write plus immediate prior-state read settled too early. Timeout, transport failure, or non-2xx could be followed by a prior-state read before a late backend commit. Exact prior is now terminal only after a successful 2xx; otherwise ownership transfers to physical recovery. Both failure cases execute reconciliation through not-applied in a replacement generation.
  3. High: authoritative cold apply rejected legacy rows it must replace. Absent authoritative state now accepts bounded inspected legacy content only on the authentic verified next-subject union and deletes it in the same CAS transaction. Absent shadow and all present-state paths remain strict. A real Oxigraph test proves legacy deletion plus stale-CAS zero-write behavior.
  4. Low: wrong null sentinel weakened the unowned fast path. The dispatch guard now applies only to lease-bearing stores, with a regression that fails if an unowned structured write evaluates managed admission state.
  5. Evidence hygiene: hosted storage conformance now includes the production epoch-adapter composition test, and default-unused allocation wording distinguishes fixed dormant registries from per-store/runtime work.

Verification

  • Core system-record suite: 60 passed, 2 skipped
  • Exact hosted storage conformance set: 310/310 passed
  • Managed HTTP test isolated: 15/15 passed
  • SPARQL HTTP, blank-node, and epoch-adapter suites serially: 56/56 passed
  • CLI managed Oxigraph suites: 63/63 passed
  • Storage/core/CLI builds: passed
  • git diff --check: passed
  • Final independent post-fix reviews: no blocker, high, medium, or low findings

The PR is converged for its explicitly default-unused merge boundary. The activation gates already listed in the PR remain mandatory before a production issuer/opener is wired.

Jurij89 added a commit that referenced this pull request Aug 7, 2026
…ove we own

Round-3 HIGH, confirmed and reproduced: the scheduler holds ordinary work while a
control barrier is pending and releases it when the barrier settles -- IDENTICALLY
whether the barrier resolved or rejected -- and no mutation path consulted
ownership at dispatch. So a mutation queued before a generation handoff resumed
after that handoff FAILED. Measured: an `INSERT DATA` went on the wire while the
store's own lease already read `terminal: true, port-release-unproven`. Every
fact needed to refuse was available at the dispatch instant and never read.

**This is not the fix the review asked for, and the difference is deliberate.**
The review asked to move B3 (#2124)'s armed mutation latch down into B2. I
measured that latch instead of copying it, and it should not come down:

1. Its safety margin is ZERO microtasks. The scheduler rejects the barrier one
   reaction before it releases queued work, so a latch written synchronously in
   the lane's `catch` wins by exactly one tick. Delaying that write by a single
   `await` -- an awaited log, a metric, a future refactor of `startBarrier` to
   `.finally().then()` -- flips the guard off with every test still green.
   Measured directly: at N=0 extra awaits the queued write sees `latched=true`;
   at N=1 it sees `latched=false`.
2. It is blanket. It arms from one `catch` covering the whole barrier, but of the
   five handoff steps two -- `destroyClient` and `rotateMaterializationEpoch` --
   leave the child proven and still listening. Latching there refuses every
   managed write for the process lifetime on a node where nothing was wrong.
3. It does not implement the review's own requirement (4). There is no clear site
   anywhere: `managedMutationFailure ??= reason` and nothing resets it. The
   clean-generation-start failure is EXPLICITLY recoverable -- the supervisor
   SIGKILLs the unproven child and `scheduleRevive`s -- so a permanent latch
   converts a self-healing respawn into a mandatory node restart. That is worse
   than the harm prevented, in the more likely of the two failure modes.
4. It fixes the RAREST instance of the class. The same exposure exists with no
   lane involved: on an ordinary child exit the supervisor invalidates ownership
   and revives, and for the whole backoff the adapter keeps POSTing to a port it
   does not own. That path ships today. The lane path has NO production caller.

So this is a dispatch-time READ of the live lease, not an armed latch:

- no arming, therefore no ordering to get wrong -- point 1 dissolves;
- keyed on proven liveness rather than "the barrier threw", so a failure that
  left the child healthy refuses nothing -- point 2;
- self-clearing by construction, because there is no state to clear: when the
  supervisor binds a proven replacement the lease reads live again -- point 3,
  and it satisfies requirement (4) more strictly than a latch could;
- it covers the child-exit window too, because that also leaves the lease
  not-ready -- point 4.

Placement is the whole guarantee, and it has its own mutant. The check is INSIDE
`postUpdate`'s work callback -- after the scheduler admits the write -- not at the
top of `insert()`. Every mutation funnels through `postUpdate`, so one site
covers `insert`/`delete`/`deleteByPattern`/`deleteBySubjectPrefix`/`update`/
`dropGraph`/`replaceGraph`/`replaceGraphAndSubject`/`replaceSubject`. Moving it to
call time leaves every other test green and reddens exactly the barrier-queued
case, which is the case the review reported.

Scope, stated rather than assumed:
- MUTATIONS only. Refusing reads would turn every child respawn into a total
  store outage -- the same reasoning that made a failed lane shutdown leave the
  child alive rather than kill the daemon's store.
- Managed stores only. An operator-configured store has no lease, returns on the
  first line, and pays one already-loaded field read.
- `isManagedOxigraphOwnershipLiveV1` is allocation-free on purpose;
  `readManagedOxigraphOwnershipSnapshotV1` freezes a new object, so it is called
  only to build the error, on the cold path.

**One guard was written and then removed.** The predicate started as
`ready && !terminal`; the `!terminal` term's solo mutant SURVIVED. Terminality
implies not-ready in both directions -- `invalidate()` clears `ready` on every
reason, and `bindReadyGeneration()` throws on a terminal lease -- so the second
term could never change an outcome. It reads as extra protection, so it is gone.

Evidence: 7 new tests, all asserting on RECORDED REQUESTS rather than on the
thrown error, because the property is zero I/O and an error raised after a socket
opened would satisfy `rejects.toThrow` while failing the actual requirement. Both
regressions the review names are covered, plus the no-lane child-exit case, the
recovery case, the unmanaged-store case and reads-still-served. Solo-mutant
sweep: 5 of 5 killed, including the placement mutant. 218/218 across the eleven
B2 storage suites; live gate `PASS: 27 checks`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants