Skip to content

fix(voice): only the primary AgentSession creates a SessionHost for a shared room#1934

Open
rodriguescarson wants to merge 1 commit into
livekit:mainfrom
rodriguescarson:fix/multi-session-shared-room-session-host
Open

fix(voice): only the primary AgentSession creates a SessionHost for a shared room#1934
rodriguescarson wants to merge 1 commit into
livekit:mainfrom
rodriguescarson:fix/multi-session-shared-room-session-host

Conversation

@rodriguescarson

Copy link
Copy Markdown

Fixes #1927

Problem

Running multiple AgentSession instances bound to the same Room within a single job — the pattern from Python's multi-user-transcriber example, one listen-only STT session per remote participant — fails on the second session.start({ room }):

Error: A byte stream handler for topic "lk.agent.session" has already been set.
    at Room.registerByteStreamHandler (@livekit/rtc-node/dist/room.js:671:13)
    at RoomSessionTransport.start (@livekit/agents/dist/voice/remote_session.js:38:15)

Every session with a room created a RoomSessionTransport + SessionHost, and Room allows a single byte stream handler per topic, so the second registration throws and the job dies. record: false avoids 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 in start() for recording purposes but didn't use it for the session host.

This PR threads isPrimary from start() into _startImpl() and gates SessionHost creation on it. Secondary sessions get full RoomIO (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 _enableRecording was true).

Testing

  • New agents/src/voice/agent_session_shared_room.test.ts: a fake room mirroring rtc-node's one-handler-per-topic behavior; verifies a secondary start({ 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.
  • Full agents/src/voice/ suite passes (36 files, 315 tests).
  • pnpm build:agents, ESLint, Prettier clean. Changeset included (patch).

🤖 Generated with Claude Code

… 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-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fcff079

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 35 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

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

@CLAassistant

CLAassistant commented Jul 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +645 to +653
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@rodriguescarson

Copy link
Copy Markdown
Author

Good flag, and confirmed intentional: this brings the JS behavior to parity with the Python SDK. In agent_session.py the SessionHost/RoomSessionTransport is created only under if is_primary: ("only the primary session can have a session host"), so a non-primary session there also has no SessionHost and _on_amd_prediction no-ops the same way.

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 SessionHost, which is what caused the second session's RoomSessionTransport to re-register the room-wide lk.agent.session byte stream handler and throw. If a future use case needs remote-session comms on a secondary session, that would be a new feature in both SDKs rather than a regression here.

@rodriguescarson

Copy link
Copy Markdown
Author

Thanks for the review. This intentionally mirrors the Python SDK's is_primary pattern: in the shared-room / multi-user-transcriber use case, secondary sessions are listen-only and the primary session owns the SessionHost (byte-stream registration, remote-session protocol, AMD prediction). Gating SessionHost creation on isPrimary matches that behavior — a secondary session needing remote-session communication would be a different use case than what is_primary models. Happy to adjust if the JS side intends different semantics here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple AgentSession.start({ room }) in one Job fails: lk.agent.session byte stream handler already set

2 participants