Skip to content

runway: realtime avatar sessions are hard-killed at 5 minutes — plugin should support session renewal (or document the cap) #6427

Description

@dev-nesoiai

Environment

  • livekit-agents 1.6.5, livekit-plugins-runway 1.6.5 (Python 3.13, LiveKit Cloud)
  • Runway gwm1_avatars realtime sessions, custom Character, LiveKit agent owns STT/LLM/TTS (ElevenLabs)

Problem

Runway enforces a hard 5-minute limit on realtime avatar sessions at runtime. Their Characters "Core Concepts" docs state "Sessions have a maximum duration of 5 minutes" — and it is enforced, even though POST /v1/realtime_sessions happily accepts maxDuration up to 1800 (that bound appears to be billing-only; values >1800 are rejected with 400 "Too big: expected number to be <=1800").

Measured timeline from a live session:

  • AvatarSession.start() → video track subscribed +5.4s (nice warm start)
  • avatar lip-syncs a real conversation flawlessly
  • +301s: the runway-avatar-agent participant disconnects mid-utterance — Runway completed the session server-side

Because the plugin routes agent audio into the avatar's DataStreamAudioOutput, two things follow for anyone running conversations longer than 5 minutes:

  1. The avatar silently disappears mid-conversation.
  2. The in-flight utterance is lost — TTS frames already handed to the dying session's stream never play, and subsequent frames go to a dead sink until the app intervenes. There is no plugin-level signal that the session expired (vs. any other disconnect).

Neither the plugin docs nor the LiveKit avatar integration page mention the cap, so this surfaces as a mystery disconnect in production. Two other integration notes that cost us time: AvatarSession.start() returns right after the POST (it does not wait for the worker to join, and first-ever provisioning can exceed 30s), and max_duration > 1800 fails session creation outright.

Workaround we built (works, but everyone will have to reinvent it)

Using the hot-swap machinery from #5863 (AgentOutput.replace_audio_tail, shipped in 1.6.x):

  1. Proactive rotation: from ~240s of session age, wait for session.current_speech to be idle, then start a second runway.AvatarSession under the same participant identity. The old worker keeps rendering through the new session's ~5s provisioning; when the new worker joins, the server drops the old one — visible gap ≈ track switchover, zero speech loss. aclose() the old session to stop billing.
  2. Reactive fallback: if the kill lands mid-speech anyway (no idle boundary in minutes 4–5), respawn on participant_disconnected and prompt the LLM to continue from the cut-off, since the utterance remainder is unrecoverable.

Proposal

Any (or all) of:

  1. auto_renew: bool (or renew_before: float) on runway.AvatarSession — the plugin rotates the underlying realtime session before the provider cap, ideally at a speech boundary, using the same identity-takeover + replace_audio_tail mechanics as fix(avatar): preserve audio wrappers across avatar hot-swaps #5863. The plugin already owns every primitive needed.
  2. A typed event (e.g. avatar_session_expired) distinguishing provider-side session completion from network loss, so apps can implement renewal themselves without sid-tracking heuristics.
  3. At minimum, document the 5-minute cap (and the maxDuration ≤ 1800 validation + "start() returns before the worker joins") on the runway plugin page — it currently reads as if sessions last as long as max_duration.

Happy to share more logs/timings or test a patch — we have a working environment that reproduces this on every session >5 min.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions