> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reactor.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> What's new in the Reactor Runtime.

Releases, breaking changes, and notable improvements to the Python runtime partners use to build and ship models on Reactor. Subscribe to updates in [Discord](https://discord.com/invite/xSbBWECQRk).

<Update label="August 17, 2026" description="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 now `False`. 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 `Audio` fields on one `Output` reach 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.
</Update>

<Update label="August 11, 2026" description="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 `TrackPayload` to 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.metadata` holds the bytes the sender sent with that frame, or `None` when it sent none. Decoding them is the model's business — the transport treats them as opaque. See [Media Input](/deploy/development/reactor-model/media-input#frames).
</Update>

<Update label="August 5, 2026" description="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. Pass `drop=True` on a producer that would rather skip a frame than wait.
  * **`output` is the playout handle, not an annotation.** Remove `output: MyOutput` from your model class — outbound tracks register when the `Output` subclass is defined, and the name now belongs to `self.output`. A class that re-annotates it contradicts the real attribute.
  * **`buffer_size` must be positive when declared.** Zero or less fails at startup instead of silently falling back to the runtime default.

  ### New

  * **`self.output` controls playout.** `self.output.fps = n` re-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](/deploy/development/reactor-model/run-loop#controlling-playout).
  * **`buffer_size` bounds 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.
</Update>

<Update label="August 4, 2026" description="reactor-runtime 3.0.1 · Command failures · Recording in process">
  ### Breaking

  * **An `@event` return annotation must name one message type, or nothing.** A handler annotates a single `ModelMessage` subclass for a typed reply, or `None` for a bodyless acknowledgement. A union — `Reply | None` included — 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 raise `CommandError` for the failure case. See [Events & Messages](/deploy/development/reactor-model/events-and-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 with `internal_error` and keeps its detail in the log.
  * **`UploadedFile.size`** reports 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 Gone` past that. `recording_dir` (or `REACTOR_RECORDINGS_DIR`) chooses where chunks are written. See [Session Recording](/deploy/development/recording).
  * **Render a schema without serving one.** `python -m reactor_runtime.schema` prints 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 /metrics`** serves the process's metrics in Prometheus text format.
</Update>

<Update label="July 28, 2026" description="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_runtime` directly.** Everything author-facing is exported from the top-level package. Replace `from reactor_runtime.interface import ...` with `from reactor_runtime import ...`.
  * **`load()` receives a path, not a dict.** The signature is now `load(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_buffer` is now `self.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()` becomes `self.output.flush()` and `output_buffer.set_fps(n)` becomes `self.output.fps = n`, both fanning out to every connection. `buffer_size` and `emit(drop=...)` keep their meaning. See [The Run Loop](/deploy/development/reactor-model/run-loop#controlling-playout).
  * **Drop the `output: MyOutput` class annotation.** Outbound tracks register when the `Output` subclass 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.
  * **`Output` and `Input` subclasses 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 the `emit()` 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_path` is no longer read by the runtime.** `get_weights_path()` resolves `$REACTOR_WEIGHTS_PATH`, falling back to `~/.cache/reactor_registry`. `reactor run` still reads `runtime.weights_path` to decide what to mount and sets the variable for you, so most workspaces need no change. See [Weights](/deploy/development/weights).
  * **`serve` has no subcommands.** The entry point is `python -m reactor_runtime.serve`, run from the directory holding `reactor.yaml`. Fetch a model's schema from `GET /schema` on the running server instead of `serve schema`.
  * **Removed from the public API:** `get_profiler()`, `ReactorConfig`, `ReactorCore`, and the `Event` / `Connected` / `Disconnected` classes. Use the `@event`, `@connected`, and `@disconnected` decorators.
  * **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_TRANSPORT` and the `GST_`-prefixed variables are gone with it.

  ### New

  * **Session lifecycle hooks.** `@session_started` and `@session_ended` bracket 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 `@disconnected` hooks. See [Sessions & Clients](/deploy/development/reactor-model/lifecycle).
  * **Command replies.** An `@event` handler that returns a `ModelMessage` sends 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.LATEST` returns the newest frames and clears the backlog, `ReadMode.FIFO` consumes in arrival order. Video wants the former, audio the latter. See [Media Input](/deploy/development/reactor-model/media-input).
  * **Rate from measured throughput.** Passing `compute_time` to `emit()` 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 touching `fps`.
  * **Defaults validated at import.** An `InputField` default that violates its own constraints now fails when the class is defined rather than on the first request, and mutable defaults are rejected outright.
</Update>

<Update label="July 13, 2026" description="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 `mediaStats` message 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.
</Update>

<Update label="June 7, 2026" description="reactor-runtime 2.8.0 · Multi-client sessions">
  ### Breaking

  * **The `aiortc` WebRTC fallback has been removed.** Running the runtime directly on a host now requires a working GStreamer installation. Stay inside the container with `reactor run`, which bundles GStreamer and every other dependency automatically.

  ### New

  * **Multiple clients can share one session.** `@connected` and `@disconnected` now run once per client as each one joins and leaves, and `self.connected` stays 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 a `client: ClientInfo` parameter for the client that triggered it. Call `client.send()` to reply to that single client or `self.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](/deploy/development/reactor-model/events-and-messages).
</Update>

<Update label="May 18, 2026" description="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. Flip `recording.enabled: true` in `reactor.yaml` and the runtime hooks the same buffer that feeds the wire, encodes fMP4 chunks in the background, and exposes a `requestClip` / `requestRecording` API to clients. Already wired in the JS SDK and the [Demo Frontend](https://reactor-sandbox.vercel.app/)'s Capture panel. See the new [Session Recording](/deploy/development/recording) page for the full configuration reference, encoder knobs (`chunk_seconds`, `crf`, `target_width`, ...), and multi-track disambiguation.
</Update>

<Update label="May 12, 2026" description="reactor-runtime 2.6.0 · Host CLI · Docker">
  ### Breaking

  * **Host CLI is now the Go `reactor` binary.** The runtime no longer publishes a console script. Replace `pip install reactor-runtime` + `reactor-runtime init|run` with the Go `reactor` CLI; the runtime itself ships only inside `reactor-runtime-base`.
  * **Workspace runtime version is now pinned in the `Dockerfile`.** `reactor init` substitutes a fully-qualified `FROM reactortechnologies/reactor-runtime-base:<X.Y.Z-N>` line. Existing workspaces using `ARG RUNTIME_VERSION` need a one-line edit; everything else keeps working unchanged.
  * **`reactor run` no longer accepts build-time flags.** Build-phase concerns (`-f`, `--no-cache`, `--build-secret`) moved to `reactor build`; run-phase concerns (`--port`, `--gpus`, `--tty`, `-e`, `--env-file`) stay on `reactor run`. Both share the same image tag, so `reactor build && reactor run` always boots the freshly built image.

  ### New

  * **Scaffolded workspaces double as plain Docker projects.** `reactor init` emits `ENTRYPOINT [..., "python", "-m", "reactor_runtime.serve"]` and `CMD ["run"]`, so `docker build && docker run -p 8080:8080 .` works without the `reactor` CLI installed. `reactor run` itself remains opinionated about the `run` subcommand.
</Update>

<Update label="May 1, 2026" description="reactor-runtime 2.5.0 · CLI · reactor.yaml · Weights">
  ### Breaking

  * **CLI renamed: `reactor` → `reactor-runtime`.** The bundled console script is now `reactor-runtime`; update `reactor run|init|schema` call sites accordingly. When the Go `reactor` CLI is installed, `reactor run` keeps working by delegating to `reactor-runtime run` under the hood.

  ### New

  * **Modern nested shape for `reactor.yaml`.** Identity and runtime entrypoint are now split under `model:` and `runtime:` 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](/deploy/development/reactor-model/model-anatomy).
  * **`get_weights_path()` helper.** New import from `reactor_runtime` returns a `pathlib.Path` to the resolved weights root, with resolution order `$REACTOR_WEIGHTS_PATH` → `runtime.weights_path` → `~/.cache/reactor_registry`. See [Weights](/deploy/development/weights).
  * **`runtime.weights_path` in `reactor.yaml`.** Optional field for committing a workspace-relative weights root (the env var still wins, so production deployments override committed values).
  * **`reactor-runtime init` improvements.** Name argument is optional (scaffolds into the current empty folder when omitted), model name is substituted into `reactor.yaml` automatically, a Dockerfile is part of the scaffold (`python:3.12-slim` + GStreamer + `uv`), and `requirements.txt` ships `reactor-runtime` so the workspace `.venv` has the import target available immediately.

  ### Improved

  * **Multi-line `ModelMessage` docstrings render correctly.** `reactor-runtime schema` and downstream SDK / docs generation now preserve the full docstring of each `ModelMessage` subclass. Wrapped one-sentence summaries are no longer truncated at the first newline, and undocumented `@dataclass` messages no longer leak their constructor signature into the description.
</Update>
