RFDs/0005
Initial local vertical slice and first SDKs
Goal
Define the initial end-to-end release that demonstrates Ryusen’s distinct value:
an offline server accepts an event, durably executes sequential
step.run callbacks through a language SDK, survives restart, and automatically
executes available compensation after terminal failure.
Release boundary
The initial version includes:
-
one statically distributable
ryusenbinary with an embedded SQLite database; -
a
ryusen devserver with a connected-worker gateway and HTTP worker invoker; -
an embedded development UI backed by durable execution telemetry;
-
Rust, Go, Elixir, and TypeScript SDKs implementing the RFD 2 protocol;
-
connected execution by default and explicitly selected HTTP execution;
-
a first-class local workspace, immutable complete application manifests and workspace routing generations with exact-name event triggers, and durable event acknowledgement;
-
one function per run with sequential
step.rundiscovery and execution; -
forward retries with deterministic backoff;
-
optional run deadlines implemented as server-owned delayed work, with no default timeout or local ceiling;
-
optional reverse sequential compensation;
-
process-restart recovery and queryable run history;
-
explicit visibility and operator resolution for
Unknownattempts; -
the seeded pure and SQLite benchmark profiles required by RFD 3 and RFD 4.
Each SDK keeps the same small semantic surface while using idiomatic syntax:
let payment = step
.run("charge", || charge(&event.data))
.compensate(|payment| refund(payment.id))
.await?;
step.run("ship", || ship(&payment)).await?;
payment, err := step.Run(ctx, "charge", charge,
step.Compensate(refund))
if err != nil {
return err
}
_, err = step.Run(ctx, "ship", func() error { return ship(payment) })
return err
{:ok, payment} =
Step.run("charge", fn -> charge(event.data) end,
compensate: fn payment -> refund(payment.id) end
)
Step.run("ship", fn -> ship(payment) end)
const payment = await step.run("charge", () => charge(event.data), {
compensate: payment => refund(payment.id),
});
await step.run("ship", () => ship(payment));
Compensation is optional. No saga-specific wrapper, integration API, or version configuration is exposed.
The function body outside step.run callbacks is repeatable optimistic
orchestration and may run more than once or declare different steps as ambient
state changes. External effects belong inside forward or compensation callbacks,
where Ryusen can apply retry, authorization, ambiguity, and recovery semantics
without adding another user-facing primitive.
Local topology
Each SDK starts an outbound connected worker by default. In local development, the SDK publishes its complete application manifest when its content differs from the active generation, then the worker registers its executable functions and capacity over the gateway connection. The developer does not configure an inbound application URL:
ryusen dev --data-dir .ryusen
# The application SDK connects to http://127.0.0.1:8288 by default.
The local server creates a default local workspace and application when none are configured. It has no authentication, so publishing a manifest, registering workers, and submitting events add no manual step to the default loopback workflow, and it sends HTTP worker invocations unsigned, so it has no secret to print or copy. Additional local apps may join the same workspace and subscribe to the same events. Hosted credentials explicitly authorize workspace event capabilities, application deployment and execution capabilities, or operator roles; naming another workspace or application in a payload never extends that authority.
HTTP mode is explicit in the SDK and exposes an idiomatic handler: a Rust
service, a Go net/http handler, an Elixir Plug, or a Fetch-compatible
TypeScript handler. Local development configures that endpoint explicitly:
ryusen dev --data-dir .ryusen \
--http-worker-url http://127.0.0.1:3000/ryusen
Manifest publication and execution registration are distinct even when one SDK startup performs both locally. Application-manifest and workspace routing generations are durable and remain active while every worker is offline. Connected registration exists only while a worker is live; HTTP execution availability follows the configured endpoint. Reconnection refreshes capability and capacity but never republishes or removes triggers. Connected replicas from one rollout share an opaque release ID and reconnect with it; the gateway uses its durable first-seen position to prefer newer compatible releases without pinning a run. When no release ID is supplied, the SDK joins an unversioned execution-contract group. HTTP mode leaves revision selection to its configured endpoint. Both modes invoke the same function registry and execution callbacks.
The server exposes a workspace event endpoint and authorized inspection endpoints. Event submission accepts an optional caller-provided idempotency key and always generates an event ID. The key is scoped to the workspace and event submission operation. The server commits its canonical request fingerprint, the event, selected workspace routing generation, and fan-out work before returning. An identical retry returns the original durable event ID and current expansion state, acknowledged as a replay of the event that first committed it, while a fresh submission is acknowledged as a creation; conflicting reuse is rejected without mutation. Matching runs are created across every subscribed application in bounded idempotent fan-out transactions and linked back to the event, application, trigger binding, application-manifest generation, and workspace routing generation.
SDK contract
RFD 2 owns the language-neutral execution semantics and RFD 6 owns the
transport lifecycle. This section owns the local developer surface the initial
Rust and Elixir SDKs share. Each SDK lives at sdk/<language>; Rust is
sdk/rust and Elixir is sdk/elixir.
Developer lifecycle
One lifecycle runs from declaration to inspection, and every step is an SDK call rather than an external command:
-
Declare. The developer registers logical functions by exact identifier and declares each function’s exact-name event triggers and optional whole-run timeout in one registry. The registry is the complete desired manifest.
-
Publish. The SDK publishes its complete manifest before any worker registers. It reads its application’s active manifest generation, publishes only when the complete desired state differs, and uses the returned
manifest_generation. -
Register. The connected worker registers with that generation as its manifest hash, exactly the manifest’s function identifiers, its instance identity, capacity one, and SDK/runtime metadata.
-
Submit. The SDK submits the workspace event that names the trigger and returns the acknowledgement.
-
Execute. The gateway dispatches discovery, then targeted forward and compensation work; the SDK runs the same
step.runcallbacks and reports the same outcomes in connected and HTTP mode. -
Inspect. The SDK reads the runs associated with the event and one run’s lifecycle to a terminal outcome.
Registration never publishes or removes triggers, and a worker that fails to register leaves the active manifest and routing generations unchanged.
Configuration
One configuration shape covers both SDKs and both transports:
| Setting | Contract | Default and derivation |
|---|---|---|
Base HTTP URL |
Absolute |
Required; the local quick start uses the address |
Gateway URL |
|
Derived from the base HTTP URL by replacing the scheme with |
Credential |
Bearer token sent as |
Optional. The local server does not authenticate callers, so a client without
one sends no |
Scope |
Workspace and application selectors carried by manifest publication and registration. |
Defaults to workspace |
Mode |
Connected worker (default) or explicit HTTP service mode. |
Connected is the only default; HTTP requires an explicit constructor, option, or adapter and a configured server endpoint. |
Explicit configuration wins over environment convenience variables. The
canonical convenience names are RYUSEN_HTTP_URL for the base HTTP URL,
RYUSEN_CREDENTIAL for the optional credential, and RYUSEN_GATEWAY_URL for an
explicit gateway override. A credential is never logged.
An HTTP worker verifies invocations with a signing key, which is separate from
the client credential: RYUSEN_SIGNING_KEY. It skips verification only in an
explicit development mode, RYUSEN_DEV=1 or the SDK’s development constructor,
because the local server does not need to sign what it sends. Without either, an
HTTP worker refuses to start rather than accept unsigned invocations.
Manifest publication and startup
Manifest payload semantics belong to RFD 2. Locally, the SDK makes its complete desired state active without an external step:
-
read the active workspace routing generation for its scope, together with the application’s active manifest generation and declarations;
-
when the application is already the SDK’s complete desired state, publish nothing and register with the active generation;
-
otherwise publish the complete manifest with the active generation as
expected_parent, or no parent on a first publication, and register with the returnedmanifest_generation; -
surface a same-application parent conflict to the developer rather than retrying silently against a fabricated parent; a retry is safe only after re-reading the active generation, so the publication is fenced against the generation it actually replaces.
The read in the first step is GET /v1/manifests/active, served by the local
server under the same bearer authentication as publication and scoped to the
same implicit local organization, workspace, and default application. It
answers with the active manifest_generation, the routing_generation that
references it, the exact contents bytes the publisher supplied, and the canonical
function declarations and trigger bindings, so a publisher compares desired state
against server-canonical state rather than against generation identifiers: a
manifest identifier hashes its expected parent, so identifiers alone cannot
establish that two generations hold the same content. A workspace with no active
manifest is answered as the API’s established missing resource, not as an empty
manifest. Rust and Elixir must use the same publication sequence and the same
canonical manifest contents, so an application deployed through either SDK
converges on one active generation.
Event client
Event payloads are bytes on the wire. The SDK accepts bytes unchanged and may add an explicit JSON convenience that serializes a JSON document to UTF-8 bytes for the caller; the convenience never rewrites bytes it was given, and the non-JSON-native tagged variants in RFD 2 stay outside it.
Every submission sends an idempotency key. A caller-supplied key is used as
given; an SDK that generates one reuses it for every retry of that same logical
submission and never silently replaces it. An identical retry returns the
original event identifier with disposition replayed, a fresh submission is
acknowledged as created, and reuse of a key with a different request is
refused as idempotency_key_reused with nothing mutated, so the SDK must not
retry that refusal under a new key without the caller’s decision. The optional
caller timestamp and the optional string routing metadata participate in the
canonical request fingerprint, so a retry repeats them exactly.
The acknowledgement carries the submission version, the event identifier, the workspace routing generation the event was acknowledged against, its expansion state, and the disposition. An SDK returns those fields instead of parsing a message.
Run query
The minimum run query covers the local example: the runs associated with one event, and one run’s lifecycle. The event-run read returns each run’s identifier, application, function, source event, phase, execution outcome, and recovery outcome. The run detail additionally returns the declared steps with their durable outcomes and the run’s terminal output or failure. The example polls the event-run read with a bounded timeout until a run reaches a terminal phase; the SDK does not hide a non-terminal result as a failure.
Acceptance workflow
Each initial SDK passes the same acceptance workflow against a real local server in both transports before the milestone is complete. The shared protocol fixtures and the fake gateway and HTTP invoker from RFD 2 and RFD 6 are a prerequisite: an SDK that has not passed the fixture matrix does not run the real-server workflow.
This milestone runs the subset of the release-level acceptance scenario below that covers the shared SDK contract; the deferred hardening at the end of this section names what the milestone does not yet cover.
The workflow is one self-contained program per SDK:
-
Start
ryusen devwith an ephemeral or file-backed database and obtain its listening address from its startup output. -
Register two compensable steps and a third step that fails terminally, plus an exact-name trigger for the acceptance event.
-
Publish the complete manifest automatically, assert the returned generation is used for registration, and assert a second start with unchanged declarations publishes nothing new.
-
Submit the acceptance event through the SDK’s event client and assert the acknowledgement is a creation; repeat the identical submission and assert it replays the original event.
-
Connected transport: register over the gateway and observe the run reach
Failedwith both compensators executed once in reverse order. -
HTTP transport: start the SDK’s explicit handler, configure the server endpoint, and observe the same registry and callbacks produce the same terminal outcome.
-
Look up the event’s runs and the run detail through the SDK’s run query and assert the durable outcomes.
-
Restart the server after the event is acknowledged and after a step result is accepted, and assert replay does not rerun accepted callbacks.
A connected run additionally proves that a failed connect attempt is visible and retried while no HTTP endpoint is selected. An HTTP run additionally proves that an unauthorized or malformed invocation is refused before any callback runs.
Current behavior, milestone requirements, and deferred hardening
This section separates what the repository does today from what this milestone requires, so no document or checklist claims work that does not exist.
Current behavior.
-
ryusen devserves the HTTP API, the WebSocket gateway, and the durable engine, and prints its listening address. It needs no credential. -
sdk/rustruns a connected worker and an explicit HTTP service over one registry and the shared protocol crate. Its control client reads the active application manifest, publishes the complete desired manifest only when it differs, submits events under stable submission identifiers, and reads an event’s runs and one run’s detail with a bounded wait for a terminal phase. -
The shipped
connected_workerandhttp_workerexamples are self-contained: they declare their workflow, triggers, contents bytes, and optional run timeout, publish their complete manifest, and take only the running server’s URL from the documented environment variable, plusRYUSEN_DEV=1for an HTTP endpoint. No example needs curl, a generation argument, or a credential. -
Both transports have automated real-server acceptance that launches the current binary on ephemeral loopback ports and storage. It covers automatic first publication, an already-current restart that creates no generation, a fenced changed publication with a refused stale fence, event replay, reverse compensation, workflow success, typed run lookup, and a connected worker restart. The connected shell harness additionally verifies the published manifest, the submitted event, and the terminal run independently over the HTTP API.
-
Connected workers execute assignments serially; capacity is advertised but a session loop cannot use more than one at a time. Heartbeats, lease extension, drain, and in-flight result redelivery are not implemented.
-
The local server issues no credential: it does not authenticate callers and sends HTTP invocations unsigned, so a restart needs no worker or endpoint to be reconfigured. A deployment that signs invocations configures a signing key on the server and the same key on its HTTP workers.
-
There is no Go or TypeScript SDK.
-
The Elixir SDK under
sdk/elixirimplements the shared contract: the immutable function registry,step.runexecution with tagged tuples, the exact tagged value codec, the connected worker through a maintained WebSocket client, the explicit HTTP Plug, one immutable configuration, the complete desired manifest, the authenticated control client, event submission with stable identity, the run reads with a bounded terminal wait, and a connected startup that ensures the manifest before it registers. It passes every shared connected and HTTP conformance scenario against the real implementation, its real-server acceptance runs in both transports are gated with the Rust ones, and the pinned development shell carries its toolchain.
Milestone requirements.
-
Rust and Elixir SDKs under
sdk/<language>share the lifecycle, configuration, manifest, event, callback, and transport contract in this document, RFD 2, and RFD 6. -
Both SDKs publish their complete manifest automatically before registration, submit authenticated events, and expose the minimum run query.
-
Both SDKs run the acceptance workflow above in connected and HTTP mode against a real local server and pass the shared fixtures and fake gateway.
-
Both SDKs read
GET /v1/manifests/activebefore publishing, so an already-current application publishes nothing and registers with the active generation, and both publish fenced by the active generation with a bounded re-read before each retry. -
The Elixir SDK uses a maintained WebSocket client library and an OTP supervision tree for the connected worker. It does not implement RFC 6455 framing itself, and a supervised worker restart reconnects and re-registers without changing the active manifest; work that was in flight follows the durable lease-recovery rules rather than being silently retried by the SDK.
-
The Elixir SDK’s low-level connected worker keeps accepting a caller-supplied manifest hash, so a caller that manages publication itself is unaffected by the high-level startup.
Deferred hardening.
-
Package registry publication for either SDK.
-
Asynchronous Rust callbacks; the Rust callback API stays synchronous.
-
Lease renewal, workflow concurrency, and capacity greater than one.
-
Heartbeats, graceful worker and gateway drain, and in-flight result redelivery.
-
Persisted or configurable credentials and TLS transports. A non-loopback listener is available only through the explicit
--allow-remoteoption. -
Hosted deployment control-plane publication and hosted capacity claims.
-
Go and TypeScript SDKs, which remain in this RFD’s release boundary but follow this local milestone.
-
Wire-format changes, including any change to byte event ingress.
Operability
The CLI prints the listening addresses, database path, active workspace routing
generation and application manifests, connected worker count, executable
function capabilities, capacities, HTTP endpoints, and SDK connectivity
failures. A minimal HTTP API returns event expansion and run associations, run
summary, step and compensation status, attempts, and transactional history. The
embedded UI in RFD 7 visualizes those APIs and provides event submission and
explicit Unknown resolution.
Graceful shutdown stops leasing new work and allows a bounded drain. Abrupt termination is a required test path: after restart, expired leases make durable work discoverable without replaying acknowledged events from another system. Connected workers automatically reconnect and re-register after the gateway returns. The local server does not authenticate them, so a worker that outlives a restart reconnects without being reconfigured. A rejected registration under an authenticating server remains a visible terminal failure rather than a silent retry or another transport.
Unknown work is never hidden as an ordinary retry. The initial operator API
resolves it by accepting a supplied portable result, marking the attempt as a
failure, or explicitly authorizing another physical attempt. Every request names
the attempt and generation, carries one idempotency key, and contains exactly one
command; the request, the acknowledgement, and the refusal are versioned JSON
envelopes, so a client acts on a field rather than on a message.
The command envelope is strict at every level, and it is the documented object form that is read: the tag names one variant, no other variant’s field may be present, an unknown field inside a declared failure is refused rather than ignored, a supplied value is checked at every level it nests through, including the fields of a blob reference and the members of an object value, whose own keys stay data, and a document that repeats a member name, spells a position positionally, or names a variant with anything but a string tag is refused rather than reinterpreted. A lenient decoder would accept a retry that also carried a supplied result and discard the result, or commit a supplied value with a field stripped, which is a different command than the operator sent. The shared worker codec stays lenient, so peers of different versions can still exchange a value that carries a field one of them added; only the operator envelope is read exactly.
An acknowledgement reports whether the command was applied or replayed, the
generation it resolved, the transition it committed, and — for an explicit retry —
the successor generation, work identifier, and physical attempt. A refusal names
one of stale, idempotency_key_reused, retry_forbidden, deadline_elapsed,
unsupported_version, invalid, limit_exceeded, or server, and never carries
the idempotency key, a stored fingerprint, a lease token, or another scope’s
attempt. A supplied result is validated against the portable value contract and a
declared failure against the structured error’s limits before any lookup, so a
refused command mutates nothing. That bound accounts for the durable record the
command is stored inside, so a command whose own size no record could hold is
refused before the attempt is read rather than by a rolled-back write. Two bounds
cannot be known before the run is read and are decided inside the transaction
instead: the aggregate replay budget, and the settled step record, which carries
the step’s declaration — durable state this server does not bound — beside the
outcome it now holds. The settled record is measured before the mutation section,
so it is refused with nothing touched; the replay budget is measured while the
outcome is recorded, so that refusal rolls the enclosing transaction back. Either
way a refusal leaves no durable state behind.
Explicit retry retains the operation ID and is unavailable for cancelling runs,
where stop intent has already forbidden new forward work. It settles the
ambiguous generation as RetryAuthorized and creates exactly one fenced
successor. A late result from the old generation remains diagnostic while the
successor can complete the logical target normally. An attempt that is still
unresolved when a stop is requested keeps the run in Cancelling: a supplied
success settles the step and joins the unwind, a declared failure settles it
without a forward retry, and the run compensates or terminalizes an empty unwind
only once every authorized attempt has settled.
Explicit retry is also unavailable once a forward attempt is at or after its run
deadline, and the refusal says so as deadline_elapsed with nothing mutated.
Waiting for the run timer to be dispatched cannot make it available again, because
the refusal is decided from durable state: a run that already committed
TimedOut keeps refusing the retry as deadline_elapsed even if a later clock
sample reads below the deadline, and one that committed Cancelled keeps
refusing it as retry_forbidden even after the deadline has elapsed. A supplied
success or a declared failure can still settle the attempt: on a still-running
expired attempt the command commits the TimedOut intent the deadline implies and
settles under stopping semantics in the same transaction, an attempt on a run that
already committed a stop reason preserves that reason, and either way the run
never reports success, no forward retry or rediscovery is scheduled, and a
repeated request replays the recorded response instead of re-deriving it from the
run’s later state.
A transport that proves an authorized effect never began — a failed connection, or a correlated pre-start rejection — is reconciled by the engine rather than by the transport. While the run is running the same physical attempt is requeued without consuming an attempt ordinal; under stop intent the attempt is fenced, the run releases ownership, and the settlement gate runs, because the attempt provably did not execute and requeueing it would leave work no dispatcher can prepare.
Acceptance scenario
An automated example performs three sequential steps where the first two have compensators and the third fails terminally. Tests stop and restart the server after event acknowledgement, step declaration, forward completion, transition to compensation, and compensation completion. The final history shows each transaction and proves that every available compensator ran once logically in reverse order.
An immediate-stop example cancels or times out a newly created Running run
before its first discovery authorization. The queued discovery is fenced, no SDK
callback executes, and the run terminalizes as
Cancelled/TimedOut + NotRequired(EmptyUnwind).
A deadline example creates a timed run, crashes immediately after fan-out, and
recovers its delayed RunTimeout item. It races completion before and after the
deadline against delayed timer processing. Pre-deadline completion may commit
normally and remove the timer; post-deadline completion materializes TimedOut,
and a late authorized success settles into the unwind rather than bypassing
timeout.
An operator-resolution deadline example resolves an unresolved authorized attempt
at and after its deadline, with the timer first and with the operator first. A
supplied success and a declared failure settle under TimedOut without creating a
successor or rediscovery, explicit retry is refused as deadline_elapsed with
nothing mutated, a pre-deadline retry replays its recorded response after the run
times out, and a cancellation that committed first keeps its own reason. The
embedded UI renders the refusal, keeps the acknowledgement visible after the
attempt leaves the projection, and reports TimedOut rather than a successful
execution.
An authorization fixture creates two applications in one workspace with
colliding function IDs. A valid credential for application B cannot publish
application A’s manifest, register a connected release or HTTP endpoint for A,
or receive A’s work. A’s work remains durable until an A-authorized worker
becomes available. A credential with the workspace’s events:send capability
may submit one event that legitimately triggers functions in both applications;
event authority does not become execution authority.
A second example removes a pending step and one compensator from the live SDK implementation. The existing run proceeds optimistically: old forward work becomes obsolete, missing compensation is skipped, and the final state is partial rather than blocked. The language-neutral behavior suite runs against all four SDKs.
A third example adds a compensator after its forward step completed. If the old run has not yet reached that unwind target, current code executes the new compensator. Adding it after a durable skip does not reopen completed recovery.
A fourth example encounters one step ID twice in a discovery pass. The first
callback may complete, but the second never executes and the run becomes
DefinitionBlocked. After deploying code that retains the first ID and assigns
the second call a unique ID, rediscovery reuses the first recorded result and
executes only the newly identified work. Variants remove the duplicate and
rename the original to demonstrate the optimistic live-code rules. The same
scenario and structured diagnostics pass in Rust, Go, Elixir, and TypeScript.
Event-ingestion fixtures lose the first committed response and retry the same key and request, race identical submissions, reuse the same key independently in another workspace, and submit a conflicting payload under the original scope. They prove one event and fan-out obligation are created for identical requests and that conflicting reuse creates no event, run, or routing mutation.
A workspace-routing fixture registers matching functions in applications A and B, submits one workspace event, and observes both runs linked to the event. It then races an application-manifest activation with event acknowledgement. The event records exactly one immutable workspace routing generation and expands against only that generation, while its runs remain free to execute on current compatible workers.
An operator-retry fixture makes generation G Unknown, authorizes G+1, and
restarts the server at that transaction boundary. It then submits a late result
for G and a valid result for G+1, proving the former is stale diagnostic data and
the latter alone advances the logical target. Identical repeated submissions
return their prior disposition idempotently; conflicting attempts to resettle
either generation are rejected. The HTTP surface is covered end to end: an
applied command, its identical replay, a reused key with a different command, a
stale generation, an unsupported version, a malformed command, a command that
mixes two variants, a supplied value carrying a field at its own level and inside
a nested member, an over-limit, an over-nested, and an unpersistable supplied
value, an unpersistable declared failure, and an unauthorized request that cannot
tell an existing attempt from a missing one. The pre-application refusal block
there also compares the work item, the run, and the run’s history before and after
it, and the engine suite covers the two bounds that can only be decided inside the
transaction: the aggregate replay budget, and a settled step record that no longer
fits beside the declaration it carries.
Cancellation settlement is covered from both sides: a stop request with an
unresolved authorized effect enters Cancelling instead of refusing, a supplied
success compensates the step it settled, a declared failure terminalizes an empty
unwind, an explicit retry is refused, and a reopened store reconstructs the same
stop intent and unresolved attempt. A correctly correlated late worker success
and a late worker failure settle the same way, a late result and an operator
command racing the same unresolved generation have exactly one winner, a result
that arrives after the winner is stale and mutates nothing, and a repeated stop
intent with the same outcome is idempotent while the run is Cancelling. A
retaining timeout replays its transition before and after a reopen, and a
transport that proves an authorized attempt never began is reconciled both while
the run is running and after a stop. The post-stop ordering is covered over a
real HTTP worker that holds its response while the run is cancelled, so the stop
is observed first; the reverse ordering, where the proof arrives before the stop,
is covered at the engine level.
Outcome fixtures also fail before any step completes, producing
Failed + NotRequired(EmptyUnwind), and run an all-skipped plan, producing
Failed + PartiallyCompensated. Restart tests cover the snapshot transaction so
neither case can strand in Compensating without work.
Each SDK also executes the acceptance workflow in both transports. Connected
tests cover disconnect before and after start authorization, result redelivery,
and graceful drain. HTTP tests cover explicit mode selection, repeatable
discovery retry, proven pre-send failure, authenticated pre-start rejection,
canonical callback results, and ambiguous effectful outcomes becoming Unknown.
The ambiguity cases include a callback effect followed by HTTP 500, timeout,
disconnect, malformed or truncated response, and invalid response correlation.
Browser acceptance submits the event from the development UI, observes its workspace routing generation, referenced application manifests, trigger matches, and associated runs, follows a live run timeline through failure and compensation, inspects worker transport telemetry, restarts the server, and verifies the same durable associations and history reappear. It also commits an update between a page’s transactional bootstrap snapshot and SSE subscription and verifies that cursor catch-up renders it. The resolution controls are driven against a real server and worker: an unresolved forward attempt resolved by an explicit retry, an unresolved compensation attempt that renders a real refusal before it is settled, a lost response that is reported as ambiguous and retried under the same key, and a narrow viewport that keeps every command reachable.
Performance acceptance
The initial local release ships the common seeded benchmark harness and its pure and SQLite profiles. CI enforces structural gates for disjoint event writes, bounded acknowledgement and fan-out work, absence of global queue mutation keys, and no full-backlog scan per lease. Stable benchmarks record a revision-relative baseline and explicit regression budget; noisy timings remain reported but do not become a false deterministic gate.
An optional developer-installed single-machine FDB profile may validate the
future adapter’s conflict behavior without changing the ryusen dev runtime or
distribution. Multi-machine FDB capacity, tail-latency, and failure-recovery SLOs
are a hosted-backend acceptance gate after this local milestone. They run from
separate load generators against a documented production-like topology as
defined by RFD 3 and use the same engine workload matrix as RFD 4.
Live-update resource limits
The live-update surface is bounded per connection and per server, with a stream limit as its only shedding policy:
-
One connection holds one server-sent event stream. While a stream is caught up it parks in a bounded wait and re-scans the journal after each wait, so an idle stream issues about four durable scans per second whether or not anything changed. A stream with changes to read scans again immediately, so while changes are flowing the read load tracks the change rate as well as the number of open streams.
-
Each stream holds a long-lived response task, re-scans the journal, and can buffer one change page. The server therefore admits at most a configured number of streams, 64 by default, and refuses the next one at once with
503and thestream_limitcode instead of adding another response and its read load; the client then polls. RFD 7 describes the refusal and the polling fallback. Per-stream buffering is bounded by the change page limit, and the client-applied cursor advances only as frames are read, so a client that stops reading stalls its own stream rather than growing a server-side buffer. -
GET /v1/serverreports the open stream count against the limit, refusals since start, how many streams are catching up, the deepest per-stream queue, and the furthest-behind stream’s lag, all as instantaneous observations. The age of the oldest retained cursor is not reported, and there is no per-workspace or per-credential stream quota. -
Draining is bounded. A stream ends as soon as it observes the shutdown signal, including one that subscribes while the drain is already under way, and a filtered backlog cannot delay it. A client that stopped reading fills the socket, though, and the transport then stops polling the response body, so the connection task cannot be cancelled from inside the body. The server therefore stops waiting for open connections after a fixed deadline and returns instead of letting one wedged client hold shutdown open.
A shared or adaptive scan cadence and the oldest retained cursor’s age are follow-up work tracked in the implementation checklist. Like the stream limit, they are correctness-neutral: durable catch-up and the polling fallback deliver the same changes at the same cursors.
Explicitly deferred
The initial release excludes FoundationDB, multiple server nodes, CEL conditions,
waitForEvent, sleep and cron APIs, workflow concurrency,
throttling, rate limits, debounce, batching, priority configuration, tenant
fairness, hosted administration, managed integrations, and SDKs beyond Rust, Go,
Elixir, and TypeScript.
These are not excluded from the architecture. They follow after the initial vertical slice validates the protocol, storage contract, run state machine, and local benchmark harness. The production-like FDB benchmark is deferred with the FDB backend, not replaced by local SQLite performance.
Dependency order
Implementation proceeds by proving a thin path rather than completing each RFD in isolation: protocol fixture, SQLite transaction, manifest activation, event/fan-out/run creation, shared dispatch, Rust connected worker, one forward step, replay, retries, compensation, HTTP mode, telemetry projection, UI timeline, and restart tests. Rust is the reference SDK because it can share repository tooling and generated wire types with the server, but it does not import server domain types. Go, Elixir, and TypeScript follow against the frozen protocol and transport fixtures before the initial milestone is complete; they do not introduce different semantics.
The local SDK milestone inside this slice proceeds Rust first, then Elixir, against the same frozen protocol, fixture, and transport contract. Go and TypeScript follow once that contract has been proved in two languages.
Implementation
The release checklist and acceptance tests are tracked in RFD 5 implementation checklist.