Skip to content

fix(sarvam, simplismart): close the streams the STT collects - #7012

Open
Rehansanjay wants to merge 1 commit into
livekit:mainfrom
Rehansanjay:fix/stt-aclose-tracked-streams
Open

fix(sarvam, simplismart): close the streams the STT collects#7012
Rehansanjay wants to merge 1 commit into
livekit:mainfrom
Rehansanjay:fix/stt-aclose-tracked-streams

Conversation

@Rehansanjay

Copy link
Copy Markdown

Found by reading the plugins rather than from a report, so there is no issue to close.

The leak

Both sarvam and simplismart give every stream its own aiohttp session:

# Create a fresh session for this stream to avoid conflicts
stream_session = aiohttp.ClientSession()

and only SpeechStream.aclose() closes it (simplismart at line 428, sarvam at 1100).

Both also collect their streams:

self._streams = weakref.WeakSet[SpeechStream]()
...
self._streams.add(stream)

But neither STT class defines aclose(). In sarvam/stt.py the only aclose in the file belongs to SpeechStream (line 1056, with class SpeechStream at 868); simplismart is the same shape. So STT.aclose() resolves to the abstract no-op on the base class, those streams are never closed, and their sessions stay open — aiohttp reports unclosed client sessions and the sockets survive until the objects happen to be collected.

Why this reads as an omission rather than a decision

The WeakSet is the tell. In both files it is initialised, added to, and then never read again — the shape of cleanup that was intended and not written. I checked every plugin that keeps a _streams set, and these two are the only ones where nothing reads it.

Everywhere else it is read:

  • cartesia, palabra, telnyx — close through it in aclose
  • rime (TTS) — for s in list(self._streams): await s.aclose()
  • sarvam's own stt_streaming.py — already has this exact method at line 466, a few hundred lines from the file this PR touches
  • the STT plugins that don't close through it (deepgram, assemblyai, azure, …) read it to propagate update_options

The base contract asks for it as well — STT.aclose is documented as "Close the STT, and every stream/requests associated with it".

The change

Eleven lines in each plugin, mirroring stt_streaming.py:

async def aclose(self) -> None:
    for stream in list(self._streams):
        await stream.aclose()
    self._streams.clear()

Neither STT owns a session of its own to close — sarvam resolves through utils.http_context.http_session() — so the streams are the whole of it.

Verification, and its limits

ruff check and ruff format --check pass on both files at the repo's line-length = 100 / target-version = py310, and both compile.

Being straight about what I did not do: I have not run the test suite locally, and there is no test in this PR. The correctness argument here is that stream() creates a session, only SpeechStream.aclose() closes it, and nothing was calling it — plus five existing implementations of the same method in this repo to match. If you would like a regression test, tell me where it belongs and I will add one.

@Rehansanjay
Rehansanjay requested a review from a team as a code owner August 27, 2026 10:54
@CLAassistant

CLAassistant commented Aug 27, 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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@Rehansanjay

Copy link
Copy Markdown
Author

recheck

Both plugins give every stream its own aiohttp session:

    # Create a fresh session for this stream to avoid conflicts
    stream_session = aiohttp.ClientSession()

and only SpeechStream.aclose() ever closes it. Both also collect their
streams in a weakref.WeakSet, but neither STT class defines aclose(), so
STT.aclose() resolves to the abstract no-op on the base class and those
sessions are never closed. aiohttp then reports unclosed client sessions and
the sockets stay open until the objects are collected.

The WeakSet is the tell: in both files it is initialised and added to and then
never read again, which is the shape of cleanup that was intended and not
written. Every other plugin that keeps a _streams set reads it - cartesia,
palabra and telnyx close through it, rime does the same for TTS, and
sarvam's own stt_streaming.py already has exactly this method a few hundred
lines away.

The base contract asks for it too: STT.aclose is documented as "Close the STT,
and every stream/requests associated with it".

Found by reading the plugins rather than from a report, so there is no issue
to close.

Written with AI assistance (Claude Code); reviewed and verified before opening.
@Rehansanjay
Rehansanjay force-pushed the fix/stt-aclose-tracked-streams branch from 9572437 to 6838386 Compare August 27, 2026 17:35
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