RFC-026 §4.7 P1: streaming enablement authority (internal schema v10) - #389
Merged
Conversation
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.
This was referenced Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
stream_profilesingleton (present from genesis, streaming disabled,profile_revision1) plus the reserved fold-attributiondead_letter_objectslot. 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 isdeny_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-graphAdminaction, first consumer), full write prelude (heal barrier → stream-admission capture → schema/branch/stream-token gates → sidecar re-check), oneSetStreamProfileexact-entry CAS with strict revision advance, one ordinary lineage commit (auditable incommit 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.graphs.<id>.streaming: true|falseincluster.yaml;cluster applyis the only flip mechanism (first-classstreaming.<id>resource — deliberately not folded into the graph composite digest, whoseDerivedclassification would fake-converge the ledger without an engine call). Absent = unmanaged; removal never disables; disable converges pending-until-drained (streaming_drain_pending, threaded likepending_recovery).cluster refreshreads the live value back from the graph, so crashed applies and out-of-band flips surface as ordinary plan drift toward the declaration.OMNIGRAPH_V9_BINseam +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 exact0.9.xrefusal 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
lifecycle.rspublic-surface cell, policy-chassis deny/allow pair proving the(Admin, Graph)scope,forbidden_apis19/19 (new writer + durable-call registered; publisher chokepoints unchanged at 1).cargo test --workspace --locked75/75 suites green; failpoints 141/141;memwal_streamgreen.Pre-existing failures on main (not this PR)
thread 'manifest_reads_capture_warm_probe' has overflowed its stack— on commits predating this branch. The same environment-sensitive overflow reproduces locally inmemwal_stream_cost::b2a_retained_history_small_depth_keeps_terms_and_old_roots_separate.table_reads17 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
FINAL_INTERNAL_V9_COMMITin 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.
stream_profilesingleton and reserved dead-letter attribution slot.Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
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 endReviews (2): Last reviewed commit: "Fix streaming authority convergence" | Re-trigger Greptile
Context used:
omnigraph-cluster)