feat(realtime): bundle initial_state into livekit_join#155
Conversation
Send the initial reference image/prompt inside the livekit_join message (initial_state) instead of as a separate set_image/prompt request after livekit_room_info arrives. The ack is still awaited (armed without re-writing) so the readiness gate is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| write: false, | ||
| }); | ||
| this.config.observability?.endPhase("initial-state-handshake", { success: true }); | ||
| if (!ack.success) throw new Error(ack.error ?? `Failed: ${request.label}`); |
There was a problem hiding this comment.
Observability marks failed ack success
Low Severity
In flushInitialState, endPhase("initial-state-handshake", { success: true }) runs before checking ack.success. When the server returns a failed set_image_ack or prompt_ack, telemetry still records a successful initial-state handshake even though the method then throws.
Reviewed by Cursor Bugbot for commit fc7cc3e. Configure here.
commit: |
The bundled livekit_join flow arms the set_image_ack/prompt_ack listener only after livekit_room_info (flushInitialState), and handleMessage drops any ack with no matching listener. So a bouncer that emits the bundled ack before room_info would have it silently dropped -> handshake timeout. Buffer set_image_ack/prompt_ack received while not yet connected (before room_info) and let request() claim a buffered ack when its listener arms. The ack timeout still starts only after room_info, so the queue-wait protection is unchanged. Buffer is cleared on teardown. Refs API-1167, API-827 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
initial_state presence is the bouncer's signal that the client is the capability-bearing SDK that buffers an early set_image_ack/prompt_ack, so the bouncer can ack a bundled set_image/prompt without waiting for livekit_room_info. Always send the field (null when there is no initial state) so the marker is unambiguous. Refs API-1167, API-827 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f49f547. Configure here.
| if (!this.connected && (msg.type === "set_image_ack" || msg.type === "prompt_ack")) { | ||
| this.bufferedAcks.push(msg); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Stale buffered acks poison requests
Medium Severity
While connected is still false, every set_image_ack and prompt_ack is appended to bufferedAcks, but leftover entries are not cleared after the initial-state handshake. Later request() calls (including setImage) resolve from the first buffered match and can skip writeMessage, so a duplicate or unrelated early ack can make a later image send appear successful without hitting the wire.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f49f547. Configure here.
… after testing) Adds a bundleInitialState option (default true) threaded connect -> StreamSession -> openAndJoin. When false, the join omits initial_state and the set_image/prompt (including the publisher bootstrap null set_image) is sent as its own message after room_info — the old two-step flow. A "Legacy connection protocol" checkbox in the demo index.html drives it, to verify the new bouncer stays backwards-compatible with old SDK versions. Revert after testing. Refs API-1167 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
491f7ad to
14f9600
Compare
…— revert after testing)" This reverts commit 14f9600.


What
Send the initial reference image/prompt inside the
livekit_joinmessage asinitial_state, instead of as a separateset_image/promptrequest fired afterlivekit_room_infoarrives.Wire change:
The inference server now gets the reference image/prompt with the join — no extra client round-trip waiting on
livekit_room_info.How
LiveKitJoinMessagegains an optionalinitial_statefield.buildInitialStateRequest()produces a single reusable{ message, matchAck }from the caller'sinitialState.request()gains awrite?flag so the ack can be armed without re-sending the payload. The initial-state ack is still awaited after room info, so the readiness gate behaves exactly as before — only the message's position on the wire moved.sendInitialState/sendInitialStateTrackedtwo-step path.Scope
Isolated to the wire format:
signaling-channel.ts, theLiveKitJoinMessagetype, and the affected unit tests. No error-verbosity, playground, or media-channel changes from the source branch were carried over.Test
vitest run unit— 210 passed.tsc --noEmitandbiome checkclean.🤖 Generated with Claude Code
Note
Medium Risk
Changes the realtime signaling wire contract and handshake timing; server must understand bundled
initial_state, though client-side connect gating behavior is intended to stay the same.Overview
Realtime join now sends reference image/prompt inside
livekit_joinasinitial_state(ornullas a capability marker), instead of a follow-upset_image/promptafterlivekit_room_info.buildInitialStateRequest()maps callerInitialStateto the nested wire message;request()gains optionalwrite: falseso the client can wait for the ack without resending. Earlyset_image_ack/prompt_ackmessages are buffered until after room info arms the waiter, preserving queue-timeout behavior and the existinginitialStateAckreadiness gate.Reviewed by Cursor Bugbot for commit 521d5cb. Bugbot is set up for automated code reviews on this repo. Configure here.