Skip to content

Reusable Realtime Session across Handoffs & Agent Tasks#1193

Merged
toubatbrian merged 10 commits intobrian/reuse-sttfrom
brian/reuse-realtime
Apr 10, 2026
Merged

Reusable Realtime Session across Handoffs & Agent Tasks#1193
toubatbrian merged 10 commits intobrian/reuse-sttfrom
brian/reuse-realtime

Conversation

@toubatbrian
Copy link
Copy Markdown
Contributor

No description provided.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 1, 2026

🦋 Changeset detected

Latest commit: 4b4f251

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

This PR includes changesets to release 22 packages
Name Type
@livekit/agents-plugin-phonic Patch
@livekit/agents Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero 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

@toubatbrian toubatbrian changed the base branch from main to brian/reuse-stt April 1, 2026 22:45
@chatgpt-codex-connector
Copy link
Copy Markdown

💡 Codex Review

if (signal) {
const abortPromise = waitForAbort(signal);
while (true) {
const result = await Promise.race([reader.read(), abortPromise]);

P1 Badge Handle pre-aborted signals before entering read loop

readStream races reader.read() against waitForAbort(signal), but waitForAbort only resolves on a future abort event. If the signal is already aborted when readStream starts (which can happen when cancellation wins a task startup race during pause/close), the abort branch never resolves and the generator keeps waiting on stream reads instead of exiting promptly, so cancellation can hang until the source ends.


taskFn: (abortController: AbortController) =>
this.realtimeSayTask(handle, text, {}, abortController),

P2 Badge Preserve addToChatCtx behavior in realtime say path

The new realtime say branch drops the addToChatCtx option by calling realtimeSayTask without passing it. Unlike the TTS path, realtime generation always inserts an assistant message into chat context, so session.say(..., { addToChatCtx: false }) no longer works for realtime-without-TTS configurations and will unexpectedly mutate conversation history.


if (this.detachRequested) {
this.detachRequested = false;
return;

P1 Badge Make detach state pump-local during source handoff

detachRequested is shared across all pump instances. During rapid detach+reattach, a newly attached pump can reach finally first (for example, if the new source ends immediately), clear this flag, and then the old detached pump will run the normal close path and close the shared writable side. That can terminate the newly attached stream unexpectedly and drop post-handoff audio.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…#1220)

Co-authored-by: Qiong Zhou Huang <qiong@phonic.co>
Co-authored-by: Brian Yin <brian.yin@livekit.io>
devin-ai-integration[bot]

This comment was marked as resolved.

@toubatbrian toubatbrian merged commit 379e79a into brian/reuse-stt Apr 10, 2026
1 of 2 checks passed
@toubatbrian toubatbrian deleted the brian/reuse-realtime branch April 10, 2026 01:28
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 17 additional findings in Devin Review.

Open in Devin Review

this.closeCurrentGeneration({ interrupted: false });
return this.startNewAssistantTurn({ userInitiated: true });
this.pendingGenerateReplyFut = new Future<llm.GenerationCreatedEvent>();
this.sendGenerateReply(instructions, requestId);
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.

🔴 Floating promise in Phonic generateReply can cause indefinite caller hang

In generateReply(), this.sendGenerateReply(instructions, requestId) at line 486 is called without await, void, or .catch(). The sendGenerateReply method is async and can throw after the await this.readyToStart.await (e.g., if this.socket.sendGenerateReply(...) at line 505 throws because the socket just closed). If this happens, the thrown error becomes an unhandled promise rejection AND this.pendingGenerateReplyFut is never resolved or rejected, causing the caller of generateReply() (which awaits this.pendingGenerateReplyFut.await at line 488) to hang indefinitely.

Prompt for agents
In plugins/phonic/src/realtime/realtime_model.ts, the generateReply method at line 486 calls this.sendGenerateReply(instructions, requestId) without awaiting or catching the returned promise. The sendGenerateReply method is async and can throw after its internal await (e.g. if socket.sendGenerateReply throws). This leaves pendingGenerateReplyFut unresolved, hanging the caller forever.

The fix should wrap the socket send inside sendGenerateReply in a try/catch that rejects pendingGenerateReplyFut on error. Alternatively, the call in generateReply could be changed to something like:

void this.sendGenerateReply(instructions, requestId).catch((error) => {
  if (this.pendingGenerateReplyFut && !this.pendingGenerateReplyFut.done) {
    this.pendingGenerateReplyFut.reject(error);
    this.pendingGenerateReplyFut = undefined;
  }
});

Either approach ensures that if sendGenerateReply fails, pendingGenerateReplyFut is rejected so the caller does not hang indefinitely.
Open in Devin Review

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

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.

2 participants