Skip to content

voice-agents-python: the Python runtime holds frames during a resume, fails lost tool calls, counts unknown session frames - #6

Merged
arbianst merged 1 commit into
mainfrom
voice-agents/render-44694f3
Nov 1, 2026
Merged

arbianst merged 1 commit into
mainfrom
voice-agents/render-44694f3

Conversation

@sent-dm-sdk-pipeline

Copy link
Copy Markdown
Contributor

Rendered by the Voice Agents lane in sent-dm-sdk-pipeline from sent-dm-voice-protocol at v0.2.0, after https://github.com/sentdm/sent-dm-sdk-pipeline/commit/44694f3a0577f5a9f88164609f62ea379a9fdf06.

What a reviewer checks: this repository's CI, the conformance job included, is green on this PR; the generated frames module and the runtime changed as the protocol or the template did; nothing release-please owns moved. Merging makes release-please open the release.

@sent-dm-sdk-pipeline
sent-dm-sdk-pipeline Bot requested a review from a team as a code owner November 1, 2026 14:50
@sent-dm-sdk-pipeline sent-dm-sdk-pipeline Bot changed the title voice-agents-python: a hand render can name its subject and its language voice-agents-python: the Python runtime holds frames during a resume, fails lost tool calls, counts unknown session frames Nov 1, 2026
@sent-dm-sdk-pipeline
sent-dm-sdk-pipeline Bot force-pushed the voice-agents/render-44694f3 branch from 0d691de to 85a58e9 Compare November 1, 2026 14:51
@arbianst
arbianst merged commit b846596 into main Nov 1, 2026
4 checks passed
@arbianst
arbianst deleted the voice-agents/render-44694f3 branch November 1, 2026 14:53

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

for session in pending:
await self._resume(connection, session)
await self._safely(self.on_connected(frame))
self._spawn(self.on_connected(frame))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 (optional) An agent that sets up state in on_connected can now have on_session_start run before that setup finishes. On the base branch, setup always finished first. _ready now spawns on_connected on a background task. The reader then dispatches the session.start that Sent sends right after connection.ready. That session's task starts while on_connected is still awaiting. This follows from the stated goal of keeping handlers off the reader. Fix: keep the reader unblocked but state the new ordering in the on_connected docstring and README handler table. Alternatively, make session tasks started on that connection await the spawned on_connected task before on_session_start.

Why this was flagged

On the base branch, _ready awaited self._safely(self.on_connected(frame)) on the reader. No later frame on that connection was read until on_connected returned, so no session.start could create a session task before then. Now agent.py:273 calls self._spawn(self.on_connected(frame)), and _read (agent.py:240) goes straight to the next frame. If that frame is session.start, _start (agent.py:289-293) creates the session task immediately. Its on_session_start runs as soon as on_connected yields at any await, such as a config fetch or opening a client. A handler that reads state on_connected was still preparing now sees missing or stale state, for example an AttributeError logged by _safely and a session with no greeting. Nothing orders the two tasks: _spawn returns no handle the session path waits on. The on_connected docstring ("A connection to Sent is ready for sessions.") and README still suggest it runs before sessions on that connection.

Verification: On the base, _ready ended with await self._safely(self.on_connected(frame)). Now agent.py:273 calls self._spawn(self.on_connected(frame)), and _read goes straight on to the next socket.recv(). If that frame is session.start, _start (agent.py:289-293) creates _run_session at once. on_session_start then sees whatever on_connected has not set up yet.

Comment on lines 245 to +250
elif isinstance(frame, frames.Ping):
await connection.send(frames.Pong(ts=frame.ts))
# A connection that lived through a heartbeat interval is a good one: the next loss starts the
# backoff over. A connection that dies before its first ping keeps it growing.
if connection.ready:
self._attempt = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 nit (optional): release-please will publish a changelog line that does not describe this change. The commit subject is "fix(sdk): a hand render can name its subject and its language". The diff actually changes the handshake timeout, the reconnect backoff, resume buffering, audio during a resume, and lost-session behaviour. Fix: reword the fix: subject so a customer can read what changed (e.g. resume and connection-loss handling), and split unrelated changes per "One logical change per PR".

Why this was flagged

CLAUDE.md lines 27-28 say the Conventional Commit subject becomes the changelog line customers read. REVIEW.md item 7 asks for a fix: subject a customer can read. Commit 0d691de's subject names a hand render and a subject/language, but no file in the diff touches rendering, subject or language. The diff touches agent.py (timeout, backoff at agent.py:245-250, background handlers), session.py (resume gating, lost flag) and frames.py. Once merged, release-please writes the misleading line into CHANGELOG.md and the release notes.

Verification: nit: the misleading changelog line appears once the PR merges and release-please cuts the release. The PR range has one commit, 0d691de fix(sdk): a hand render can name its subject and its language. Nothing in the diff deals with rendering, a subject or a language. The PR title has no prefix, so release-please takes only this subject, against CLAUDE.md:27-28.

Comment on lines +335 to 339
if frame.frame_type != "session.resume" and frame.error.code not in ("resume_failed", "connection_full"):
return
session._resuming = False
connection.sessions.pop(session.id, None)
if frame.error.code == "connection_full":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟣 pre-existing, not blocking: pre-existing, partly fixed: after a connection_full refusal, an audio session's PCM can still reach another caller on the full connection. _error clears _resuming but leaves session._connection on the full connection. So send_audio sends with the old connection's stream number, which may belong to another call there. Fix: keep the session gated until _resume reattaches it on the new connection, for both send_audio and _send (keep _resuming set, or add a detached flag both check). The new session.py:98 guard only covers an unanswered resume.
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

_resume (agent.py:275-281) sets session._connection = connection and _resuming = True, then sends session.resume. Sent answers with error code connection_full because that connection already carries max_sessions. _error (agent.py:335-340) sets session._resuming = False, pops the session and calls _open([session], 0.0), but leaves session._connection on the full, still-open connection. Until the new connection's ready, the session task's send_audio passes the new _resuming check at session.py:98. It then calls send_bytes(audio_frame(self.stream, ...)) on the full connection with the stream number assigned by the old connection. The diff's own comment at session.py:99-100 notes that number may be another caller's there. Base sent this audio for the whole resume window. The diff closes the unanswered-resume part, but this window still behaves exactly as base.

Verification: pre-existing (security-relevant). Trigger: Sent refuses a session.resume with connection_full. _error (agent.py:337-342) runs session._resuming = False. It never detaches session._connection. An audio frame carries only that stream number, so Sent routes the PCM to whichever session on the full connection owns it, which can be another caller. The base send_audio has no gate at all.

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