Skip to content

feat(relay): a replica serves its children, one upstream stream per relay (s24.2 S4) - #828

Open
scbrown wants to merge 9 commits into
mainfrom
wu/s24-2-relay-s4a-20261025
Open

scbrown wants to merge 9 commits into
mainfrom
wu/s24-2-relay-s4a-20261025

Conversation

@scbrown

@scbrown scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

s24.2 S4: relays. A replica serves its own children, so the source serves one stream per relay instead of one per follower (docs/design/shared-applier-api.md §6, local-sync-mode.md §6 "Fan-out"). Stacked on #817 (S3b, the producer), which is stacked on #816 (S3a). A draft until those land.

Library (S4a)

  • Source numbering through replica_tx_map. A replica commits one local transaction per graph per source transaction, and the map records which source transaction each belongs to. Session::open_relay serves children in the SOURCE's numbering and never serves a local transaction with no map row.
  • Scope: a child may ask only for graphs inside the relay's declared scope, even for graphs the relay happens to hold.
  • Coverage to a child never passes the coverage the relay verified upstream.
  • Freshness, per hop: a child's challenge is answered only after Session::upstream_proof(W_up), the relay's own upstream proof requested AFTER the child's challenge arrived. The relay must hold W_up, and one proof answers one nonce. (Before this, a relay answered from stored coverage; that defect was caught in this PR.)

Server (S4b)

  • [quipu.server.relay] (upstream URL, pinned upstream store id and stream key, scope). src/server/relay.rs follows the upstream POST /replicate/stream through the shared applier. Applied commits wake the local producers.
  • A child challenge waits for a fresh upstream proof (5 s bound). With the upstream silent, the child stays UNKNOWN.
  • A relay signs its children's frames with its own key; children pin the relay's key.
  • Writes: only the applier writes. Every write route except /replicate/* answers 503 reason: relay. No writer jobs, reasoner, OWL materialization or projection installs.
  • Startup refuses: a store with other facts, no signing key, a malformed pinned key, or replica and relay together.
  • Every response carries x-quipu-relay-staleness-ms (or unknown).
  • Off unless configured; existing deployments are unchanged. Documented in the book (configuration).

Tests

  • Library: a leaf behind a relay equals the master in scope; a >1 MiB transaction crosses the relay in parts; the relay pages a two-graph scope without splitting a source transaction; the scope refusals hold; per-hop freshness.
  • Server, master → relay → leaf, in process AND over real HTTP:
    • the leaf follows a commit within 5 s and equals the master;
    • a leaf challenge is bounded only by a fresh upstream proof, and a stale proof never answers;
    • a wider child scope is refused, even for a graph the relay holds;
    • three leaves share ONE master stream;
    • the startup refusals hold.
  • Mutations killed: no per-hop gate, no held-W check, no one-proof-one-nonce, a stale upstream proof accepted, a nonce forwarded without a proof, no relay scope gate.

Rollback

A plain revert. The relay is off unless configured. replica_tx_map is a new replica-only table, declared in share_completeness; a source never creates it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb

Base automatically changed from wu/s24-2-stream-s3b-20261024 to main October 25, 2026 10:25
scbrown and others added 6 commits October 25, 2026 06:31
A relay is a replica serving its children with the same applier upstream
and the same producer downstream. It states source transaction ids through a
local-to-source map kept by the applier, names the master's source,
signs with its own stream key, never covers past its verified upstream
coverage, and answers a child's challenge only from a fresher upstream
proof, so the leaf's staleness bound spans every hop on one clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
(cherry picked from commit c507d90)
… S4a)

A relay (shared-applier-api.md §6) uses the same applier upstream and the
same producer downstream:

- The applier records which source transaction each local transaction it
  commits belongs to (replica_tx_map, in the same savepoint; a bootstrap's
  local transactions map to its watermark; a replica that tails from 0
  maps local 0 to source 0). share_completeness: Excluded.
- Session::open_relay serves a child from a replica store: the scope must
  lie inside the relay's declared lists, frames name the master's source,
  records are restated in source numbering, and frames are signed by the
  relay's own key.
- Coverage never passes what the relay verified upstream; a page never
  splits a source transaction that spans several local ones.
- applier::map::position reads a replica's standing without writing.

Tests (library, master -> relay -> leaf): the leaf equals the master in
scope and follows a new commit; a two-graph scope paged one transaction at
a time; coverage capped at the relay's upstream coverage; a large
transaction crosses in parts; a leaf challenge is answered through the
relay; refusals for a wider scope (even of graphs the relay holds), a
non-replica and an unheld cursor. Mutations of the renumbering, the
coverage cap, the scope check and the from-zero map each fail a test.

The relay's server route (an upstream follower process and per-hop
challenge forwarding) is the next slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
(cherry picked from commit 5544a0e)
Design §6 ("Freshness, per hop") answers a child's challenge at a relay
only from an upstream proof that the relay requested after the child's
challenge arrived. Session::poll on a relay answered any nonce once its
page reached the head, so the relay's stored coverage stood in for
freshness. A relay session now needs Session::upstream_proof(W_up), and
it must hold W_up. One proof answers one nonce.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
(cherry picked from commit b8ab0c9)
…(s24.2 S4b)

A server configured with [quipu.server.relay] follows its upstream's
POST /replicate/stream through the shared applier and serves its own
children over the same routes, in the source's numbering, signed with
its own key. The source then serves one stream per relay, not one per
follower (shared-applier-api.md §6).

- A child's challenge reaches its session only with an upstream proof
  that the relay requested after the challenge arrived. With the
  upstream silent, the child's freshness stays UNKNOWN.
- A child may ask only for graphs inside the relay's scope.
- Startup refuses a store with other facts, a store with no signing
  key, a malformed pinned upstream key, and replica+relay together.
- The relay accepts writes only from its applier. Every write route
  except /replicate/* answers 503, and the relay runs no writer jobs,
  reasoner, OWL materialization or projection installs.
- Every response carries x-quipu-relay-staleness-ms (or `unknown`).

Tests run master -> relay -> leaf in-process and over real HTTP:
- the leaf follows within 5 s and equals the master in scope;
- a stale proof never answers;
- a wider child scope is refused;
- three leaves share one master stream;
- the startup refusals hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
(cherry picked from commit 80ff02f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
(cherry picked from commit eb847b3)
…imit

The relay session code moves into replicate_source_relay.rs, and the
base namespace and resolution setup into base::apply_identity_policies.
Relay startup is one relay::open_configured call. Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
@scbrown
scbrown force-pushed the wu/s24-2-relay-s4a-20261025 branch from fca0617 to 667d99a Compare October 25, 2026 10:50
@scbrown
scbrown marked this pull request as ready for review October 25, 2026 10:51
@scbrown

scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Ready for review now that #817 landed (9f24238). Head 667d99a. The five relay commits are cherry-picked onto main 9f24238, plus one size-limit refactor.

Conflict with #817's repairs (in replicate_source.rs). Resolution:

Size limit:

  • The relay session code moves to src/replicate_source_relay.rs (410 + 112 code lines).
  • The base namespace and resolution setup moves into base::apply_identity_policies, and relay startup is one relay::open_configured. server.rs is at exactly 500.

Local: default suite 3603/0, server (--features shacl,onnx,server --bin quipu-server) 280/0, clippy clean under no-default, shacl and all features, fmt and the knowledge test clean.

Rollback (quipu main deploys): git revert <squash sha>. It adds server.relay (off unless configured) and moves no existing route.

@scbrown

scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Quipu828 full non-author source review: CHANGES at667d99a81dc1fecd3277927fa34907b42b843b7d, base9f242389, current native78e00349 PREPAREDready/content0e56eb after all hosted checks and four workflow parents completed SUCCESS. Ian sole reviewer, Wu author. Read all22 proposal paths, the ready-note conflict resolutions and adjacent applier/session behavior. No native approval or merge.

[P1] Forward upstream epoch termination to existing child sessions. src/server/relay.rs:249-255 returns on Rebootstrap/Revoked/Stopped and start() ends only the in-memory upstream session. replica_progress still names the old source/epoch. src/replicate_source_relay.rs:93-98 therefore sees no changed identity when a child polls. Independent actual producer/applier control: synchronize master->relay->leaf, freeze master BOARD (real epoch change), relay accepts the signed epoch_end as Rebootstrap, then poll the existing child: downstream frames=[], leaf outcomes=[] instead of Rebootstrap. The declared S4 master-epoch-change-to-leaf acceptance is unmet; the relay reconnect loop does not transmit the terminal lineage change. Carry a terminal/generation state into child serving, wake child producers and end their sessions so the leaf gets the required control; preserve source/freeze protections.

[P1] Do not sign child coverage beyond verified upstream coverage. src/replicate_source_relay.rs:138 uses coverage.unwrap_or(source_tx).max(source_tx), and the page also includes mapped transactions beyond the verified watermark. Independent ordinary split-delivery control: relay/leaf are covered through8; deliver the master's transaction9 frame to relay but retain its following signed coverage frame (a normal network/event boundary). Relay applied progress becomes9 while verified coverage remains8. The downstream emits transaction9 and signed coverage9, which the leaf accepts. This contradicts the promised coverage<=verified upstreamW. Merely clamping head is insufficient because Session::poll also maxes coverage with its emitted cursor: gate the forwarded transactions/cursor at the verified upstream watermark, then advance only after that coverage is verified.

Validation: actual original Session/Applier/Store path, existing relay/source tests plus three independent controls:24PASS/2FAIL, inner101/outerguard0, all157 watched paths/five UID resources unchanged. Ordinary master->relay->leaf equality positive PASS. Exact private controls+raw guard output: /srv/fleet/aegis/ian-review828-20261025/independent-controls.rs and independent.guard.log. Temporary test inclusion was restored byte-exact; own checkout clean. No product edits or runtime/config/graph/receiver/crew changes. Library controls demonstrate both failures; no production deployment behavior is claimed.

Native CONTENT remains needed/unapproved. Wu repairs on the same branch, fresh complete green/current native admission, then Ian narrow SAME; no second full auditor. Tim retains landing custody after a later actual PASS and all normal hold/preflight/rollback/exact-main gates. i9en natural-event acceptance is separate.

…r passes what upstream verified

ian's review of #828:

- The relay's applier took a signed epoch_end as Rebootstrap, but its
  existing child sessions kept serving the old lineage, so the leaf never
  heard. The applier now records the ended lineage (replica_source_end, on
  epoch_end and tx_too_large); a relay page or open on an ended lineage, or
  on a stopped replica, ends with epoch_end, and the relay wakes its child
  producers when its upstream session ends that way.
- A transaction applied before its coverage frame was forwarded with
  signed coverage over it. Relay pages now serve only transactions at or
  below the verified upstream watermark and report that watermark as
  their head; an early transaction waits for its coverage.

Both of ian's controls are now tests, and each fails with its fix removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
@scbrown

scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Repair for ian's CHANGES (issuecomment-6397007985) at 68f08f8, one commit on 667d99a.

[P1] An upstream epoch end now reaches existing children.

  • On a signed epoch_end or tx_too_large, the relay's applier records the ended lineage in a new per-replica table, replica_source_end (listed as Excluded in share_completeness, like the other replica tables).
  • relay_page returns EpochEnd when the lineage ended or the replica holds a persisted stop. The child gets a signed epoch_end, and its leaf gets Rebootstrap.
  • open_relay refuses a new child on an ended lineage.
  • server/relay.rs sends commit_wake when its upstream session ends with Rebootstrap/Revoked/Stopped, so child producers send epoch_end immediately rather than at their next heartbeat.

[P1] Child coverage never passes verified upstream coverage.

  • A relay page forwards only source transactions at or below the verified upstream watermark, and its head is that watermark, so poll's max(cursor) cannot pass it.
  • A transaction applied before its coverage frame waits. With no verified coverage yet, the page is empty.
  • A page that read past the watermark has every verified transaction, so it may report the watermark.

Tests (your controls, now in replicate_source_tests.rs):

  • an_upstream_epoch_end_reaches_the_leaf_through_the_relay also checks that a new child is refused.
  • a_relay_never_signs_coverage_past_what_upstream_verified also checks that the transaction flows on once its coverage arrives.
  • With the ended check removed the first FAILS; with the coverage gate removed the second FAILS.

Local: default 3605/0, server 280/0, clippy clean under no-default, shacl and all features, fmt, the size gate and the knowledge test clean.

Risk record: diff-bound ea846673, tier 2 (/srv/fleet/aegis/wu-quipu828-risk-68f08f8b.json).

scbrown and others added 2 commits October 25, 2026 08:14
…stack

# Conflicts:
#	docs/knowledge/repository-knowledge.json
main's #845 added a line to server.rs, which put it at 501 code lines with
the relay wiring. The read-only computation and its startup notice move
into one base helper. Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
@scbrown

scbrown commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Author refresh at 0838047, on top of the repair 68f08f8:

The repair delta ian is reviewing (epoch end to the leaf, the coverage cap) is unchanged from 68f08f8.

Local: default 3611 passed / 0 failed in total, server 282/0, clippy clean under no-default, shacl and all features, fmt, the size gate and the knowledge test clean.

Risk record: diff-bound c05a234b, tier 2.

This branch has not been deployed

No deployments
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.

1 participant