reactor-runtime 3.6.0 · Multi-GPU inference · The end of a recording
New
- Run a model on several GPUs with
DistributedRunner. Import it fromreactor_runtime.distributed. The runner starts one process per rank and constructs your worker class in each one. Onegenerate(input)call reaches every rank and returns rank 0’s result. A model that splits a step gathers the pieces onto rank 0 insidegenerate(), because the runner assembles nothing itself. Your application starts the runner in its ownload()and calls it fromgenerate(), and keeps the client’s commands and tracks. Giveworld_sizethe GPU count you ask for inreactor.yaml. - Write each worker as a
DistributedWorker. The base class declaresrank,world_size, anddevice, and the runner sets all three before it callsload(). Your subclass writesload(),generate(), andreset(). When every rank raises the same type of error, the call raises rank 0’s exception and the group stays usable. A crash, a timeout, or ranks that disagree raiseWorkerCrashed,WorkerTimeout, orRankDesync. The runner then refuses further calls, so shut it down and construct a new one. See Multi-GPU inference.
Fixed
- A recording keeps the frames from the end of a session. The recorder closed its encoder as soon as the session stopped, while frames it had already taken were still queued for that encoder. Those frames never reached the file, so the recording and any clip over the end of the session both stopped early. The recorder now waits up to a second for the queue to reach the encoder, and closes the encoder after that. An encoder still behind after the wait loses the rest of the queue. The runtime logs a warning with the number of frames left in it.
reactor-runtime 3.5.0 · ReactorApp · The step loop
New
ReactorAppis the class you subclass, andgenerate()is the method you write. The runtime drives the class one step at a time: each step callsprocess_input(), thengenerate(input), thenprocess_output(outcome), and emits theOutputthe last one returns.generate()runs one step of inference, synchronously, and returns the result or raises. The other two have defaults, so a model that returns anOutputfromgenerate()writes nothing else. Overrideprocess_input()to refuse a step withApplicationError("reason")or to shape what the model gets; overrideprocess_output()to map a result onto tracks, send messages, or recover from an error.StepOutcomecarries whatgenerate()did:result,error, andelapsed. A raise out ofprocess_output()ends the session with an error; the default re-raises. See The Step Loop.- Declare what a client can set as
state:. Annotate anInputStatesubclass on the app and every public field becomes aset_<field>command, validated from itsInputFieldconstraints and documented from its description. The runtime buildsself.statefrom the field defaults when a session starts, andprocess_input()reads it there on every step; the hook takes no parameters. A field with a leading underscore is session scratch with no command. A hand-written@eventof the same name replaces a generated setter. See Managing State. - Handlers run between steps. Every
@eventhandler and lifecycle hook waits for the step in flight to finish before it runs, so a step reads one consistent state and a handler can call into the model safely. When the last client leaves or the session ends, the loop stops at the next step boundary and resets the input buffers. - Playout follows the model’s speed unless you pin it. A class that declares
fpsplays out at that rate. One that does not plays each step’s frames at the rate the step was produced, from the runtime’s own measurement ofgenerate(), so a hand-passedcompute_timeis no longer needed. ReactorModelis nowReactorApp, andInputis nowMediaInput. Both old names still import and resolve to the same classes, with aDeprecationWarning. A model with a hand-writtenrun()keeps working unchanged: overridingrun()replaces the step loop and only the step loop, and the three hooks are then never called. The wire, the schema, and the client SDKs do not change.ReactorPipelineandIdleare deprecated. Both still import fromreactor_runtimeand resolve to themselves, so a model on the generator pattern keeps running, but each import now raises aDeprecationWarningand writes one warning to the log. The replacement is a port, not an import edit: subclassReactorAppand writegenerate(), and refuse a step by raisingApplicationErrorfromprocess_input()whereinference()yieldedIdle. All four deprecated names are removed in the next major.- The runtime’s examples are split into an application half and a model half.
examples/starteris the spinning logoreactor initscaffolds, written as two halves; the logo image is its weights, read from the workspace’sweights/directory, and it runs on a CPU.examples/echoloops the client’s webcam and microphone back through one of seven video effects.examples/waypointdrives a world model from an uploaded seed frame and keyboard and mouse controls. In each,<name>.pyis theReactorAppand<name>_model.pyis a plain class that imports nothing from the runtime.examples/brightnessis gone;startertakes its place. See Application and Model.
reactor-runtime 3.4.0 · Faster connection setup
New
- A connection is live sooner. The runtime answers the two WARP opt-ins, a pair of IETF drafts that shorten the WebRTC handshake. One carries the DTLS handshake inside the ICE checks, and the other lets the data channel skip a round trip of its own setup. Both are on by default, and you do not have to change your model. A client that does not ask for them gets the ordinary handshake, so what you save depends on the SDK at the other end.
runtime_webrtc_connect_secondsreports the client’s wait until the wire is live.
reactor-runtime 3.3.2 · Metrics for a live wire
New
GET /metricsnow reports how a live wire carries your media. The runtime samples each WebRTC connection and adds the readings to the Prometheus output it already serves. Per track, it counts the packets, bytes, and video frames that went each way. It also records the loss the receiver reports on what you sent, and the jitter on what you receive. Thetracklabel is the name you declared on yourOutputorInputclass. Per connection, it records the bandwidth congestion control believes the path will carry, and two round trips. The ICE checks measure one and the receiver measures the other on the media itself, so a gap between them is a queue the checks do not pass through.- The runtime also counts the media it dropped or invented before it reached the wire.
runtime_media_dropped_frames_totalandruntime_media_dropped_samples_totalcount the frames and audio samples it discarded.runtime_media_silence_frames_totalcounts the audio frames it sent as silence because your model produced none. None of this appears in the transport readings, so read these counters beside them to tell a slow model from a poor network. Two histograms time the handshake as well.runtime_webrtc_negotiation_secondsmeasures the answer the runtime builds, andruntime_webrtc_connect_secondsmeasures the client’s wait until the wire is live.
reactor-runtime 3.3.1 · Uploads inside a list
Fixed
- A handler that declares a list of uploads now receives the files. The runtime resolved an
UploadedFileparameter, but not one nested inside alist, adict, or a dataclass field. Each of those reached the handler as the client’s raw reference, a mapping with anupload_idkey. The handler then failed when it read.data. The runtime now follows the command’s declared types and fetches every upload it finds, at any depth. It reads the declaration, not the values. A mapping of your own that carries anupload_idkey still arrives as the client sent it.
reactor-runtime 3.3.0 · Per-session recording ids · Caller-supplied ICE
New
- The runtime fixes a session’s recording id when it accepts the start. A
session_idin the start parameters becomes the session’s own id. The runtime stores and addresses the recording under that id, and every log record the session writes carries it. Clips and logs then match the id the caller knows the session by. A start that names nosession_idgets a fresh id, so two sessions in one runtime process never write into the same recording directory. A rejected start leaves a live session’s id alone. The transport session id does not change. - A caller can supply a connection’s ICE credentials and port range. Beside
ice_servers, an offer to a connection’ssdp_paramstakes two more optional fields.ice_credentialsis aufragandpwdpair, andport_rangeis an inclusive[min, max]. Leave both out and the media engine makes its own credentials and uses the configured port range. That is the usual case. They exist for a deployment that puts a relaying layer in front of the runtime. That layer has to know a connection’s ICE credentials and media address before the connection exists. A single-port range pins the connection to one port.port_rangereplaces the configured range. It does not narrow it, soWEBRTC_PORT_RANGEdoes not bound what a caller asks for. Ask for a port another live connection already holds and the runtime refuses with409, instead of a negotiation that quietly fails later.
reactor-runtime 3.2.7 · Recording a batch's audio
Fixed
- A recording keeps every sample of a batch’s audio. The recorder’s audio buffer held one second of samples, whatever the size of the batch handed to it. A batch that carried more than a second lost its oldest samples. The recorder then paired the audio that was left with the batch’s first frames and filled the rest with silence. A clip lost most of its audio, and what remained ran ahead of the picture. Live playout kept every sample throughout. The buffer is now never smaller than the batch it takes, so every sample reaches the recording.
reactor-runtime 3.2.6 · Traceable logs · Full-rate video
New
- Write structured logs with
get_logger(). Import it fromreactor_runtime. Pass context as keyword arguments:logger.info("scene changed", prompt=self.prompt). A record prints as the message and then itskey=valuetokens. WhenREACTOR_LOG_FORMAT=jsonis set, each record prints as one JSON object per line. - Every record names the session and the phase it was written in. While a session is live, every record carries its id as
session_id. One filter then finds everything that run logged. The phase arrives at two levels of detail:state, the session’s own word for it, andruntime_state, a coarser one (loading,available,serving,terminated). The runtime stamps a record where it is written, not where it is made. A plainlogging.getLogger(__name__)and the libraries your model imports carry the same fields, so you never thread a session id through your own calls.
Fixed
- Video above 960x540 no longer stops at 2.5 Mbps. Each outbound video track ran on the encoder ceiling the transport derives from the frame size, which is 2500 kbps for a frame that large. A 720p or 4K stream held that rate on a fast link, and the picture stayed soft. The runtime now gives each video track a ceiling of 10 Mbps, the limit the connection’s bandwidth estimate already had. Set
WEBRTC_SENDER_MAX_KBPSto choose a different one.
reactor-runtime 3.2.5 · Nested recorder block · Every ICE candidate
Rolls up 3.2.4 and 3.2.5.
New
- The recorder’s settings nest under
runtime:. The runtime reads therecording:block asruntime.recording, besideruntime.importandruntime.config. A top-levelrecording:block still works, so an existing manifest needs no change. Declare the block in one place. A manifest that carries both takes the nested block whole. It reads nothing from the top-level one, so anenabled: trueleft up there leaves the recorder off. See Session Recording.
Fixed
- The end-of-candidates marker no longer fails a client’s ICE request. A client marks the end of its trickle-ICE candidates with an empty candidate string (RFC 8838). The runtime rejected that marker. The request that carried it failed, and the runtime dropped any candidate sent with it. The connection then had fewer network paths to try. The runtime now accepts the marker and ignores it.
reactor-runtime 3.2.3 · Sender capture time · Recording a batch
Rolls up 3.2.1 through 3.2.3.
New
- Read when the sender captured a frame.
InputFrame.capture_time_usholds the capture time the sender declared for that frame, in microseconds, orNonefor a frame that carried no stamp. A sender that declares nothing gets a clock reading from its transport, so most frames carry a stamp. A client that stamps several tracks from one clock reading puts that value on all of them, so a multi-camera capture reads as one moment. Unlikepts, the stamp reads another machine’s clock: two stamps from one sender differ by that sender’s own timing. Subtract a stamp from your own clock, and you mostly measure the offset between two clocks that drift apart. See Video & Audio Tracks.
Fixed
- A recording keeps every frame of a batch. The recorder’s feed queue held four frames, whatever the size of the batch handed to it. Anything larger waited on the encoder, and the frames past that wait never reached the recording. You saw the loss in the log as a full feed queue, and live playout kept every frame throughout. The queue is now never smaller than the batch it takes, so a whole batch fits.
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 Video & Audio Tracks.
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 Video & Audio Tracks. - 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.