fix(voice): only the primary AgentSession creates a SessionHost for a shared room#1934
Conversation
… shared room
Multiple AgentSession.start({ room }) calls in one job (e.g. one listen-only
transcriber session per remote participant) failed on the second start with
'A byte stream handler for topic "lk.agent.session" has already been set.'
Every session created a RoomSessionTransport, and a room allows a single byte
stream handler per topic.
Gate SessionHost creation on the primary-session designation, mirroring the
Python SDK's is_primary gate in AgentSession.start (agent_session.py).
Fixes livekit#1927
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: fcff079 The changes in this PR will be included in the next version bump. This PR includes changesets to release 35 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| if (isPrimary) { | ||
| // Only the primary session can have a session host: its transport | ||
| // registers the room-wide `lk.agent.session` byte stream handler, and | ||
| // a room allows a single handler per topic. Mirrors Python's | ||
| // `is_primary` gate so multiple sessions can share one room. | ||
| const transport = new RoomSessionTransport(room, this._roomIO); | ||
| this.sessionHost = new SessionHost(transport); | ||
| this.sessionHost.registerSession(this); | ||
| } |
There was a problem hiding this comment.
🚩 Secondary sessions lose SessionHost features beyond just byte-stream registration
The isPrimary gate at agents/src/voice/agent_session.ts:645-653 prevents secondary sessions from getting a SessionHost entirely — not just the byte stream handler. This means secondary sessions won't receive remote session protocol messages (e.g. from client SDKs via RemoteSession), and _onAmdPrediction at agents/src/voice/agent_session.ts:933 will silently no-op. This mirrors the Python is_primary pattern and is likely intentional for the multi-user-transcriber use case (secondary sessions are listen-only), but it's worth confirming that no secondary session scenario requires remote session communication.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Good flag, and confirmed intentional: this brings the JS behavior to parity with the Python SDK. In Secondary sessions are listen-only (the multi-user-transcriber pattern), so remote-session protocol messaging is correctly a primary-only feature. Before this change JS tried to give every session a |
|
Thanks for the review. This intentionally mirrors the Python SDK's |
Fixes #1927
Problem
Running multiple
AgentSessioninstances bound to the sameRoomwithin a single job — the pattern from Python's multi-user-transcriber example, one listen-only STT session per remote participant — fails on the secondsession.start({ room }):Every session with a
roomcreated aRoomSessionTransport+SessionHost, andRoomallows a single byte stream handler per topic, so the second registration throws and the job dies.record: falseavoids the primary recording conflict but not the transport conflict.Fix
Port the Python SDK's gate: in
agent_session.py's_start_impl, the session host is only created for the primary session (if is_primary: # only the primary session can have a session host). The JS port already computes the primary designation instart()for recording purposes but didn't use it for the session host.This PR threads
isPrimaryfromstart()into_startImpl()and gatesSessionHostcreation on it. Secondary sessions get fullRoomIO(audio input, transcription output per participant) but no remote-session transport — same semantics as Python, where multi-participant transcription works today.Also mirrors Python's demotion ordering: a secondary session is now marked non-primary regardless of whether recording is enabled (previously the demotion branch was only entered when
_enableRecordingwas true).Testing
agents/src/voice/agent_session_shared_room.test.ts: a fake room mirroring rtc-node's one-handler-per-topic behavior; verifies a secondarystart({ room })no longer throws and the handler is registered exactly once, plus primary-restart keeps its designation. Fails with the exact production error without the fix.agents/src/voice/suite passes (36 files, 315 tests).pnpm build:agents, ESLint, Prettier clean. Changeset included (patch).🤖 Generated with Claude Code