Skip to content

RFC-026 §4.7 P1: streaming enablement authority (internal schema v10) - #389

Merged
aaltshuler merged 8 commits into
mainfrom
rfc026/exp-p1-enablement
Jul 28, 2026
Merged

RFC-026 §4.7 P1: streaming enablement authority (internal schema v10)#389
aaltshuler merged 8 commits into
mainfrom
rfc026/exp-p1-enablement

Conversation

@aaltshuler

@aaltshuler aaltshuler commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Slice 1 of the experimental streaming profile (RFC-026 §4.7, selected 2026-07-27): the graph-wide enablement flag as durable manifest state, end to end.

What ships

  • Internal schema v10 — the required graph-global stream_profile singleton (present from genesis, streaming disabled, profile_revision 1) plus the reserved fold-attribution dead_letter_object slot. The bump is forced by decode semantics: v9 decoders silently skip unknown row kinds, so only the stamp turns "older binary meets streaming-capable graph" into a refusal instead of a writer blind to the freeze; and the attribution summary is deny_unknown_fields + structurally equality-compared between recovery sidecar and lineage row, so the DLQ slot cannot be retrofitted through a serde default later.
  • Omnigraph::set_streaming_enabled_as — Cedar-gated (reserved per-graph Admin action, first consumer), full write prelude (heal barrier → stream-admission capture → schema/branch/stream-token gates → sidecar re-check), one SetStreamProfile exact-entry CAS with strict revision advance, one ordinary lineage commit (auditable in commit list). No recovery sidecar: zero pre-manifest durable effects (repair-style registered sidecar-free writer). Disable refuses typed (StreamingDisablePending, naming the undrained tables) while any stream lifecycle is non-terminal.
  • Cluster propagationgraphs.<id>.streaming: true|false in cluster.yaml; cluster apply is the only flip mechanism (first-class streaming.<id> resource — deliberately not folded into the graph composite digest, whose Derived classification would fake-converge the ledger without an engine call). Absent = unmanaged; removal never disables; disable converges pending-until-drained (streaming_drain_pending, threaded like pending_recovery). cluster refresh reads the live value back from the graph, so crashed applies and out-of-band flips surface as ordinary plan drift toward the declaration.
  • Genuine v9↔v10 fenceOMNIGRAPH_V9_BIN seam + current_v10_refuses_and_rebuilds_genuine_v9_and_v9_refuses_v10, validated locally against a binary built from the pinned final-v9 commit; CI builds that binary (FINAL_INTERNAL_V9_COMMIT) before the workspace suite with the same 7-package cargo-clean discipline as the v8 step. New in-source migrations assertions pin the exact 0.9.x refusal strings locally so a release-map edit breaks fast, not only in the env-gated cell (the Align crossversion refusal assertions with the 0.9.0-dev naming #387 failure class).

Enabling the flag activates no ingest surface — P2–P6 of the profile remain unimplemented; this slice only makes the graph durably, universally know whether streaming is enabled, with the off-switch semantics built correctly from day one.

Evidence

  • Engine: 348 in-source cells (genesis/CAS/revision/reopen; serde exact incl. explicit-null DLQ slot and absent-key refusal; undrained-refusal with injected OPEN lifecycle), lifecycle.rs public-surface cell, policy-chassis deny/allow pair proving the (Admin, Graph) scope, forbidden_apis 19/19 (new writer + durable-call registered; publisher chokepoints unchanged at 1).
  • Cluster: 122 in-source cells incl. full flip e2e (create+flip → idempotent → explicit disable → unmanage keeps graph state), refresh-convergence e2e, import-over-pre-existing-graph flip, drain-pending classification unit, unmanaged-digest-stability (no approval churn).
  • CLI: both cluster suites green; lifecycle e2e pins import-records-born-disabled → declared-drift-applies → status-echoes-stored-observations-by-design → refresh-reports-live-truth. S3 lifecycle fixture now exercises the flip on RustFS CI.
  • Gates: cargo test --workspace --locked 75/75 suites green; failpoints 141/141; memwal_stream green.

Pre-existing failures on main (not this PR)

  • Main's post-merge Test Workspace is currently red with thread 'manifest_reads_capture_warm_probe' has overflowed its stack — on commits predating this branch. The same environment-sensitive overflow reproduces locally in memwal_stream_cost::b2a_retained_history_small_depth_keeps_terms_and_old_roots_separate.
  • Under an enlarged stack, that cell reveals a warm-ack flatness failure (table_reads 17 vs 11 across retained depth 8 vs 1). Verified byte-identical at the base commit on the same machine (reads 11/17, bytes 38,399/63,993 on both trees) — this slice is cost-neutral on the warm-ack path to the byte. Both issues deserve their own investigation.

Pre-merge checklist

  • Re-pin FINAL_INTERNAL_V9_COMMIT in ci.yml if main advances with any v9-writing commit before this merges (it must remain the last v9 commit; it becomes immutable after merge). A concurrent format-bumping PR would be a real conflict — only one v10 bump can exist.

Greptile Summary

Adds durable graph-wide streaming enablement without activating an ingest surface.

  • Introduces internal manifest schema v10 with a required stream_profile singleton and reserved dead-letter attribution slot.
  • Adds Cedar-authorized, revisioned streaming enable/disable operations with undrained-stream protection.
  • Models streaming enablement as an independently converged cluster resource with refresh and retry behavior.
  • Adds genuine v9↔v10 compatibility fencing, tests, CI coverage, and operator documentation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
crates/omnigraph-cluster/src/diff.rs Adds first-class streaming resource classification and now correctly demotes it when its graph prerequisite fails.
crates/omnigraph-cluster/src/lib.rs Executes streaming convergence through the engine and records only successfully applied changes.
crates/omnigraph/src/db/omnigraph/stream_profile.rs Implements the authorized, revisioned streaming profile transition and undrained-disable refusal.
crates/omnigraph/src/db/manifest/stream_profile.rs Defines durable stream-profile state and exact manifest CAS behavior.
crates/omnigraph/src/db/manifest/migrations.rs Extends strict internal-format fencing and rebuild guidance to schema v10.
.github/workflows/ci.yml Builds a pinned final-v9 binary so CI exercises genuine bidirectional v9/v10 refusal and rebuild behavior.

Sequence Diagram

sequenceDiagram
    participant Config as cluster.yaml
    participant Apply as cluster apply
    participant Engine as Omnigraph
    participant Manifest as __manifest v10
    participant Ledger as cluster state

    Config->>Apply: "graphs.<id>.streaming"
    Apply->>Engine: set_streaming_enabled_as(...)
    Engine->>Manifest: exact-CAS stream_profile revision
    alt Flip succeeds
        Manifest-->>Engine: committed profile
        Engine-->>Apply: streaming status
        Apply->>Ledger: "record streaming.<id> digest"
    else Disable blocked by undrained streams
        Engine-->>Apply: StreamingDisablePending
        Apply->>Ledger: mark streaming_drain_pending
    else Graph prerequisite failed
        Apply->>Apply: demote streaming dependency
        Apply->>Ledger: preserve prior streaming digest
    end
Loading

Reviews (2): Last reviewed commit: "Fix streaming authority convergence" | Re-trigger Greptile

Context used:

Six dated parameter decisions (2026-07-27) for the first public activation of
the streaming lane as an experimental, cluster-only feature: cluster-declared
manifest-propagated enablement, lazy graph-wide enrollment, caller-supplied
vectors, per-key object-form dead letter with fail-closed structural refusal,
no read-your-writes bridge, and a dependency-ordered resident fold driver;
upsert-only confirmed as a lane boundary. Adds the §13 EXP phasing row and the
header stanza. Design selection only — nothing is implemented and no schema or
product surface is activated by this section.
RFC-026 §4.7 P1's enablement authority as durable manifest state: one
required graph-global stream_profile row (genesis-provisioned, streaming
disabled, profile_revision 1), decoded on every open, movable only through
the shared publisher's new SetStreamProfile exact-entry CAS with a strict
revision advance. Snapshot exposes streaming_status() { enabled, undrained }
computed with zero extra I/O.

The stamp bumps to 10 because v9 decoders silently skip unknown row kinds —
only the stamp turns 'older binary meets streaming-capable graph' into a
refusal instead of a writer blind to the freeze — and because the correct
shape is a required genesis singleton absent from every v9 graph. The same
bump reserves the fold-attribution dead_letter_object slot (explicit null,
deserialize_present_option, excluded from the attribution digest preimage):
StreamFoldAttributionSummary is deny_unknown_fields and structurally
equality-compared between the recovery sidecar and the lineage row, so the
slot cannot be retrofitted later through a serde default.

Release map: v10 => 0.10.0-dev until 0.10.0 release-prep flips it; a new
in-source grammar assertion pins the exact v9 refusal strings the gated
v9-to-v10 crossversion fence will assert, so a future map edit breaks fast
and locally instead of only in the env-gated CI cell.
Move every currently-served-format claim from v9 to v10 across the canonical
docs: versioning.md gains the v10 paragraph (what it adds, why the bump is
forced by v9's silent unknown-row skip, both-direction refusal); upgrade.md
gains the v10 table row, the updated refusal sample, and a 'Migrating from
internal schema v9 to v10' section; storage.md's object_type enumeration and
graph-global-row notes gain stream_profile; constants.md records
INTERNAL_MANIFEST_SCHEMA_VERSION = 10 and the stream-profile protocol
constant; invariants.md and canon.md record the v10 strand entry; AGENTS.md's
storage bullet and capability matrix now serve v10.
…t flip

The sole intended production caller is cluster apply (§4.7 P1: cluster.yaml
declares, apply propagates, the manifest row is what every process obeys).
The writer enforces the reserved per-graph Admin action, takes the full write
prelude (heal barrier → stream-admission capture → schema/branch/stream-token
gates → sidecar re-check), captures one fresh main snapshot under the gates,
and publishes one SetStreamProfile exact-entry CAS with an ordinary lineage
commit — auditable in commit list. No recovery sidecar: zero pre-manifest
durable effects (repair-style registered sidecar-free writer). The publish
precondition is Any by design: the profile row CAS is the OCC, and an
unrelated content commit moving graph_head must not spuriously fail a flip.

Disable refuses typed and effect-free (StreamingDisablePending, naming the
undrained tables) while any stream lifecycle is non-terminal — structurally
satisfied in this slice, tightened by later slices without changing shape.

Evidence: lifecycle.rs public-surface cell (fresh-off, flip/idempotence/
versioning/reopen durability), policy chassis deny/allow pair proving the
(Admin, Graph) scope through the policy grammar, in-source undrained-refusal
cell with an injected OPEN lifecycle, forbidden_apis registration
(write_surfaces + durable_calls; publisher chokepoints unchanged at 1).
…utor, observation

cluster.yaml gains per-graph `streaming: true|false` (absent = UNMANAGED —
removal never disables; skip_serializing_if keeps pre-streaming configs'
desired_config_digest byte-identical so pending approvals do not churn). The
declaration emits a first-class `streaming.<id>` resource — deliberately NOT
folded into graph.<id>'s composite digest, whose Derived classification would
fake-converge the ledger without the required engine call.

classify_changes gains the threaded streaming_drain_pending set (the
pending_recovery shape): a drain-pending graph blocks with
"streaming_drain_pending" — structurally empty this slice, populated from
observation by later slices. The apply executor runs after graph create and
schema apply: it opens the graph and calls set_streaming_enabled_as; a typed
StreamingDisablePending demotes the change to the pending disposition without
failing the run. No cluster-side sidecar — the boolean is re-derivable from
observation, so refresh reads streaming_status() from the live graph, regains
the ledger row from ENGINE truth, and surfaces out-of-band drift as an
ordinary plan Update back toward the declaration.

Evidence: config accept/reject + unmanaged-digest-stability cell; classify
unit pinning the drain-pending block, non-Derived application, and
Delete-as-unmanage; full e2e (create+flip → idempotent → explicit disable →
unmanage keeps graph state); refresh convergence e2e (wiped row regained from
engine truth; out-of-band flip → observed drift → re-apply converges). Full
cluster suite green (121 in-source cells).
The CLI cluster fixture declares streaming: true, so every apply-create e2e
exercises the real flip; the fabricated-ledger helper strips the key (the
flag is the one non-create per-graph resource whose apply opens the real
graph, so a declared flag over a fabricated ledger correctly fails). The
lifecycle e2e pins the full contract: import records the born-disabled
observation, the declared flag surfaces as applied drift, apply flips the
graph, and — because status echoes STORED observations by design (apply owns
statuses; refresh/import own observations) — the live streaming_enabled /
streaming_matches_desired assertions sit after refresh. A new in-lib cluster
cell pins the same import→apply flip over a pre-existing graph. The S3
lifecycle fixture also declares the flag, so RustFS CI exercises the flip.

crossversion_upgrade.rs gains the OMNIGRAPH_V9_BIN seam and
current_v10_refuses_and_rebuilds_genuine_v9_and_v9_refuses_v10 — validated
locally against a binary built from the pinned final-v9 commit: exact 0.9.x
refusal strings both message slots, export/init/load rebuild fidelity,
exact-id PK metadata, and reverse refusal. ci.yml builds that binary
(FINAL_INTERNAL_V9_COMMIT e889a1c…, immutable) before the workspace suite
with the same 7-package cargo-clean discipline as the v8 step.

The server error taxonomy gains the StreamingDisablePending → conflict arm
(unreachable over HTTP in this slice; the mapping keeps the taxonomy total).
testing.md documents the new seam; clusters/config.md documents the
experimental streaming key (absent = unmanaged; removal never disables;
disable is pending-until-drained).
The pre-implementation format audit resolved to its pre-registered default
(internal schema v10): v9 decoders silently skip unknown row kinds, and the
correct shape is a required genesis singleton. The header stanza, §4.7
preamble, and §13 EXP row now record P1 as implemented 2026-07-28 — required
genesis stream_profile row + reserved dead-letter slot, Cedar-gated
single-CAS flip, cluster-apply-only propagation with refresh convergence and
typed pending-until-drained refusal, genuine v9↔v10 fence in CI — with P2–P6
explicitly still unimplemented and no ingest surface active.
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