Skip to content

Conversation

@cshape
Copy link
Contributor

@cshape cshape commented Jan 16, 2026

Inworld has a maximum of 20 connections and 5 contexts per websocket connection, so the plugin has been updated to support this.

Summary by CodeRabbit

  • New Features

    • Configurable TTS options: max concurrent connections and idle timeout; shared connection pool with prewarm and graceful shutdown.
    • Synthesis and streaming reuse per-session contexts and support chunked streaming for long inputs.
  • Performance Improvements

    • Lower per-request overhead via pooled connections, session-wide context reuse, automatic idle cleanup, and improved concurrency/error handling.
  • Breaking Changes

    • TTS constructor/API now accepts max_connections and idle_connection_timeout parameters.

✏️ Tip: You can customize this high-level summary in your review settings.

@davidzhao
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Shared WebSocket-based Inworld TTS pool added: manages multiple connections and per-connection contexts with waiter-based capacity signaling, outbound/inbound loops, context lifecycle (create/send/flush/close), idle eviction, and integration into TTS public API.

Changes

Cohort / File(s) Summary
Inworld TTS pooling & runtime
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
Added _ConnectionPool, _InworldConnection, _ContextInfo, _ContextState, outbound message types, _send_loop/_recv_loop, per-context waiters, capacity checks, context acquisition/release, error propagation, and idle eviction.
TTS API & streams refactor
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
Updated TTS.__init__ signature to accept max_connections and idle_connection_timeout; replaced direct WebSocket usage with lazy _get_pool(), made prewarm() initialize pool, aclose() close pool; refactored ChunkedStream and SynthesizeStream to acquire pooled contexts, chunk text (1000 chars), reuse context per session, and route send/flush/close through pool.
Constants / types / imports
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
Exposed DEFAULT_MAX_CONNECTIONS, DEFAULT_IDLE_CONNECTION_TIMEOUT; expanded typing/imports (Enum, field, time, Callable, etc.) to support pool behavior and timeouts.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TTS
    participant Pool as ConnectionPool
    participant Conn as InworldConnection
    participant Inworld as InworldAPI

    Client->>TTS: synthesize(text)
    TTS->>Pool: acquire_context()
    alt capacity available
        Pool->>Conn: assign existing connection/context
    else create or wait
        Pool-->>TTS: wait for capacity
        Pool->>Conn: create new connection if under max_connections
    end
    Pool-->>TTS: return context_id & connection

    loop chunked text (<=1000 chars)
        TTS->>Conn: send_text(chunk)
        Conn->>Inworld: WebSocket send
        Inworld-->>Conn: audio frames/events
        Conn-->>TTS: deliver frames/events to stream
    end

    TTS->>Conn: flush_context()
    Conn->>Inworld: flush message
    Inworld-->>Conn: final frames
    TTS->>Conn: close_context()
    Conn->>Pool: release context capacity
    Pool->>Pool: signal waiting acquirers / evict idle
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

"I nibble bytes and weave a net so wide,
Connections pooled where small voices hide.
Chunks hop through tunnels, contexts reuse,
The rabbit hums as streams peruse,
Hop, synth, and sing — the pool's delight!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Inworld websocket improvements' is vague and generic. While it relates to changes in the Inworld TTS plugin involving WebSocket infrastructure, it does not clearly convey the main accomplishment: implementing a connection pool to manage multiple WebSocket connections and contexts within Inworld API limits. Consider a more specific title such as 'Add connection pool for Inworld TTS to manage websocket constraints' or 'Implement Inworld connection pool with idle cleanup and capacity signaling' to better communicate the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (3)

897-902: Fire-and-forget task for prewarm.

The prewarm() method creates a task without awaiting or storing a reference. If _prewarm_impl raises an exception, it will only be logged by asyncio's default handler. This is acceptable for warm-up behavior, but consider storing the task reference if you want to handle failures explicitly.


1092-1092: Consider making the extra timeout configurable.

The hardcoded + 60 seconds on the waiter timeout provides buffer for server-side processing, but this magic number could be confusing for maintainers. Consider extracting it to a constant or making it configurable.


267-267: Prefer asyncio.get_running_loop() over asyncio.get_event_loop().

get_event_loop() is deprecated in Python 3.10+ when called from a coroutine. Since this code runs inside an async method, use asyncio.get_running_loop().create_future() instead.

Suggested fix
-                    waiter: asyncio.Future[None] = asyncio.get_event_loop().create_future()
+                    waiter: asyncio.Future[None] = asyncio.get_running_loop().create_future()
📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dea1f26 and 760a2ca.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🔇 Additional comments (10)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (10)

94-110: LGTM!

The context state management is well-structured. The addition of close_started_at properly addresses the stale-context cleanup timing issue.


112-135: LGTM!

Clean message type definitions for the outbound queue pattern.


137-218: LGTM!

The reservation system (reserve_capacity/release_reservation) with _pending_acquisitions properly prevents over-subscription when the pool lock is released during context acquisition. The has_capacity property correctly accounts for both active contexts and reserved slots.


235-298: LGTM!

The acquire_context method properly addresses previous review concerns:

  1. Lost-wakeup race fixed by moving clear() after wait() returns (line 291)
  2. Closed connection guards added at entry and after waking (lines 250-251, 257-258, 294-295)
  3. The comment at line 262-263 correctly explains why context_count is used instead of has_capacity

313-323: LGTM!

The close_context method now properly sets close_started_at timestamp, which enables correct stale-context cleanup timing.


406-419: LGTM!

The error handling now properly releases stuck contexts and signals capacity when a server error occurs, preventing pool exhaustion.


462-494: LGTM!

Both cleanup mechanisms properly address past review concerns:

  1. _cleanup_stale_contexts uses close_started_at for accurate timing and resolves waiters before eviction
  2. _handle_connection_error now wakes local waiters and notifies the pool, preventing blocked callers from timing out unnecessarily

575-646: LGTM!

The pool acquisition logic properly addresses all previous review concerns:

  1. Context acquisition happens outside the pool lock (line 616), preventing head-of-line blocking
  2. Closed connections are pruned before capacity checks (line 585)
  3. Reservation is released on exception (line 619)
  4. Failed new connections are removed from pool and closed (lines 621-625)
  5. Lost-wakeup race fixed by clearing event after wait returns (line 642)

662-668: LGTM!

The idle cleanup condition now correctly accounts for connections already queued for removal (len(self._connections) - len(connections_to_close) > 1), ensuring at least one connection remains in the pool.


1091-1108: LGTM!

The exception handling now properly calls close_context on all error paths, addressing the previous review concern about context leaks. The finally block ensures tasks are cancelled and resources are cleaned up.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py`:
- Around line 102-108: The stale-context cleanup uses created_at so long-lived
streams get evicted immediately after close_context and their waiter futures
stay unresolved; add a close_started_at timestamp to _ContextInfo, set it in
close_context, update the cleanup logic to base eviction age on close_started_at
when present (fallback to created_at), and when evicting a context ensure any
waiter (tts._ContextInfo.waiter) is resolved (e.g., set_result(None) if not
done) so upstream timeouts don't occur; also make sure the existing
contextClosed handling still resolves the waiter if it arrives before cleanup.
- Around line 522-543: Before checking capacity and creating a new connection,
prune closed entries from self._connections so closed connections don't count
toward self._max_connections; iterate self._connections, remove or filter out
conn where conn._closed is True, then perform the existing capacity loop and the
len(self._connections) < self._max_connections check. Update references in this
block that create _InworldConnection, call conn.acquire_context, and use
notify_capacity_available so behavior is unchanged except closed connections are
excluded.
- Around line 212-257: The acquire_context loops have a lost-wakeup race because
they call self._context_available.clear() before awaiting
self._context_available.wait(); move the clear() call to after wait() returns so
a set() that happens between the capacity check and the wait won't be lost.
Update both _InworldConnection.acquire_context and
_ConnectionPool.acquire_context: remove/relocate the pre-wait clear() and call
clear() only after asyncio.Event.wait() completes (or equivalently clear
immediately after the awaited wait_for returns) while preserving the timeout
logic and exception handling.
- Around line 365-372: When status.code != 0 in the response-handling block, you
must release the stuck creation context instead of leaving it in self._contexts
in CREATING state; after setting ctx.waiter exception
(ctx.waiter.set_exception(error)) call the cleanup path (e.g., invoke
close_context(ctx.id) or transition ctx.state to ContextState.CLOSING and remove
it from self._contexts) and then signal capacity (call the existing
_signal_capacity() or equivalent) so the pool no longer counts the failed
context toward capacity. Ensure you reference the same symbols used in the file:
ctx, ctx.waiter, self._contexts, close_context (or ContextState.CLOSING) and
_signal_capacity when implementing the fix.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de72c6e and 641ddf0.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (2)
livekit-agents/livekit/agents/utils/misc.py (2)
  • shortuuid (21-22)
  • is_given (25-26)
livekit-agents/livekit/agents/_exceptions.py (1)
  • APIError (14-42)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@cshape
Copy link
Contributor Author

cshape commented Jan 19, 2026

@davidzhao made small updates in response to coderabbit review. re-tested both the normal agent convo flow and connection pool performance and looks good.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py`:
- Around line 1037-1039: The generic exception handler logs the error and raises
APIConnectionError but does not call close_context, which can leak context_id in
the pool; update the except Exception as e block (where logger.error and raise
APIConnectionError() from e occur) to call close_context(context_id) before
raising (ideally in a finally-like cleanup or immediately after logging), and
ensure close_context is itself protected (e.g., swallow/log any errors from
close_context) so the original exception is re-raised as APIConnectionError.
- Around line 548-561: The new connection is appended to self._connections
before calling _InworldConnection.acquire_context, so if acquire_context raises
the pool retains a failed connection; either defer appending until after ctx
acquisition succeeds or catch exceptions around acquire_context, remove the conn
from self._connections and close it. Specifically, create the _InworldConnection
with session/_ws_url/_authorization, then call await
conn.acquire_context(emitter, opts, remaining_timeout) inside a try block; on
success append conn to self._connections and log; on exception ensure you call
conn.close()/cleanup, do not append (or remove) and re-raise or return the
error. Ensure notify_capacity_available usage remains unchanged.
🧹 Nitpick comments (5)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (5)

159-160: Consider bounding the outbound queue to prevent memory growth.

The _outbound_queue is unbounded. If the WebSocket send rate is slower than the producer rate (e.g., during network issues), the queue can grow indefinitely. Consider setting a reasonable maxsize (e.g., 1000) to apply backpressure.

♻️ Suggested change
-        self._outbound_queue: asyncio.Queue[_OutboundMessage] = asyncio.Queue()
+        self._outbound_queue: asyncio.Queue[_OutboundMessage] = asyncio.Queue(maxsize=1000)

833-838: prewarm doesn't actually establish a connection.

The current implementation only creates the pool object but doesn't establish any WebSocket connection until the first acquire_context call. If the intent is to reduce latency on first synthesis, consider having prewarm actually create and connect at least one connection.

♻️ Suggested enhancement
 async def _prewarm_impl(self) -> None:
-    # Just ensure the pool is created - first acquire will establish a connection
-    await self._get_pool()
+    pool = await self._get_pool()
+    # Create one connection to reduce latency on first synthesis
+    conn = _InworldConnection(
+        session=self._ensure_session(),
+        ws_url=self._ws_url,
+        authorization=self._authorization,
+        on_capacity_available=pool.notify_capacity_available,
+    )
+    await conn.connect()
+    async with pool._pool_lock:
+        pool._connections.append(conn)

1028-1028: Consider extracting the magic number 60 to a named constant.

The + 60 buffer on the waiter timeout is not immediately clear. Consider defining a constant like SYNTHESIS_TIMEOUT_BUFFER = 60.0 to document its purpose.


1015-1019: _mark_started() is called for every chunk; consider calling once.

_mark_started() is called inside the loop for each 1000-char chunk. If this method has side effects or is not idempotent, it may cause unexpected behavior. Consider moving it outside the inner loop or guarding with a flag.

♻️ Suggested fix
         async def _send_task() -> None:
+            started = False
             async for ev in sent_tokenizer_stream:
                 text = ev.token
                 # Chunk to stay within Inworld's 1000 char limit
                 for i in range(0, len(text), 1000):
                     connection.send_text(context_id, text[i : i + 1000])
-                    self._mark_started()
+                if not started:
+                    self._mark_started()
+                    started = True
                 connection.flush_context(context_id)
             connection.close_context(context_id)

235-235: Use asyncio.get_running_loop() instead of get_event_loop() in async context.

asyncio.get_event_loop() emits a DeprecationWarning in Python 3.10+ when called from a coroutine. Since acquire_context() is an async function, use asyncio.get_running_loop().create_future() instead.

♻️ Suggested fix
-                    waiter: asyncio.Future[None] = asyncio.get_event_loop().create_future()
+                    waiter: asyncio.Future[None] = asyncio.get_running_loop().create_future()
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 641ddf0 and 8c59a4d.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: type-check (3.9)
  • GitHub Check: type-check (3.13)
  • GitHub Check: unit-tests
🔇 Additional comments (6)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (6)

21-25: LGTM!

The new imports (time, field, Enum) are appropriate for the connection pool infrastructure.


94-135: LGTM!

The context state tracking and message types are well-structured. The addition of close_started_at properly addresses the stale-context cleanup concern from prior review.


424-440: LGTM on stale context cleanup implementation.

The cleanup logic now correctly uses close_started_at for timing (falling back to created_at), and resolves the waiter before eviction. This addresses the prior review feedback.


588-615: LGTM!

The idle connection cleanup logic properly keeps at least one connection, checks periodically, and closes connections outside the lock to avoid blocking the pool.


759-770: LGTM!

The lazy pool initialization with proper locking and closed-pool detection is well implemented.


855-862: LGTM!

The shutdown sequence properly closes streams first, then the pool, ensuring clean resource cleanup.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py`:
- Around line 442-448: When a connection errors, in addition to failing each
context (ctx.waiter) and setting self._closed, also wake any local waiters and
the pool capacity event so callers can retry/create new connections: after
clearing self._contexts and setting self._closed in _handle_connection_error, if
the instance has a pending waiter attribute (e.g., self._waiter or similar) and
it is not done, set_exception(error) on it, and if the pool-level
asyncio.Event/condition named _capacity_available exists, set/notify it (e.g.,
call its set() or notify_all) so pool callers blocked on capacity are unblocked
to observe the closed connection and create a replacement.
- Around line 225-259: The acquire_context flow can enqueue a create on a dead
connection; before creating the _ContextInfo and after waking from
self._context_available.wait(), check self._closed (and/or any WS-closed flag)
and if true raise an immediate connection-closed error instead of proceeding;
specifically update the block guarded by self._acquire_lock (around has_capacity
/ ctx creation) to bail early when self._closed, and add the same self._closed
guard right after the wait/clear so waiters fail fast rather than enqueuing
_CreateContextMsg or returning a waiter on a closed connection.
- Around line 530-567: Holding self._pool_lock while awaiting
conn.acquire_context causes head-of-line blocking; instead, inside the lock
select or construct the candidate connection(s) but do not call await while
locked: pick an existing conn or instantiate _InworldConnection (with session,
ws_url, authorization, on_capacity_available=self.notify_capacity_available) and
store it in a local variable, then release the lock and call await
conn.acquire_context(emitter, opts, remaining_timeout) outside the lock; after
the await succeeds, re-acquire the lock to append a newly created connection to
self._connections (and set self._cleanup_task if needed) or to prune closed
connections as before, and ensure you close and cleanup the conn on exception.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c59a4d and 0aac452.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (3)
livekit-plugins/livekit-plugins-cartesia/livekit/plugins/cartesia/tts.py (2)
  • _TTSOptions (63-82)
  • _ensure_session (223-227)
livekit-agents/livekit/agents/utils/misc.py (2)
  • shortuuid (21-22)
  • is_given (25-26)
livekit-agents/livekit/agents/_exceptions.py (3)
  • APITimeoutError (91-95)
  • APIConnectionError (84-88)
  • APIError (14-42)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (1)

1-25: Fix Ruff formatting to pass CI.

The pipeline indicates that Ruff formatting check failed. Run ruff format to fix code style issues before merging.

ruff format livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py`:
- Around line 621-648: The idle-cleanup loop in _cleanup_idle_connections can
remove every connection because the check uses the original self._connections
length; change the logic so you compute the remaining pool size accounting for
already-selected closures (e.g., remaining = len(self._connections) -
len(connections_to_close)) and only append a conn to connections_to_close if
remaining > 1 (or remaining >= 1 depending on intent) to guarantee at least one
connection remains; keep the rest of the removal and aclose behavior the same
and reference _cleanup_idle_connections, self._connections,
connections_to_close, conn.is_idle, conn.last_activity, and self._idle_timeout
when making the change.
🧹 Nitpick comments (1)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (1)

273-296: Consider removing dead QueueFull exception handling or adding queue bounds.

The outbound queue (line 160) is created without a maxsize, making it unbounded. The except asyncio.QueueFull handlers will never execute since put_nowait() only raises QueueFull for bounded queues.

Either:

  1. Remove the try/except and use put_nowait() directly (if unbounded is intentional), or
  2. Add a maxsize to the queue if you want backpressure protection.
Option 1: Remove dead code (if unbounded is intentional)
     def send_text(self, context_id: str, text: str) -> None:
         """Queue text to be sent to a context."""
-        try:
-            self._outbound_queue.put_nowait(_SendTextMsg(context_id=context_id, text=text))
-        except asyncio.QueueFull:
-            logger.warning("Outbound queue full, dropping text")
+        self._outbound_queue.put_nowait(_SendTextMsg(context_id=context_id, text=text))
 
     def flush_context(self, context_id: str) -> None:
         """Queue a flush message for a context."""
-        try:
-            self._outbound_queue.put_nowait(_FlushContextMsg(context_id=context_id))
-        except asyncio.QueueFull:
-            logger.warning("Outbound queue full, dropping flush")
+        self._outbound_queue.put_nowait(_FlushContextMsg(context_id=context_id))
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0aac452 and cfeffc3.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (2)
livekit-agents/livekit/agents/_exceptions.py (3)
  • APIConnectionError (84-88)
  • APITimeoutError (91-95)
  • APIError (14-42)
livekit-agents/livekit/agents/utils/misc.py (1)
  • shortuuid (21-22)
🪛 GitHub Actions: CI
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py

[error] 1-1: Ruff formatting check failed. 1 file would be reformatted. Run 'ruff format' (or 'uv run ruff format --fix .') to fix code style issues.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: unit-tests
🔇 Additional comments (12)
livekit-plugins/livekit-plugins-inworld/livekit/plugins/inworld/tts.py (12)

94-109: LGTM!

The _ContextState enum and _ContextInfo dataclass properly track context lifecycle. The close_started_at field correctly addresses the previous concern about stale-context cleanup using the wrong timestamp.


112-135: LGTM!

Clean message type definitions for the outbound queue.


137-196: LGTM!

The _InworldConnection class is well-structured with clear capacity management (MAX_CONTEXTS = 5), proper synchronization primitives, and useful properties for pool management.


197-271: LGTM!

The connection establishment and context acquisition logic correctly addresses previous concerns:

  • Closed-state guards at lines 228-229, 235-236, and 268-269 prevent hanging waiters.
  • The clear() after wait() pattern at lines 263-265 fixes the lost-wakeup race.
  • Atomic capacity check with _acquire_lock ensures thread-safe context creation.

298-353: LGTM!

The send loop correctly processes all message types and propagates errors to _handle_connection_error on failure.


355-434: LGTM!

The receive loop correctly handles all message types, including proper cleanup and capacity signaling on errors (lines 388-392) and context closures (lines 423-427). The previous concern about stuck contexts on server errors is addressed.


436-452: LGTM!

The stale context cleanup correctly uses close_started_at (falling back to created_at) for eviction timing, and properly resolves waiters before eviction. This addresses the previous concern about long-lived streams being evicted prematurely.


454-467: LGTM!

The error handler correctly fails all waiters, clears contexts, and signals both local waiters and the pool to enable recovery. This addresses the previous concern about waiters timing out when connections die.


469-491: LGTM!

Clean shutdown implementation that properly cancels tasks, fails waiters, and closes the WebSocket.


690-803: LGTM!

The TTS class correctly integrates the connection pool with new configuration parameters and lazy initialization via _get_pool(). The docstrings clearly document the new max_connections and idle_connection_timeout parameters.


866-895: LGTM!

The prewarm() implementation correctly initializes the pool lazily, and aclose() properly cleans up pool resources.


1030-1077: LGTM!

The stream implementation correctly:

  • Acquires contexts from the connection pool (lines 1030-1035)
  • Chunks text to stay within Inworld's 1000-character limit (lines 1048-1050)
  • Calls close_context() on all error paths (lines 1063, 1066, 1072), addressing the previous concern about context leaks

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Member

@davidzhao davidzhao left a comment

Choose a reason for hiding this comment

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

nice work!

@davidzhao davidzhao merged commit dcc9c2f into livekit:main Jan 21, 2026
9 checks passed
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