fix(actor-plugin): live sessionEvent streaming over the RivetKit actor path#1541
Merged
NathanFlurry merged 1 commit intoJun 26, 2026
Conversation
Member
Author
|
Stack for rivet-dev/agentos
Get stack: |
|
🚅 Deployed to the agentos-pr-1541 environment in agentos
🚅 Deployed to the agentos-pr-1541 environment in rivet-frontend
|
…r path
The published 0.2.1 quickstart streamed ZERO sessionEvents (conn.on("sessionEvent")).
Root-caused three distinct bugs in the native actor plugin, each blocking the next:
1. actor_loop starvation (WS-setup timeout). The single event loop ran
ensure_vm().await inline on the first action; a cold VM boot (>5s) starved the
queued ConnOpen, so RivetKit-core failed connection setup at 5000ms
("actor websocket connection setup timed out after 5000 ms") and no events
could flow. Fix: move VM-touching work (Action/Http/Sleep/Destroy) onto a
dedicated serial worker task; the loop now answers connection-lifecycle events
inline and never blocks on the VM.
2. Subscribe event rejected. RivetKit delivers an AbiEventTag::Subscribe to the
plugin when a client subscribes to an event name; the loop had no arm, so it
hit the generic "event not supported" reply, REJECTING every subscription. With
no subscription registered, broadcast() (which filters on is_subscribed) routed
zero events to the connection. Fix: accept Subscribe inline (reply_ok).
3. sessionEvent broadcast missing + mis-encoded. spawn_event_capture only persisted
each event to SQLite and never broadcast it. Added the broadcast, encoded as the
RivetKit event wire expects: a CBOR array of handler arguments
([{ "event": <notification> }]) so the client `(data) => data.event` listener
receives it. Also fixed vmBooted/vmShutdown, which had the same JSON-instead-of-
CBOR encoding bug.
Verified locally against the published 0.2.1 npm packages with AGENTOS_PLUGIN_BIN
override + a real Anthropic key: 13 live session/update events stream end-to-end.
Known residual: a timing race remains between subscription registration and the
event burst (reliable under normal/slower timing, ~50% under rapid back-to-back
connections). The action reaches the plugin ~before the subscribe, and the engine
spawns actions without blocking, so this appears to live in the RivetKit
client/engine subscribe-vs-action ordering (preview dep), not the plugin.
7d4cf86 to
4c2b9bb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The published 0.2.1 quickstart streamed ZERO sessionEvents (conn.on("sessionEvent")).
Root-caused three distinct bugs in the native actor plugin, each blocking the next:
actor_loop starvation (WS-setup timeout). The single event loop ran
ensure_vm().await inline on the first action; a cold VM boot (>5s) starved the
queued ConnOpen, so RivetKit-core failed connection setup at 5000ms
("actor websocket connection setup timed out after 5000 ms") and no events
could flow. Fix: move VM-touching work (Action/Http/Sleep/Destroy) onto a
dedicated serial worker task; the loop now answers connection-lifecycle events
inline and never blocks on the VM.
Subscribe event rejected. RivetKit delivers an AbiEventTag::Subscribe to the
plugin when a client subscribes to an event name; the loop had no arm, so it
hit the generic "event not supported" reply, REJECTING every subscription. With
no subscription registered, broadcast() (which filters on is_subscribed) routed
zero events to the connection. Fix: accept Subscribe inline (reply_ok).
sessionEvent broadcast missing + mis-encoded. spawn_event_capture only persisted
each event to SQLite and never broadcast it. Added the broadcast, encoded as the
RivetKit event wire expects: a CBOR array of handler arguments
([{ "event": }]) so the client
(data) => data.eventlistenerreceives it. Also fixed vmBooted/vmShutdown, which had the same JSON-instead-of-
CBOR encoding bug.
Verified locally against the published 0.2.1 npm packages with AGENTOS_PLUGIN_BIN
override + a real Anthropic key: 13 live session/update events stream end-to-end.
Known residual: a timing race remains between subscription registration and the
event burst (reliable under normal/slower timing, ~50% under rapid back-to-back
connections). The action reaches the plugin ~before the subscribe, and the engine
spawns actions without blocking, so this appears to live in the RivetKit
client/engine subscribe-vs-action ordering (preview dep), not the plugin.