reactor-runtime 3.2.0 · Opt-in moderation · Audio on the wall clock
Breaking
- Marking a field for moderation is opt-in.
InputField(moderate=True)marks a field whose text a deployment should have moderated, and the default is nowFalse. Earlier releases marked every field unless it opted out, so a model that wants a field moderated has to say so. Mark only the fields a client writes free text into — a mark on an enum, a number, or a file changes nothing, since only free text is eligible — and only on a command that arrives occasionally, like a prompt or a script. A check runs one command at a time and admits anything still waiting after two seconds, so marking a per-frame command neither gets it moderated nor leaves room for the prompts that matter. The runtime moderates nothing itself: the mark is a preference a deployment reads off the published schema, where every field now states it either way.
New
- A model can send more than one audio track. Outbound audio is keyed by track name, the way video always was, so two
Audiofields on oneOutputreach the client as two tracks. Declaring two previously concatenated them into one buffer that played out through whichever track negotiated last, at twice the rate, with nothing rejecting or logging it. - Clients are told why a session ended. A session the platform ends can carry a human-readable reason, delivered to every connected client before its connection closes, in place of a bare disconnect. Nothing in the model changes.
Fixed
- Audio no longer drifts against video. An outbound audio track’s clock advances only with the samples pushed onto it, so a tick the model did not fill was time the stream never accounted for — and because the packets either side of it stayed contiguous, the client read the whole stream as arriving late, grew its jitter buffer, and time-stretched audio to refill it. That stretching was the artefact and the swinging buffer was the drift. The runtime now pushes a frame on every tick, sending silence for one the model did not fill, and warns with the track’s name when it has had to manufacture a meaningful share of the last second.
- Pausing an audio track stops the audio. A paused track is skipped before anything is read from it, so a client that pauses stops receiving packets instead of roughly fifty a second of digital silence — and the pause no longer counts against the under-production warning, which had been blaming the model for audio the client declined.
reactor-runtime 3.1.2 · Per-frame metadata
Rolls up 3.1.0 through 3.1.2. The two patch releases pin the transport dependency and change nothing you write.
New
- Send metadata with a frame. Wrap a track’s payload in
TrackPayloadto tag what it emits:TrackPayload(frame, metadata={"seed": seed}). A mapping travels as JSON and bytes travel as they are, and a batch carries either one value for the whole batch or one per frame. A bare array still works everywhere it did. - Read the metadata a client attached.
InputFrame.metadataholds the bytes the sender sent with that frame, orNonewhen it sent none. Decoding them is the model’s business — the transport treats them as opaque. See Media Input.
reactor-runtime 3.0.2 · Output backpressure · The playout handle
Breaking
emit()waits for downstream room. A model that generates faster than its playout rate is throttled to that rate rather than piling up latency, so a hand-rolled rate limiter is now redundant and should come out. The wait runs off the model loop, so commands and lifecycle hooks keep dispatching while it holds. Passdrop=Trueon a producer that would rather skip a frame than wait.outputis the playout handle, not an annotation. Removeoutput: MyOutputfrom your model class — outbound tracks register when theOutputsubclass is defined, and the name now belongs toself.output. A class that re-annotates it contradicts the real attribute.buffer_sizemust be positive when declared. Zero or less fails at startup instead of silently falling back to the runtime default.
New
self.outputcontrols playout.self.output.fps = nre-paces frames that are already queued rather than waiting for the next emit, so a speed command takes effect immediately.self.output.flush()drops what is queued and cuts the client to black, which is what a scene reset wants so none of the old content plays afterwards. Both fan out to every connection, including ones that join later. See The Run Loop.buffer_sizebounds buffered latency. It declares how many frames may sit between the model and each client, and is never applied below one emitted chunk, so a batching model always fits a whole batch.- A quiet wire on underrun. When no frame is ready for a tick nothing is sent and the client holds what it has, instead of the stream spending bandwidth repeating it. One black frame marks each boundary — a connection opening, or a flush.
reactor-runtime 3.0.1 · Command failures · Recording in process
Breaking
- An
@eventreturn annotation must name one message type, or nothing. A handler annotates a singleModelMessagesubclass for a typed reply, orNonefor a bodyless acknowledgement. A union —Reply | Noneincluded — is now rejected when the class is declared, because the schema publishes one response shape and a client generated from it would expect no body and receive one. Annotate the message and raiseCommandErrorfor the failure case. See Events & Messages.
New
- Command handlers can fail out loud.
raise CommandError(code, message)answers the calling client with a failure it can branch on, correlated with its command, so an awaiting caller rejects with a reason instead of hanging. Any other exception answers withinternal_errorand keeps its detail in the log. UploadedFile.sizereports the byte length of an upload.- Recording encodes in process. No external encoder binary is involved. A finished recording stays fetchable for five minutes after its session ends and is then deleted; the clip endpoints answer
410 Gonepast that.recording_dir(orREACTOR_RECORDINGS_DIR) chooses where chunks are written. See Session Recording. - Render a schema without serving one.
python -m reactor_runtime.schemaprints the OpenAPI contract of the model in a directory, so a build step can publish it without booting the runtime. The schema is titled with the name the model publishes. GET /metricsserves the process’s metrics in Prometheus text format.
reactor-runtime 3.0.0 · New authoring surface
A rebuilt runtime with a smaller, sharper authoring surface. Every change below is source-level and mechanical; the shape of a model — declare tracks,
load(), run(), emit — is unchanged.Breaking
- Import from
reactor_runtimedirectly. Everything author-facing is exported from the top-level package. Replacefrom reactor_runtime.interface import ...withfrom reactor_runtime import .... load()receives a path, not a dict. The signature is nowload(self, config_path: Path | None), and the runtime no longer parses the file. Read it however you like:yaml.safe_load(config_path.read_text()) if config_path else {}.output_bufferis nowself.output. There is no single buffer any more — each connection paces its own playback — so the handle is named for what it does.output_buffer.flush()becomesself.output.flush()andoutput_buffer.set_fps(n)becomesself.output.fps = n, both fanning out to every connection.buffer_sizeandemit(drop=...)keep their meaning. See The Run Loop.- Drop the
output: MyOutputclass annotation. Outbound tracks register when theOutputsubclass is defined, so the annotation was never read — and the name now belongs to the playout handle, so leaving it in contradicts the real attribute. Delete the line; nothing replaces it. OutputandInputsubclasses drop@dataclass. Declare the track annotations and nothing else.Output.__init__now validates that you supplied exactly the declared tracks, so a missing track fails at theemit()call rather than silently streaming nothing.@event(dedupe=True)is removed. Every command is delivered. A handler that must collapse a burst should track the latest value itself.runtime.weights_pathis no longer read by the runtime.get_weights_path()resolves$REACTOR_WEIGHTS_PATH, falling back to~/.cache/reactor_registry.reactor runstill readsruntime.weights_pathto decide what to mount and sets the variable for you, so most workspaces need no change. See Weights.servehas no subcommands. The entry point ispython -m reactor_runtime.serve, run from the directory holdingreactor.yaml. Fetch a model’s schema fromGET /schemaon the running server instead ofserve schema.- Removed from the public API:
get_profiler(),ReactorConfig,ReactorCore, and theEvent/Connected/Disconnectedclasses. Use the@event,@connected, and@disconnecteddecorators. - Python 3.12 or newer is required.
- No media libraries on the host. The runtime carries its own WebRTC stack as a wheel, so a model image needs nothing from the system package manager and a workspace Dockerfile can start from a plain Python base.
PREFERRED_TRANSPORTand theGST_-prefixed variables are gone with it.
New
- Session lifecycle hooks.
@session_startedand@session_endedbracket the session as a whole and fire once each, so a model can tell a new viewer joining apart from the session itself beginning. Note that a session end tears its connections down wholesale without firing the per-client@disconnectedhooks. See Sessions & Clients. - Command replies. An
@eventhandler that returns aModelMessagesends it as that command’s correlated reply, so a client awaiting the command resolves with the state that actually took effect — useful when a value is clamped or a default is resolved server-side. - Read order per track. Reads take a
mode:ReadMode.LATESTreturns the newest frames and clears the backlog,ReadMode.FIFOconsumes in arrival order. Video wants the former, audio the latter. See Media Input. - Rate from measured throughput. Passing
compute_timetoemit()plays the chunk back at the rate you actually produced it, so a model that warms up or slows under load stays in sync without touchingfps. - Defaults validated at import. An
InputFielddefault that violates its own constraints now fails when the class is defined rather than on the first request, and mutable defaults are rejected outright.
reactor-runtime 2.9.4 · High-res codecs · Network resilience
Rolls up the patch releases since 2.8.0.
New
- Clients are told when the network is struggling. The runtime periodically sends a
mediaStatsmessage on the data channel carrying an aggregate video quality score (0–10), so frontends can show a “connection is unstable” notice when reduced stream quality comes from network conditions rather than the model. Already surfaced by the JS SDK.
Fixed
- H.264 and H.265 now stream above 720p. The senders pinned the level offered by the client onto the encoder, and browser offers advertise a ~720p level — so any higher-resolution model produced zero video on those codecs while the session otherwise looked healthy. The encoder now derives a level that fits the actual resolution (only the negotiated profile is enforced), and 2K+ output flows on both codecs, verified against real Chrome (H.264) and Safari (H.265) clients.
- Video survives low-MTU networks. RTP packets are now capped at 1200 bytes, matching mainstream WebRTC stacks. Sessions on VPNs, WireGuard/Tailscale tunnels, PPPoE ISPs, and similar paths previously connected fine (data channel, audio) but silently black-holed every full-size video packet. Overridable per deployment with
GST_RTP_PAYLOAD_MTU. - Every frame width works over WebRTC. Widths that are not a multiple of 4 no longer come in as a grayscale, sheared smear or get dropped by the encoder on the way out — the transport now honours GStreamer’s row padding in both directions. Widths that already worked keep the exact same fast path.
- Native macOS runs. The GStreamer send pipeline no longer fails to build on Homebrew PyGObject, so running the runtime directly on a Mac (outside
reactor run) works for local development.
reactor-runtime 2.8.0 · Multi-client sessions
Breaking
- The
aiortcWebRTC fallback has been removed. Running the runtime directly on a host now requires a working GStreamer installation. Stay inside the container withreactor run, which bundles GStreamer and every other dependency automatically.
New
- Multiple clients can share one session.
@connectedand@disconnectednow run once per client as each one joins and leaves, andself.connectedstays set while at least one client is connected. Single-client models keep working unchanged:self.send()still reaches the one client connected. - Per-client messaging with
ClientInfo. Any handler (@event,@connected,@disconnected,@file_uploaded) can accept aclient: ClientInfoparameter for the client that triggered it. Callclient.send()to reply to that single client orself.send()to broadcast to everyone, and store the handle to message a client later. Runtime responses (requestClip,requestRecording, schema) now address only the requesting client instead of broadcasting. See Events and Messages.
reactor-runtime 2.7.0 · Session recording
New
- Session recording, configured from
reactor.yaml. Models can now record every session continuously and let clients request snap clips or full recordings of the live stream. Fliprecording.enabled: trueinreactor.yamland the runtime hooks the same buffer that feeds the wire, encodes fMP4 chunks in the background, and exposes arequestClip/requestRecordingAPI to clients. Already wired in the JS SDK and the Demo Frontend’s Capture panel. See the new Session Recording page for the full configuration reference, encoder knobs (chunk_seconds,crf,target_width, …), and multi-track disambiguation.
reactor-runtime 2.6.0 · Host CLI · Docker
Breaking
- Host CLI is now the Go
reactorbinary. The runtime no longer publishes a console script. Replacepip install reactor-runtime+reactor-runtime init|runwith the GoreactorCLI; the runtime itself ships only insidereactor-runtime-base. - Workspace runtime version is now pinned in the
Dockerfile.reactor initsubstitutes a fully-qualifiedFROM reactortechnologies/reactor-runtime-base:<X.Y.Z-N>line. Existing workspaces usingARG RUNTIME_VERSIONneed a one-line edit; everything else keeps working unchanged. reactor runno longer accepts build-time flags. Build-phase concerns (-f,--no-cache,--build-secret) moved toreactor build; run-phase concerns (--port,--gpus,--tty,-e,--env-file) stay onreactor run. Both share the same image tag, soreactor build && reactor runalways boots the freshly built image.
New
- Scaffolded workspaces double as plain Docker projects.
reactor initemitsENTRYPOINT [..., "python", "-m", "reactor_runtime.serve"]andCMD ["run"], sodocker build && docker run -p 8080:8080 .works without thereactorCLI installed.reactor runitself remains opinionated about therunsubcommand.
reactor-runtime 2.5.0 · CLI · reactor.yaml · Weights
Breaking
- CLI renamed:
reactor→reactor-runtime. The bundled console script is nowreactor-runtime; updatereactor run|init|schemacall sites accordingly. When the GoreactorCLI is installed,reactor runkeeps working by delegating toreactor-runtime rununder the hood.
New
- Modern nested shape for
reactor.yaml. Identity and runtime entrypoint are now split undermodel:andruntime:sections. The legacy flat shape keeps working but logs a one-shot deprecation warning per process; new scaffolds emit the modern shape. See Model Anatomy. get_weights_path()helper. New import fromreactor_runtimereturns apathlib.Pathto the resolved weights root, with resolution order$REACTOR_WEIGHTS_PATH→runtime.weights_path→~/.cache/reactor_registry. See Weights.runtime.weights_pathinreactor.yaml. Optional field for committing a workspace-relative weights root (the env var still wins, so production deployments override committed values).reactor-runtime initimprovements. Name argument is optional (scaffolds into the current empty folder when omitted), model name is substituted intoreactor.yamlautomatically, a Dockerfile is part of the scaffold (python:3.12-slim+ GStreamer +uv), andrequirements.txtshipsreactor-runtimeso the workspace.venvhas the import target available immediately.
Improved
- Multi-line
ModelMessagedocstrings render correctly.reactor-runtime schemaand downstream SDK / docs generation now preserve the full docstring of eachModelMessagesubclass. Wrapped one-sentence summaries are no longer truncated at the first newline, and undocumented@dataclassmessages no longer leak their constructor signature into the description.