Skip to content

004: Streaming

One logical client database owns separate durable positions for receipt, processing, and app delivery. Network subscriptions and unary reads share receipt and processing. App message streams read stored messages. Sync does not consume messages on behalf of the app.

Spec 001 defines wire types and public limits. Spec 002 defines backend storage visibility, tailer recovery, and bounded fetch turns. Spec 003 defines the preserved trust and validation rules. Requirements use STR-nnn. Existing backend wire requirements remain unchanged.

This revision permits a new client database format. An upgrade from an earlier self-hosted client database is not required. It does not add a backend RPC or a general database change feed. The implementation also retires the unused Get RPC under specs 001 and 002; ordered Query replaces its client uses. Durable conversation-discovery replay, replay of later message edits or deletions, and client database identity and restore rules are outside this spec. Conversation callbacks remain live notifications.

These positions belong to the client database. The backend keeps its own per-topic watermarks and allocation boundary under spec 002. No client position is sent to or stored by the backend.

PositionMeaning
F(topic)Received boundary. Every retained envelope after the starting anchor through F is durably pending or already handled.
P(topic)Processed boundary for a group or identity topic. The prefix through P is applied or handled under the explicit rejection policy.
D(group)Default app delivery boundary. The local message prefix in this group through D is acknowledged or excluded by a filter.
H(topic)Fixed network target for one registration or sync obligation. It is not a durable receipt or delivery position.

Network sequence IDs can have gaps. A boundary does not assert that every integer is an envelope ID. Group and identity topics have P <= F. Welcome completion uses F and the unresolved Welcome set, not the greatest successful Welcome ID. D uses a separate database-local message order, not network sequence IDs.

  • STR-001: Every bidirectional stream starts with Started(keepalive_interval_ms). No topics are registered yet. Zero means the client uses its default interval. An empty interest set stays open.
  • STR-002: Process Update requests in receive order. Validate each update before applying it atomically. Each topic occurs at most once across adds and removes. Duplicate or overlapping entries fail with INVALID_ARGUMENT.
  • STR-003: Every update ID is nonzero and strictly greater than the previous ID on this connection. Invalid IDs fail the stream. A new connection can restart its IDs.
  • STR-004: Adding an absent topic supplies an exclusive starting cursor C. Register the topic before capturing its visible head H from the selected read database. Queue Applied before any message for that registration. A publication racing registration must not be lost.
  • STR-005: Adding an active topic is a no-op, regardless of cursor. It changes neither its delivery position nor its initial catch-up target. The protocol has no seek operation for an active topic.
  • STR-006: Every accepted update receives one Applied with the same ID while the stream remains healthy. Acknowledgements follow update order and do not wait for history delivery. added_targets contains only newly registered topics, in add order; a new empty topic has target zero. An empty result does not mean existing topics finished processing.
  • STR-007: Removing an absent topic is a no-op. Removing an active topic cancels its pending work. Already queued messages may precede its acknowledgement. No message from the removed registration may follow that acknowledgement. Bytes already sent cannot be retracted.
  • STR-008: A later add of a removed topic creates a new registration from its supplied cursor. Internal generation checks discard stale fetch results. Ordered acknowledgement boundaries distinguish registrations without tagging every message.
  • STR-009: An unset inbound oneof or a structural limit violation fails with INVALID_ARGUMENT. Keep the spec 001 add/remove and topic-count limits. There is no wave-count limit.
  • STR-010: While a registration remains active, deliver every retained envelope above C in strictly increasing topic sequence order across frames, or explicitly fail the stream. Initialize its delivery floor to C. Gaps between sequence numbers are legal; cross-topic order is unspecified. A closed allocation boundary is only proof for retiring an absent gap; it is not a live-delivery ceiling, so visible rows above it may be delivered.
  • STR-011: Historical and newly published envelopes use the same Messages frame. There are no wave IDs, replay/live tags, TopicsLive, or CatchupComplete frames. A topic can continue delivering after its initial history without waiting for another topic.
  • STR-012: H is fixed for the registration. It is the head observed in the serving database, including replica lag, not a promise about the primary’s current head. Separate updates do not form one global snapshot. A cursor at or above H owes no initial history and still filters future delivery at or below C.
  • STR-013: THE SDK SHALL report processing through H complete only when P >= H for a group or identity topic, when the group is inactive under STR-086, or when F >= H and no unresolved Welcome at or below H remains. Required dependencies and group discovery are part of completion. Receiving Applied or the last envelope alone SHALL NOT establish completion. New publications SHALL NOT move H.
  • STR-014: THE SDK SHALL expose catch-up status for the app’s current interest set, including pending registrations, unfinished targets, and discovered groups. WHEN a Welcome joins a group, THE SDK SHALL durably record that discovery before completing the Welcome. It SHALL enroll required discovered group work before completing the parent catch-up operation. Completed topics SHALL NOT restart merely because another topic is added.
  • STR-015: WHEN an operation removes a topic, THE SDK SHALL cancel that operation’s outstanding obligation for the topic without reporting it complete or deleting durable pending data. It SHALL retain interests required by other operations. THE SDK SHALL report connection failure separately from processing completion. Reconnect SHALL produce new registration targets without changing an existing explicit sync target.
  • STR-016: Use bounded fair turns for topics with pending catch-up. A served topic returns to the back of the ready queue when it needs more work. Newly ready topics also join the back. Topics not visited before a byte cutoff retain priority. Do not repeatedly select the first topics by topic or sequence-ID sort order. Spec 002 supplies batch and byte bounds.

Fairness prevents starvation among ready topics; it does not promise equal throughput or a fixed latency. Shared connection bandwidth and a slow consumer still affect the whole stream.

3. Client processing and application choice

Section titled “3. Client processing and application choice”
  • STR-020: THE CLIENT SHALL use one shared durable receipt path and one processing policy per topic. Receipt SHALL commit encrypted pending envelopes with F. Processing SHALL commit state changes, produced messages, pending-work completion, and P together. WHEN an envelope fails transiently, THE CLIENT SHALL NOT advance P to or past that envelope’s sequence ID. P SHALL remain at the last applied or terminally rejected envelope, and the failed envelope SHALL stay pending, even when F is already greater.
  • STR-021: THE SDK SHALL CONTINUE TO let the app choose topics and filters, including denied topics. Consent and membership SHALL NOT become subscription authorization checks or force removal. THE CLIENT SHALL CONTINUE TO enforce the existing MLS authentication and decryptability rules.
  • STR-022: WHEN the app removes a group from delivery scope, THE SDK SHALL stop new handoffs for that group immediately. An executing callback cannot be undone. Queued items from the old scope SHALL NOT become eligible merely because the group is added again; the reader SHALL select them again from current local state. Network interest SHALL be removed only when no delivery, sync, send, or dependency operation needs it. Local stored-message delivery SHALL NOT wait for a network add acknowledgement.
  • STR-023: THE SDK SHALL expose one active default message consumer per client database, shared by all-groups and selected-group entry points. It SHALL reject a second default consumer with a typed AlreadyActive error. An explicit replay reader SHALL be independent of default delivery progress. Starting either reader SHALL NOT rewind network receipt or processing.
  • STR-024: THE SDK SHALL CONTINUE TO expose message-stream entry points, app-selected filters, and lifecycle callbacks, subject to the revised ownership and replay rules. Recovery SHALL use durable receipt and processing state. The SDK SHALL NOT promise exactly-once callbacks across a crash or consumer-owner change. Conversation callbacks SHALL remain live notifications; message replay SHALL NOT imply conversation-discovery replay.
  • STR-025: THE SDK SHALL expose a current catch-up snapshot and change notifications on each message-stream handle. The snapshot SHALL identify the scope generation, connection state (Connecting, Connected, Reconnecting, Failed, or Closed), and each selected topic’s registration state (Pending, Active, or Removed), optional fixed H, durable progress, and processing state (Pending, Complete, Blocked, or Cancelled). A pending registration SHALL have no target until acknowledged. Pending discovery SHALL remain visible until required group targets are enrolled. Scope changes SHALL increment the generation and report cancelled obligations under their old generation. The current scope SHALL be caught up only when all its obligations are complete and no registration or discovery is pending. Bindings SHALL preserve these fields and state meanings. A reconnect SHALL change connection status without erasing committed progress.
  • STR-030: Retain the configured fetched-data and outbound bounds. A fetch turn is not permission to materialize an arbitrary payload volume. Pause work only while required state remains safe; otherwise fail with RESOURCE_EXHAUSTED. Never skip an envelope and advance its floor.
  • STR-031: Every legal envelope must fit a delivery frame with worst-case metadata and framing. Startup rejects a permitted envelope size that cannot fit the delivery frame, transport cap, fetched-data budget, and outbound byte budget. Oversized responses return the existing size error. A slow stream must not block the shared tailer.
  • STR-032: Update and client Ping each use a per-stream bucket of 10 frames/s with burst 100. Exhaustion closes the stream with RESOURCE_EXHAUSTED. Pong consumes neither bucket. Caller quotas remain Phase 6 work.
  • STR-033: Either peer may send Ping. Reply with Pong carrying the same nonce. Keep at most one server challenge outstanding. Unrelated inbound traffic does not satisfy it.
  • STR-034: Reset the server send-idle timer on outbound admission, not inbound traffic. Start the pong deadline at Ping transport handoff. Before expiring it, consume already available inbound frames once without blocking. Missing Pong closes with DEADLINE_EXCEEDED.
  • STR-035: Cancellation, native request half-close, shutdown, and failure end the session and deregister its topics. Half-close is not a catch-up command and does not wait for targets. Tailer or any required database failure, including boundary-task failure, closes affected streams with UNAVAILABLE; a boundary lock timeout retains recovery work for a later attempt.
  • STR-036: WHEN a subscription reconnects, THE SDK SHALL use backoff, the current desired topic set, and each topic’s latest durable F. It SHALL deduplicate overlap through shared admission. It SHALL NOT use an uncommitted received ID, an old registration floor in place of newer F, or another topic’s progress. Durable pending data SHALL remain available for processing without another download.
  • STR-040: WHEN explicit sync starts, THE SDK SHALL capture a fresh fixed H for each starting topic from QueryNewest or a new subscription registration. An existing registration’s target or an active-topic add no-op SHALL NOT supply a fresh sync target. A missing replica-visible topic SHALL have H zero. Sync SHALL share receipt and processing with active subscriptions; it SHALL NOT require a dedicated stream or a second processor.
  • STR-041: THE SDK SHALL finish sync only after all enrolled processing obligations and required dependencies complete. It SHALL fix the starting Welcome target and enroll groups discovered by Welcomes at or below that target, with a fixed target for each group. Later traffic and unrelated locally created groups SHALL NOT extend the run. Completion or cancellation SHALL release only that run’s interests, without closing a receiver needed elsewhere. There is no history_only mode or server drain protocol.
  • STR-042: WHEN explicit sync has an unfinished target with F < H, THE SDK SHALL immediately schedule bounded Query pages after durable F through shared admission, without first waiting for a live receiver. A concurrent stream-first operation SHALL NOT delay that Query, and shared receipt MAY satisfy both operations. WHEN sync enrolls a discovered group or a required Welcome prefix, that work SHALL use the same immediate-Query policy and the run’s existing deadline. Once F >= H, it SHALL wait for processing without re-fetching the stored prefix.
  • STR-043: WHEN a sync run ends with unfinished obligations, THE SDK SHALL return a typed incomplete or blocked failure. Each unfinished obligation SHALL identify its topic, fixed H, F, P for a group or identity topic or unresolved Welcome IDs through H, and typed cause. Existing unit or count success results SHALL be returned only when all required obligations complete. Bindings SHALL preserve the failure’s per-obligation fields. A blocked group or Welcome SHALL NOT end the run before independent obligations complete, block, or reach the run’s deadline. Cancellation SHALL preserve committed pending work for a later processor. Receipt alone or a summary that omits per-envelope failures SHALL NOT count as success.
  • STR-044: THE SDK SHALL define sync success as processing through the sampled serving-database heads, including replica lag. It SHALL NOT claim visibility of every primary commit before the call. WHEN traffic continues beyond those heads, THE SDK SHALL permit the run to finish without processing that later traffic.
  • STR-050: A static request supplies 1 to 10,000 unique topic/cursor pairs. Empty sets, duplicates, and excess entries fail with INVALID_ARGUMENT.
  • STR-051: Its first Started frame supplies the keepalive interval and one fixed target per requested topic, in request order, including empty topics. It precedes every message. The stream then delivers ordinary envelopes until cancellation or failure.
  • STR-052: Use the same per-topic ordering, fixed targets, processing-completion meaning, fair fetch turns, and capacity rules as native streams. The end of the unary request does not trigger native half-close.
  • STR-053: Keepalive frames are one-way. Reopen after three keepalive intervals without any frame; data also proves activity. Use the client default for interval zero.
  • STR-054: WHEN static topic interests change, THE SDK SHALL open replacement streams from durable F and cancel the replaced registrations after replacement readiness. Shared admission SHALL handle overlap. THE SDK SHALL split more than 10,000 topics across streams. An empty logical subscription SHALL wait locally for its first topic.
  • STR-055: Send gRPC-Web requests directly to Tonic on the backend listener. HTTPS termination at the load balancer must pass requests through without gRPC-Web conversion or response buffering, with the required CORS and header handling. No WebSocket adapter or separate browser control service is required.
  • STR-060: THE CLIENT SHALL admit envelopes only from an ordered Query, Subscribe, or SubscribeStatic prefix with a known exclusive starting boundary at or below F. It SHALL validate topic, metadata, the shape of the backend-provided message_hash, and strictly increasing per-topic IDs, including order across frames in one registration. It SHALL retain that hash unchanged as the authoritative envelope hash under API-024 and API-025. It SHALL NOT validate it by recomputing the backend’s canonical encoding or hash. A valid join anchor is the only exception to receiving the prior group prefix.
  • STR-061: WHEN a batch overlaps F, THE CLIENT SHALL retain at most one pending envelope per topic and sequence ID. It SHALL insert the new suffix and advance F to its highest admitted ID in one transaction. A failed transaction SHALL leave both pending data and F unchanged. An empty batch SHALL leave F unchanged.
  • STR-062: THE CLIENT SHALL NOT advance F from QueryNewest, a registration target, Get, a push payload, or a publish response. WHEN a source starts beyond F without a validated join anchor, THE CLIENT SHALL refuse that admission and fetch the missing ordered prefix. Gaps between returned IDs SHALL NOT require a fetch for every absent integer.
  • STR-063: THE CLIENT SHALL bound admission batches, pending encrypted bytes, and in-memory fetched data. WHEN a topic reaches its per-topic pending bound, it SHALL pause receipt for that topic only. WHEN a kind budget is exhausted, it SHALL pause receipt for the topics holding the most pending data first. As capacity becomes available, it SHALL fairly admit ready topics, including topics whose pending queue is empty. An empty topic SHALL NOT bypass a shared kind row or byte limit. WHEN no legal admission fits the remaining capacity, it SHALL return a typed capacity failure and preserve pending data and F. A pause SHALL NOT drop unprocessed data or advance F past it. It SHALL retain separate bounded capacity for Welcome and identity dependencies so that group backlog alone cannot exhaust it. Storage failure SHALL leave durable progress unchanged and return a typed error.
  • STR-064: THE CLIENT SHALL CONTINUE TO protect pending envelopes with the client database’s encryption and access rules. It SHALL NOT log raw payloads, private keys, database keys, or full installation identifiers. Durable receipt SHALL NOT establish MLS authentication or permission to apply content.
  • STR-065: WHEN a push notification supplies a group and target, THE CLIENT SHALL treat it as a fetch hint. It SHALL query after F and use shared receipt and processing before returning the stored message. For an unknown group, it SHALL first resolve required Welcome discovery. A push deadline SHALL return incomplete work without discarding committed pending envelopes. It SHALL NOT directly apply an isolated push envelope.
  • STR-066: THE SDK SHALL enforce the internal client limits below, with documented positive finite values. It SHALL NOT expose these limits as application settings. It SHALL check both row and byte limits before admitting a batch. Group, Welcome, and identity pending budgets SHALL be separate; one kind SHALL NOT consume another kind’s budget. Byte accounting SHALL include the stored received envelope encoding for pending data and the decoded item encoding for local reads. It SHALL NOT require reproduction of the backend’s canonical envelope encoding. Row limits SHALL also bound per-row metadata. A legal item that cannot fit its budget SHALL produce a typed capacity error without cursor advancement. These limits do not claim to bound the whole encrypted database file.
Internal client limitsAccounting scope
max_admission_rows, max_admission_bytesOne admission batch
max_fetched_rows, max_fetched_bytesAll fetched data waiting in client memory
max_pending_rows[kind], max_pending_bytes[kind]Durable pending data for each topic kind, across its topics
max_pending_rows_per_topic, max_pending_bytes_per_topicDurable pending data for one topic
max_dependency_requestsAll concurrent dependency requests in one client process
max_local_read_rows, max_local_read_bytesOne local-reader batch

8. Ordered state processing and dependencies

Section titled “8. Ordered state processing and dependencies”
  • STR-070: THE CLIENT SHALL serialize state writers across database connections and processes. Each attempt SHALL acquire the database writer before loading mutable MLS state, relevant intent state, or Welcome keys. The same rule SHALL cover receive, outgoing preparation, proposals, Welcome installation, repair, import or restore, and key maintenance. A process-local lock alone SHALL NOT satisfy this rule.
  • STR-071: THE CLIENT SHALL keep each state-write attempt synchronous and transaction-scoped. It SHALL perform no network wait inside that transaction and SHALL NOT reuse mutable MLS objects outside it. WHEN an attempt or savepoint rolls back, retries, or waits for a dependency, it SHALL discard trial state and load fresh state in the next attempt.
  • STR-072: THE CLIENT SHALL process group and identity envelopes as a strict per-topic prefix. It SHALL apply the next pending envelope or record a permitted terminal rejection before advancing P. A blocked head SHALL prevent later envelopes on that topic from applying. Topics SHALL be independent: a blocked, waiting, or slow topic SHALL NOT delay a ready topic for longer than one bounded state-write attempt, and every ready topic SHALL be attempted within a bounded number of scheduling turns. The only cross-topic waits SHALL be the declared dependencies in STR-073 and STR-082.
  • STR-073: WHEN processing needs a dependency, THE CLIENT SHALL preserve the pending parent, release its write transaction, and resolve the dependency outside it. On return, it SHALL acquire the writer and re-read the pending head, current state, and required proofs. If another process completed the parent, it SHALL not apply it again. It SHALL revalidate state-dependent results before committing.
  • STR-074: THE CLIENT SHALL distinguish successful application, safe terminal rejection, transient retry, and blocked valid or unsupported work. Safe group rejection SHALL mean that the existing validation policy definitively rejects supported input as malformed, unauthenticated, stale, or conflicting, with the preceding prefix complete and all required validation state available. A non-retryable error alone SHALL NOT authorize rejection. Terminal rejection SHALL include an envelope whose epoch precedes the installed join epoch, an envelope whose epoch is older than the retained past-epoch window, a commit for an epoch that the ordered prefix has already advanced past, and an own-published envelope with no matching prepared attempt; these are not missing state. WHEN the complete preceding supported prefix leaves the group at an earlier epoch, an envelope naming a future epoch SHALL be rejected. WHEN a commit references a proposal absent from that complete preceding prefix, it SHALL be rejected. These cases SHALL NOT block later valid envelopes. A missing earlier prefix or missing local state for a proposal known to have been applied SHALL remain a blocked dependency or storage failure, not a rejection. A rejection decision SHALL depend only on the ordered topic prefix, the installed state, and verified proofs. It SHALL NOT depend on local timing, pending outgoing attempts, or delivery state. Missing state, an unsupported version, or an unresolved valid dependency SHALL block the affected prefix. An invalid identity prefix SHALL block identity processing and dependent work; it SHALL NOT become verified state. A terminal rejection SHALL record its topic, sequence ID, and typed reason atomically with P or Welcome completion, before pending work is removed.
  • STR-075: WHEN independent dependencies are resolved together, THE CLIENT SHALL use bounded concurrency and return a typed ready, retry, or permanent-failure result for each. One failure SHALL NOT discard successful sibling results or cancel unrelated dependency work. After a restart, it SHALL derive unresolved requirements again from durable pending parents.
  • STR-076: WHEN a parent requires identity state for an inbox at sequence N, THE CLIENT SHALL supply a verified association-state snapshot for that exact inbox and N, or an explicit failure. P(identity) >= N alone SHALL NOT satisfy that requirement. WHEN an envelope at sequence S references N with N >= S, THE CLIENT SHALL reject that envelope as invalid: the backend allocates one global sequence, so a referenced update commits before the referencing envelope is allocated. WHEN N < S and the serving replica lacks N, it SHALL retry through a primary Query for up to identity_reference_wait while the backend connection remains healthy. WHEN N is still absent after that wait, it SHALL treat the reference as permanently invalid and reject the envelope. A connection failure during the wait SHALL NOT count toward it. THE CLIENT SHALL CONTINUE TO preserve signature checks, replay protection, and verifier retryability from spec 003.
  • STR-077: WHEN a processing commit succeeds, THE CLIENT SHALL make its produced messages available to local readers without advancing D. WHEN a commit fails or the process dies before commit, it SHALL leave no partial MLS change, processed-position advance, message insertion, key change, or pending-work completion from that attempt.
  • STR-078: THE CLIENT SHALL treat local wake events as hints. Active processors, barrier waiters, interest controllers, and message readers SHALL make fresh database checks using active_database_poll_interval. A missed wake or a write from another process SHALL be found by a subsequent check without requiring a new network message. Processing correctness SHALL NOT depend on app-consumer ownership.
  • STR-079: THE SDK SHALL define named internal constants receiver_fallback_interval, active_database_poll_interval, default_consumer_lease_duration, and identity_reference_wait. Each SHALL have a documented positive finite value that fits the platform timer range. identity_reference_wait SHALL exceed the backend statement timeout; its value is 30 seconds. Active work SHALL start its next fresh check no later than active_database_poll_interval after the previous check finishes. A default owner SHALL renew before its lease expires or stop new handoffs. These are internal client policies, not application settings or backend wire limits. The default barrier timeout SHALL also be an internal constant. Existing operation-specific timeout arguments SHALL remain available.
  • STR-080: WHEN a Welcome fails transiently, THE CLIENT SHALL retain that Welcome and its retry state. It SHALL permit later independent Welcomes to complete within bounded scheduling turns. A later success SHALL NOT hide an earlier unresolved Welcome from a barrier through both IDs. Unsupported Welcomes SHALL remain blocked within their retention window under STR-115; only validation-policy rejection or the expiry policy SHALL complete unusable work.
  • STR-081: WHEN a Welcome pointer is unresolved, THE CLIENT SHALL query its destination topic and validate the expected pointee. An empty result SHALL remain retryable within the existing pointer deadline. It SHALL persist that deadline once and SHALL NOT extend it on retry or restart. The pending Welcome SHALL have one retry owner. Required private keys SHALL be retained subject to the existing key and expiry policy.
  • STR-082: THE CLIENT SHALL install a Welcome for an existing group only inside the state transaction of STR-070, after re-reading the group state, P, and the pending Welcome. It SHALL install only when the group is absent, or when the local state is inactive and P is below the validated join anchor. It SHALL also permit installation when inactive MLS state has P equal to the anchor and the Welcome epoch is at or beyond that state’s epoch. This exception permits a replacement Welcome from the commit that removed and re-added the installation. Welcome publication order SHALL NOT establish MLS epoch order. WHEN the local state is active and its epoch is at or beyond the Welcome epoch, it SHALL reject the Welcome as stale. WHEN the local state is active and its epoch is below the Welcome epoch, it SHALL first process the old-group prefix through the anchor as a dependency under STR-073, then re-evaluate under this rule: a removal found in that prefix permits installation, and still-active state rejects the Welcome as conflicting. The P write SHALL be conditional on the current P being below the anchor, or equal to the anchor only for the inactive replacement case above; a failed condition SHALL roll back the installation. It SHALL NOT rewind P or overwrite newer active group state.
  • STR-083: WHEN a valid join installs state, THE CLIENT SHALL validate the encrypted join metadata and cursor bounds. Missing or malformed metadata SHALL NOT silently become zero. Zero SHALL be accepted only for a supported initial-state case. The anchor SHALL be below the Welcome envelope’s own sequence ID; a larger anchor SHALL be a validation rejection. The validated anchor SHALL establish the new group’s starting P, and F SHALL become the greater of its current value and that anchor.
  • STR-084: THE CLIENT SHALL commit joined MLS state, group metadata, any join transcript and its local delivery number, key changes, join positions, and Welcome completion together. It SHALL remove buffered group envelopes at or below the validated anchor and retain those above it. A failed installation SHALL leave the previous state and pending Welcome intact.
  • STR-085: THE CLIENT SHALL persist group discovery before completing the Welcome. An all-groups controller SHALL reconcile interests from stored groups at startup and on the bounded fresh checks in STR-078. A missed join wake SHALL NOT permanently omit the group. THE CLIENT SHALL CONTINUE TO preserve consent, duplicate-DM, and one-shot Welcome rules.
  • STR-086: WHEN ordered processing applies a commit that removes this installation, THE CLIENT SHALL mark the group inactive, set P to that commit, and stop processing later envelopes on that topic while the group is inactive. It SHALL drop network interest for the topic unless another operation requires it and SHALL retain already pending envelopes above P. A barrier obligation on an inactive group SHALL be complete at its removal position. Only a valid later Welcome under STR-082 SHALL resume processing for that group.

Raw Welcomes are not app messages. A stored join transcript can be an app message. Its delivery position does not acknowledge a conversation callback.

  • STR-090: WHEN a message first becomes deliverable under STR-103, THE CLIENT SHALL atomically assign it one immutable, strictly increasing database-local delivery number. Optimistic insertion before eligibility SHALL NOT consume that message’s delivery number. Import SHALL assign fresh local numbers. Deletion SHALL NOT cause number reuse. A duplicate stable message ID SHALL NOT receive a second number or move its first number.
  • STR-091: THE SDK SHALL persist D per group and use zero when D is absent. A default consumer SHALL read retained candidates in local delivery-number order above each selected group’s D. It SHALL include eligible messages stored by earlier sync, push, import, or another process. Receipt and processing SHALL NOT advance D.
  • STR-092: THE SDK SHALL use the same saved D values for all-groups and selected-group default streams. WHEN scope excludes a group, it SHALL leave that group’s D unchanged. WHEN scope includes the group again, it SHALL deliver its retained matching backlog above D. A newly discovered group with no D SHALL start from its retained beginning.
  • STR-093: WHEN a candidate within selected scope fails the current consent or conversation-type filter, THE SDK SHALL advance that group’s D through the scanned row without a callback. Filter changes SHALL NOT rewind D or replay rows already scanned under an earlier filter. Filter changes SHALL affect future selection, not an item already handed to the app.
  • STR-094: THE SDK SHALL keep at most one unacknowledged message per default consumer. WHEN a callback returns normally, or an iterator caller requests the next item, it SHALL attempt to persist D for the previous item before handing off another. Queuing a callback, returning the first iterator item, dropping an iterator, or a callback error SHALL NOT acknowledge that item. Async work started by a callback after its return is outside this acknowledgement contract.
  • STR-095: WHEN default acknowledgement persistence fails, THE SDK SHALL retry that write or stop the reader before any later handoff or D advance. It SHALL NOT skip the unacknowledged item on restart. WHEN a crash occurs after app handling but before durable acknowledgement, it SHALL permit the item to repeat with the same stable message ID.
  • STR-096: THE SDK SHALL acquire default-consumer ownership atomically across processes, renew it while active, and check it before each handoff. Each acquisition or renewal SHALL set expiry using default_consumer_lease_duration. The SDK SHALL release ownership on clean close and permit takeover after expiry. Every D mutation, including filter exclusion and acknowledgement, SHALL atomically validate the current unexpired owner token. An old owner SHALL NOT advance the new owner’s D. Suspension or owner expiry during a callback can cause a duplicate; ownership SHALL NOT be claimed as exactly-once app execution.
  • STR-097: WHEN the default stream closes, THE SDK SHALL preserve F, P, and D. A later default stream SHALL resume from saved D without normal replay of acknowledged rows. Explicit replay SHALL remain available under STR-100. A network reconnect SHALL NOT reset local delivery progress.
  • STR-098: THE SDK SHALL attach a typed delivery_cursor to every delivered message item, for both default and explicit replay. The cursor SHALL contain the database identity and that item’s immutable local delivery number. It SHALL be available at handoff and SHALL mean resume strictly after this item; exposing it SHALL NOT acknowledge default delivery. The SDK SHALL also supply a beginning cursor with local number zero for the current database. A network sequence ID or timestamp SHALL NOT be accepted as this cursor. WHEN the supplied database identity differs, it SHALL return a typed cursor error.
  • STR-099: THE SDK SHALL provide a history snapshot and delivery cursor from the same database snapshot for a gap-free history-to-stream transition. It SHALL include only messages eligible within that snapshot’s selected scope and filters, and use the snapshot’s local delivery boundary. Later eligible messages SHALL receive higher local delivery numbers.
  • STR-100: WHEN an explicit from delivery cursor is supplied, THE SDK SHALL create an independent replay reader. It SHALL read retained matching messages with greater local numbers in local order, then continue with new local messages. It SHALL neither read nor change default D and SHALL NOT acquire default-consumer ownership. The app controls this reader’s restart cursor.
  • STR-101: THE SDK SHALL bound local read batches and SHALL check fresh local state before sleeping for more work. It SHALL use the bounded checks in STR-078 to find cross-process writes and missed wakes. Network failure SHALL NOT prevent delivery of already stored matching messages.
  • STR-102: THE SDK SHALL limit replay to retained deliverable message records. A cursor SHALL NOT restore deleted, expired, or deliberately removed content. Later metadata edits, deletions, and conversation notifications SHALL NOT be presented as new messages solely to make them replayable.
  • STR-103: THE CLIENT SHALL make retained Application and MembershipChange message records eligible only when they have Published delivery status and are committed by successful ordered MLS processing, valid Welcome or local-group installation, or an accepted history import. For an outgoing network message, preparation, optimistic storage, and a publish receipt alone SHALL NOT establish eligibility; the ordered processor SHALL confirm its outcome first. Unpublished or failed outgoing records SHALL have no delivery number. At selection, the SDK SHALL exclude deleted records and records whose client disappearing-message deadline has elapsed. Backend expiry metadata alone SHALL NOT hide a message. Scope and filters SHALL apply after this eligibility decision. THE CLIENT SHALL CONTINUE TO preserve existing content, stable message IDs, and disappearing-message rules.
  • STR-104: Bindings SHALL carry a per-handoff acknowledgement token with default delivery through each callback queue, worker boundary, or iterator wrapper. The host-side wrapper SHALL acknowledge that token only on the callback-return or next-item boundary in STR-094. The core SHALL validate the token’s owner and item before advancing D. Callback failure or iterator cancellation SHALL leave the item unacknowledged. Existing public callback and iterator entry points SHALL perform this exchange automatically; the app SHALL NOT need a new manual acknowledgement call. A binding queue’s successful enqueue or internal callback return SHALL NOT substitute for the app boundary.

For example, an all-groups consumer acknowledges A1 and B1. An A-only consumer then acknowledges A2 while B2 is stored by push processing. Returning to all-groups delivers B2, but does not repeat A1, B1, or A2. A filter exclusion differs from scope exclusion: filtered rows inside selected scope advance D.

  • STR-110: BEFORE publishing an outgoing attempt, THE CLIENT SHALL persist its exact envelope bytes, a payload identity computed over the inner opaque MLS bytes, staged state when required, and base MLS state identity. The payload identity SHALL NOT depend on the outer envelope encoding or on the backend’s message_hash. It SHALL preserve the exact bytes of required follow-up Welcome batches. Preparation SHALL occur under the fresh-state writer rule and SHALL select the next eligible logical intent.
  • STR-111: WHILE a publish outcome is ambiguous, THE CLIENT SHALL retry the same prepared envelope bytes and SHALL NOT replace that attempt by re-encryption. Only a known outcome or ordered processing that proves the attempt superseded SHALL permit a replacement attempt. Legitimate retries SHALL preserve the logical message’s stable identity.
  • STR-112: WHEN multiple processes help an outgoing membership intent, THE CLIENT SHALL permit only one eligible prepared membership attempt for that group. A second process SHALL help the existing attempt rather than prepare a competing commit. It SHALL validate staged state against current group state before applying it; a competing ordered commit SHALL cause stale staged state to be discarded before a legitimate replacement is prepared.
  • STR-113: WHEN a publish reply arrives, THE CLIENT SHALL attach it only to the matching current prepared attempt and SHALL store the returned message_hash as that attempt’s authoritative envelope hash without recomputing it. A late reply for an obsolete payload identity SHALL NOT overwrite or complete a newer attempt. Ordered processing SHALL match an incoming envelope to the attempt by payload identity over the returned inner bytes, so a match SHALL NOT require the reply. A matching envelope received before the reply SHALL be handled through ordinary ordered processing.
  • STR-114: WHEN send waits for its publication and a healthy receiver covers the topic with F below the publication target, THE CLIENT SHALL first wait for receipt within receiver_fallback_interval. WHEN no receiver covers the topic, or that interval ends before receipt reaches the target, it SHALL use bounded Query pages after durable F through shared admission. If the interval would reach the deadline, it SHALL start Query without that wait. Partial receipt SHALL NOT reset the interval. Once receipt reaches the target, it SHALL wait for processing without re-fetching the stored prefix. It SHALL NOT apply the publish receipt as an ordered-prefix proof or start a send-only MLS receive path. Send success SHALL require the logical intent’s outcome and required follow-up work, not merely P >= the published sequence. WHEN the publish reply is a success receipt but processing through its sequence does not complete within the deadline, THE CLIENT SHALL return a typed published-but-unconfirmed result, keep that attempt current, and SHALL NOT prepare a replacement attempt for the logical intent until ordered processing reports its outcome.
  • STR-115: THE CLIENT SHALL retain a Welcome it cannot process for a bounded retention window and SHALL persist that deadline once, without extending it on retry or restart. It SHALL recheck blocked Welcomes on a bounded interval as well as on a new coordinator, so a client that stays running reaches the deadline. At or after the deadline it SHALL make one further processing attempt, and SHALL complete and remove the Welcome only when that attempt does not succeed. A removed Welcome SHALL NOT hold a barrier obligation, retained Welcome capacity, or key retirement. Retention SHALL NOT be a substitute for validation: a Welcome that is invalid rather than unsupported SHALL still be rejected immediately.
  • Future epoch: admit a supported envelope that names a future epoch after a complete prefix, followed by a valid current-epoch envelope. Reject the first with a typed reason and permit the second to apply. A missing prefix or unsupported preceding input must still block.
  • Missing proposal: admit a supported commit whose referenced proposal never occurred in the complete preceding prefix. Reject the commit and permit later valid work. In a separate case, remove local state for a proposal already applied; processing must block without advancing P.
  • Shared capacity: fill one kind’s pending row or byte budget, then offer a legal envelope for an empty topic of that kind. Return a typed capacity failure without changing F or deleting pending data. Resume fair admission when capacity is available. Separate Welcome and identity budgets must remain usable.
  • Authoritative hash: return a well-formed backend hash that differs from a local envelope re-encoding. Preserve and use that hash without comparison to a local recomputation. Match an own-published message by its unchanged inner payload identity.