Skip to content

fix(runway): warn and surface session status when avatar leaves before max_duration - #6901

Open
piyushrajyadav wants to merge 1 commit into
livekit:mainfrom
piyushrajyadav:fix/runway-warn-early-session-end
Open

fix(runway): warn and surface session status when avatar leaves before max_duration#6901
piyushrajyadav wants to merge 1 commit into
livekit:mainfrom
piyushrajyadav:fix/runway-warn-early-session-end

Conversation

@piyushrajyadav

Copy link
Copy Markdown

What this fixes

Closes #6703

When using runway.AvatarSession with max_duration=1800, the Runway server silently terminates sessions at ~300 s. The plugin correctly forwards maxDuration in the creation request but the response contains only an {"id": ...} — no echoed duration — so the applied server-side cap is invisible client-side. The avatar participant then leaves the room mid-session with no warning, and the AgentSession continues without video.

Changes

livekit-plugins/livekit-plugins-runway/livekit/plugins/runway/avatar.py

Tracking + flag (minimal state, no API surface change):

  • _session_started_at: float | Nonetime.monotonic() snapshot captured right after _create_session returns.
  • _session_ending: bool — set to True in both _on_agent_session_close and _ensure_end_session_task() so agent-initiated teardowns are distinguishable from unexpected server-side disconnects.

participant_disconnected listener registered on the room in start():

  • Filters to the avatar participant identity only.
  • No-ops if _session_ending is True (normal teardown path).
  • Logs a WARNING with elapsed_seconds, session_id, and (when max_duration was set and the gap is >60 s) a human-readable note that the server may have applied a lower cap.
  • Schedules _log_session_status() as a fire-and-forget task.

_log_session_status(session_id) (new async helper):

  • Calls GET /v1/realtime_sessions/{id} using the already-initialised HTTP session.
  • Logs the Runway-reported status, duration, and failureCode fields at WARNING so operators can see what Runway recorded without reading the issue tracker.
  • Errors are caught and logged at DEBUG — never raises, never blocks the session.

Why this approach

The issue's proposed solution asks for two things:

  1. A warning when elapsed << max_duration → covered by the participant_disconnected handler.
  2. Exposing the terminal status (status, duration, failureCode) from the GET endpoint → covered by _log_session_status.

Both are best-effort: they surface information for operators without changing any error paths or adding retry logic, keeping the change minimal and safe.

No-API-change guarantee

  • No new public methods or classes.
  • No new runtime dependencies (time is stdlib).
  • Existing teardown paths (_ensure_end_session_task, _end_runway_realtime_session, _cancel_runway_realtime_session) are unchanged in their async logic; only _session_ending = True is added before the task is created.

How it was tested

  • ruff check --fixAll checks passed!
  • ruff format1 file reformatted (only asyncio.ensure_future(...) call condensed to one line by the formatter), then re-checked → All checks passed!
  • The Runway plugin has no unit tests in the repo. The logic is straightforward event-listener and async-HTTP code following the same patterns as _cancel_runway_realtime_session. I have flagged that dedicated unit tests could mock the participant_disconnected event and the aiohttp GET call — happy to add them if the maintainers want to introduce a test harness for this plugin.

Flagging for maintainer review: The 60-second threshold for the "early end" warning is a judgment call. If Runway ever legitimately ends a session within 60 s of the requested limit (e.g. for very short sessions), this could produce a misleading warning. An alternative would be a percentage-based threshold (e.g. warn if elapsed < 50% of max_duration). I went with 60 s as an absolute floor because the repro shows 1500 s remaining, which is unambiguously wrong, but I have noted this in the code comment.

@piyushrajyadav
piyushrajyadav requested a review from a team as a code owner August 19, 2026 09:01
devin-ai-integration[bot]

This comment was marked as resolved.

@piyushrajyadav
piyushrajyadav force-pushed the fix/runway-warn-early-session-end branch from aed15bc to 7050285 Compare August 19, 2026 09:10
@piyushrajyadav

Copy link
Copy Markdown
Author

Thanks for the review, Devin — that's a legitimate catch.

Fixed in the force-push to the branch (7050285):

  • Added _pending_status_tasks: set[asyncio.Task[None]] to track all in-flight status lookups with a strong reference, preventing GC mid-flight.
  • Replaced both bare �syncio.ensure_future(...) calls with a new _schedule_log_session_status() helper that adds the task to the set and registers a done_callback to discard it on completion — the same bookkeeping pattern as _end_session_task.
  • In �close(), added �wait asyncio.gather(*self._pending_status_tasks, return_exceptions=True) so any status fetch started near shutdown completes and emits its log line before the session tears down.

uff check +
uff format both pass cleanly on the updated diff.

devin-ai-integration[bot]

This comment was marked as resolved.

…ration (livekit#6703)

Runway silently terminates realtime sessions at ~300 s even when the
plugin sends maxDuration=1800 at session-create time.  The creation
response contains only an id (no echoed duration) so the applied
server-side cap is invisible client-side.  The avatar participant then
disappears mid-session with no warning, leaving the AgentSession running
without video.

Changes
-------
* Track session wall-clock start time (	ime.monotonic()) after
  _create_session returns so elapsed time can be computed later.
* Introduce _session_ending: bool flag; set it eagerly in both
  _on_agent_session_close and _ensure_end_session_task() so that
  agent-initiated teardowns are clearly distinguishable from unexpected
  server-side disconnects.
* Register a participant_disconnected listener on the room.
  When the avatar participant leaves while _session_ending is False:
  - Log a WARNING with elapsed and (if set) requested max_duration.
  - When max_duration was requested and the disconnect happens >60 s
    early, a dedicated message calls out the likely server-side cap.
  - In both cases, fire an async task to GET /v1/realtime_sessions/{id}
    and log the Runway-reported status, duration, and ailureCode
    (new _log_session_status helper), matching the second half of the
    proposed solution in the issue.
* No new public API surface; no new runtime dependencies (	ime is
  stdlib).

Fixes livekit#6703
@piyushrajyadav
piyushrajyadav force-pushed the fix/runway-warn-early-session-end branch from 7050285 to e804311 Compare August 19, 2026 09:19
@piyushrajyadav

Copy link
Copy Markdown
Author

Addressed in e804311 (force-pushed). Three issues fixed in one pass:

1. Handler lifetime (this finding):

  • Converted the inline @room.on('participant_disconnected') closure to a proper named method _on_participant_disconnected on the class (matching the base class pattern where _on_connection_state_changed / _on_conversation_item_added are named methods registered/deregistered symmetrically).
  • In �close(), call self._room.off('participant_disconnected', self._on_participant_disconnected) before super().aclose() clears self._room — so no late events can spawn new tasks after deregistration.

2. Late task creation during shutdown window:

  • �close() now sets self._session_ending = True as its very first action, so even if a disconnect event somehow fires during the �syncio.shield(end_session_task) await, the handler will no-op.

3. �syncio.ensure_future -> �syncio.create_task (anticipated finding):

  • Replaced with �syncio.create_task(..., name=f'runway_log_session_status_{session_id}') to match the existing codebase convention (line 269 already uses create_task) and provide a descriptive task name for debug output.

Ordering in �close():

  1. _session_ending = True — handler no-ops on any late event

oom.off(...) — handler deregistered, no new invocations possible
3. _ensure_end_session_task() — end-session lifecycle runs
4. gather(*_pending_status_tasks) — drain any in-flight lookups (set is now stable since the handler can't add more)
5. _pending_status_tasks.clear() — clean state
6. super().aclose() — base class teardown

uff check +
uff format both pass clean.

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.

Runway plugin: maxDuration silently clamped to ~300s on realtime sessions (documented API limit is 30 min)

1 participant