feat: s2-stream-config header for auto-created streams - #718
Open
infiniteregrets wants to merge 27 commits into
Open
feat: s2-stream-config header for auto-created streams#718infiniteregrets wants to merge 27 commits into
s2-stream-config header for auto-created streams#718infiniteregrets wants to merge 27 commits into
Conversation
When a basin has `create_stream_on_append` enabled, an append can now carry a
`create_stream_config` that is layered over the basin's default stream config
if that append creates the stream. It is ignored once the stream exists, so
clients can send it on every append without tracking creation state.
- specs: bump submodule (s2-specs#21) for the proto/OpenAPI additions.
- api: JSON `AppendInput.create_stream_config`, proto <-> config
conversions, `AppendRequest` carries `AppendMessage`.
- common: `AppendMessage { input, create_stream_config }`.
- lite: thread config through auto-create; sessions peek the first frame
only if the stream is missing and auto-create is enabled.
- sdk: `with_create_stream_config` on `AppendInput`, `AppendInputs`
and `ProducerConfig`.
Co-authored-by: Cursor <cursoragent@cursor.com>
infiniteregrets
marked this pull request as draft
September 3, 2026 22:46
Contributor
Greptile SummaryThe PR adds an
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant API
participant Lite as S2 Lite
participant Metadata
Client->>API: Append/read + s2-stream-config
API->>API: Parse and validate JSON header
API->>Lite: Request + OptionalStreamConfig
Lite->>Metadata: Look up stream
alt Stream is missing and auto-create is enabled
Lite->>Metadata: Create using basin defaults layered with supplied fields
else Stream exists
Lite->>Metadata: Compare supplied fields with existing config
end
Lite-->>Client: Append/read response
Reviews (3): Last reviewed commit: "cli tests: bound read, assert mismatch r..." | Re-trigger Greptile |
Co-authored-by: Cursor <cursoragent@cursor.com>
…cking the response Clients wait for the response headers before sending the first S2S frame, so peeking the first frame before responding deadlocked until the request timeout, after which the stream was created with basin defaults. Check existence (and whether auto-create is allowed) before responding, and only create the stream inside the response stream once the first message and its create_stream_config are available. An empty or undecodable first frame no longer creates a stream. Co-authored-by: Cursor <cursoragent@cursor.com>
Takes JSON in the same shape as a stream config in an s2 apply spec and passes it through the producer so an auto-created stream picks it up. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…onfig
Replace the should_auto_create closure with AutoCreateOn::{Append, Read}, and
carry create_stream_config as a plain OptionalStreamConfig (an empty one means
inherit all basin defaults), so call sites read AutoCreateOn::Append /
OptionalStreamConfig::default() instead of closures and bare Nones.
Co-authored-by: Cursor <cursoragent@cursor.com>
Switch from an `AppendInput.create_stream_config` body/proto field to a request header holding a compact JSON `StreamConfig`. - api: `CreateStreamConfigHeader` (ParseableHeader, validated like CreateStream), `S2CreateStreamConfigHeader` OpenAPI param; the header is parsed once per request and carried on both `AppendRequest` variants. The proto/body field and `AppendMessage` wrapper are gone. - lite: the config is known before the response, so the deferred session creation machinery is removed; `open_for_append` serves unary appends and sessions alike. - sdk: `S2Stream::with_create_stream_config` sets the header on appends, append sessions and producers (per-stream, like the encryption key), replacing the per-input/producer builders. - cli: `--create-stream-config` now goes through the stream handle. - specs: proto reverted; openapi gains the header param. Co-authored-by: Cursor <cursoragent@cursor.com>
create_stream_config on append for auto-created streamss2-create-stream-config header for auto-created streams
Matches the noun style of the other s2-* headers (s2-format, s2-basin, s2-encryption-key) and stays accurate if reads honor it later. The create-only semantics are spelled out in the header description. Co-authored-by: Cursor <cursoragent@cursor.com>
s2-create-stream-config header for auto-created streamss2-stream-config header for auto-created streams
…wording - SDK `S2Stream::with_stream_config`, CLI `--stream-config`, and all internal fields/params now match the `s2-stream-config` header. - Drop tests that duplicated lower layers or were no longer about this feature (backend session test, proto-existing-stream handler test, S2S responds-before-first-frame test, CLI inherit block and second invalid-JSON case). - One phrasing everywhere: "Stream configuration to apply if the stream is created on append. Unset fields inherit the basin's default stream configuration. Ignored if the stream already exists." Co-authored-by: Cursor <cursoragent@cursor.com>
…g-on-append Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # sdk/src/api.rs # sdk/src/ops.rs # sdk/src/producer.rs # sdk/src/session/append.rs # sdk/tests/stream_ops.rs
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
infiniteregrets
marked this pull request as ready for review
September 7, 2026 19:56
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…smatch() Co-authored-by: Cursor <cursoragent@cursor.com>
…ionPolicy Co-authored-by: Cursor <cursoragent@cursor.com>
…reate race, fix stale docs Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Member
Author
|
@greptile-apps review |
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Summary
When a basin has
create_stream_on_appendenabled, an append can carry ans2-stream-configheader whose value is a compact JSONStreamConfig. If that request is the one that creates the stream, the config is layered over the basin'sdefault_stream_config; unset fields inherit the defaults. It is ignored once the stream exists, so clients can attach it to every append without tracking whether the stream has been created.This gives per-stream config (e.g. retention, delete-on-empty) on auto-created streams without a control-plane round trip.
Spec half: s2-streamstore/s2-specs#21 (this PR bumps the
api/specssubmodule to that branch's commit; re-bump to the merge commit once it lands).API
One header, same for JSON, proto and S2S (an append session is a single request, so the header covers the whole session):
The value is validated exactly like a
CreateStreamconfig; an invalid value is rejected with400 bad_headerbefore any lookup and no stream is created:{"code":"bad_header","message":"Invalid header `s2-stream-config`: age must be greater than 0 seconds"}SDK: the option lives on the stream handle, like the encryption key, and applies to unary appends, append sessions and producers:
CLI:
Why a header
AppendInput(which flows into storage) stays untouched.create_stream_on_read) later, sinceGEThas no body.Changes
v1::config::STREAM_CONFIG_HEADER(s2-stream-config) andStreamConfigHeader, aParseableHeaderthat deserializes the JSONStreamConfigand reusesTryFrom<StreamConfig> for OptionalStreamConfigso validation lives in one place.to_header_valuefor clients.data::S2StreamConfigHeaderdocuments the header in OpenAPI (string schema, with an example value; utoipa cannot expresscontenton a parameter).AppendRequest::Unary/S2sgainstream_config: OptionalStreamConfig, parsed once in the extractor.stream_handle_with_auto_createtakes anAutoCreateOn(Append/Read) and theOptionalStreamConfigto layer over the basin defaults when creating.Backend::open_for_append(.., stream_config)serves both unary appends and sessions; the stream is created (or the request fails) before the response, as before this feature.S2Stream::with_stream_config, mirroringwith_encryption_key. Internally,AppendHeaders { encryption, stream_config }is threaded through sessions/producers and set on every (re)connect.s2 appendaccepts the same stream config flags ascreate-stream(--retention-policy,--storage-class,--timestamping-*,--delete-on-empty-min-age), listed under their own help heading; set on the stream handle.Compatibility
s2-apipublic API change:AppendRequestvariants gain a field.Testing
s2-apiunit: header parse/validate (valid,{}, invalid JSON,age: 0) andto_header_valueroundtrip.400 bad_headerwith no stream created (both invalid config and non-JSON); S2S session with header.s2 lite: unary + producer create with config, existing stream unchanged.s2 lite: 47/47 pass.clippy -D warningsclean; workspace unit suites pass.Made with Cursor