Skip to content

feat: speko.ai router extensions (asr, llm2, tts2) - #2401

Closed
Ilmak17 wants to merge 17 commits into
TEN-framework:mainfrom
SpekoAI:feat/speko-router-extensions
Closed

Ilmak17 wants to merge 17 commits into
TEN-framework:mainfrom
SpekoAI:feat/speko-router-extensions

Conversation

@Ilmak17

@Ilmak17 Ilmak17 commented Nov 13, 2026

Copy link
Copy Markdown

Continues #2307 (Speko router extensions: speko_asr_python, speko_llm2_python, speko_tts2_python). Abat, the author of #2307, is away, so I'm taking it over from Speko's fork. His four commits are kept as they were, rebased onto current main, and the review fixes follow on top. Once this one is open, #2307 can be closed.

Reviewed against @nitin4real's review of 8163fb0a.

Review items

ASR (speko_asr_python)

  1. Upper bound for disconnected buffering.
    • buffer_duration_ms must be between 0 and 30000. A bad value fails init and latches as one fatal error, with no connection attempt.
    • The retained bytes are capped at 10 MiB whatever the sample rate or channel count.
    • On overflow, the oldest frames are dropped and recorded as dropped audio in the timeline. This is documented in the README.
    • Tests cover 30000, 30001 and -1, the byte cap at 48 kHz/8 ch and 192 kHz/8 ch, and dropping oldest frames into the timeline.
  2. No reconnect after a non-retryable setup failure.
    • A handshake error that the Router marks retryable=false is fatal. That covers an HTTP 400 on upgrade and an error before session.ready.
    • It is reported once, latched, and never reconnected. The latch, asr_error and connection_status all use one classification.
    • The regression is parametrized over HTTP 400 and a pre-ready error event. It asserts exactly one fatal error, one connect, and no reconnect task, even after more audio and trigger_connect.
    • A caller queued behind the failing handshake no longer emits a second connecting/disconnected pair.

LLM (speko_llm2_python)

  1. Malformed responses normalized.
    • Malformed 2xx JSON is normalized into SpekoRouterError, and so are {"error": "text"} envelopes over both HTTP and SSE.
    • Unexpected exceptions on the request path go through the same error report.
    • Tests assert exactly one TEN error with vendor_info.vendor == "speko" for each case.
    • on_stop now cancels in-flight requests and closes the client before the base stops.

TTS (speko_tts2_python)

  1. Deterministic audio-end accounting.
    • tests/test_audio_accounting.py drives known PCM through the real AsyncTTS2BaseExtension path, with a recording ten_env, a fake clock and a scripted client.
    • It covers success, partial audio then error, a non-final error, flush mid-stream, no-audio completion, and odd-sized chunks (4801 + 4799 B).
    • Each case asserts event order, exactly one matching tts_audio_end, duration from emitted bytes, and an interval from the matching tts_audio_start.
    • Two bugs this exposed are fixed:
      • Duration counted received bytes, not emitted ones. The base keeps odd trailing bytes back.
      • The interval was measured from the first audio chunk, not from tts_audio_start.
  2. Independent TTFB validation.
    • tts_guarder/tests/ttfb_hooks/speko.py registers a WebSocket observer in the current guarder TTFB infrastructure. It times the first non-empty input.append send and the first non-empty binary audio frame per request.
    • Setup frames (session.configure) are excluded, and so are control frames (commit/cancel/close) and all JSON events.
    • The endpoint is mapped from base_url inside the guarder.
    • It joins the shared hook tests, and adds speko cases for ignored control frames and for old-connection audio not completing a new request.
    • The client now stamps TTFB right before the text send and right after the first audio frame is received, so it matches the reference to within 1 ms.

Live guarder failures from your run

  • ASR finalize timed out without a final. The cause was on Speko's Router, not in the extension. With auto routing, en-US went to a provider whose realtime API has no client turn-commit and closes streams when audio stops. So input.commit sometimes got no transcript.final, and the session was then dropped.
    • The Router is fixed and deployed. Every input.commit now gets exactly one transcript.final, which is empty if nothing was pending. Silence between turns no longer ends a session.
    • The extension is also hardened: a commit that gets no final keeps the session and still sends exactly one asr_finalize_end, with no error.
  • TTS process exited in test_invalid_text_handling. An exception escaped on_stop while draining the Router socket, and TEN exits the process when that happens.
    • Stop now closes without draining, bounds the drain, treats a closed socket as closed, and never raises.
    • Whitespace-only chunks are skipped.

Rebased onto main with both guarder conflicts resolved: fish_audio_tts_python and speko_tts2_python are both kept, and shared files are not reformatted.

Validation (image ten_agent_build:0.7.14, live against router.speko.dev)

task tts-guarder-test EXTENSION=speko_tts2_python CONFIG_DIR=tests/configs
task asr-guarder-test EXTENSION=speko_asr_python CONFIG_DIR=tests/configs
Suite HEAD Result
TTS guarder 6ceb7a530 all 17 Speko tests passed: 71 passed, 10 skipped (2 subtitle alignment, which is disabled; 8 TTFB endpoint cases for other vendors), 5 failed (other vendors' guarder unit tests that need google/cosy modules missing from the image; main fails the same way)
ASR guarder 89c4bc849 12 passed, 1 deselected (test_long_duration_stream, deselected by the guarder's bin/start). This run pinned routing to soniox/stt-rt-v5, and every asr_finalize was acknowledged. The run with the default config (auto) will follow in a comment once the Router rollout finishes in every region.
Standalone 89c4bc849 ASR 82, LLM 22, TTS 57 passed
Format/lint 89c4bc849 black clean, pylint 10.00/10 on all three
  • test_sequential_requests_ttfb: the extension reported 184/199 ms, matching the guarder's independent reference within 1 ms.

  • TTS screenshot:

    TTS guarder

  • ASR screenshot:

    ASR guarder

  • TTS PCM output, from tests/configs/property_dump.json: speko_tts_test_interleaved_request_id_1.pcm (.wav). It is 24000 Hz, 16-bit signed little-endian, mono, 1,036,800 bytes (21.6 s).

Still to come: the quick check in the voice-assistant example (from your Slack note). I'll add the result here.

Open questions

  • In TTS, an error on a non-final chunk followed by a successful final chunk ends the request with REQUEST_END, and the error is reported separately before the end, as the base class does today. Say if you'd rather it end with ERROR.

idafoh and others added 11 commits November 12, 2026 23:18
pytest 8.4 removed PytestUnhandledCoroutineWarning, so the runner's -W filter
failed to parse with the newest pytest 8.x. pytest >= 8.4 fails unrun async
tests by itself; require it and drop the filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilures

- buffer_duration_ms must be 0..30000 and the disconnected buffer is capped
  at 10 MiB; overflow drops the oldest frames into the dropped-audio timeline.
- A non-retryable error from the handshake (HTTP 400 on upgrade, or an error
  event before session.ready with retryable=false) is fatal: one fatal error,
  latched, no reconnect task. One classification drives latch and report.
- A commit the Router does not acknowledge (provider already finalized the
  turn) keeps the session: warn, no asr_error, one asr_finalize_end, and the
  turn cursor advances as for an acknowledged commit. An empty
  transcript.final acknowledges the commit. Real disconnects still reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Wrap JSON decode failures of non-streaming responses in SpekoRouterError.
- Accept {"error": "text"} envelopes over HTTP and SSE.
- Backstop: any other request-path failure, including an invalid TEN request,
  is emitted as one TEN error with vendor_info before the request ends.
- on_stop cancels in-flight streams and closes the client before the base
  stops, and never raises.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d frames

- on_stop closes the session without draining and logs cleanup failures;
  an exception escaping on_stop made TEN exit the process (guarder
  test_invalid_text_handling).
- The close drain treats ConnectionClosed as closed and has a total bound.
- Whitespace-only chunks are not sent to the Router.
- tts_audio_end duration counts bytes emitted as frames and its interval runs
  from the request's tts_audio_start; a final-chunk error reports one
  disconnect; a close failure during shutdown never finishes as success.
- TTFB starts right before the input.append send and is rounded.
- tests/test_audio_accounting.py drives known PCM through the real base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpekoObserver times the first non-whitespace input.append frame to the first
nonempty binary audio frame per connection, excluding control frames and Router
JSON events. The guarder maps base_url to the Router TTS socket itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live guarder's provider-boundary reference saw TTFB 192.5 ms where the
extension reported 195 ms (1 ms tolerance). The arrival time was read after
asyncio.wait_for's task hop back to the caller, which can take milliseconds
under load. Take it in the receive coroutine, right after recv returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A caller queued on _connection_lock behind a handshake that failed
fatally passed the unlocked latch check, so the base start_connection
wrapper emitted a second connecting -> disconnected pair for the same
root cause. Re-check the latch under the lock.

Audio arriving before on_start also scheduled a reconnect that raced
the first handshake and spent retry budget. Schedule audio-driven
reconnects only after a connection has been requested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

[P1] Keep buffered ASR audio within its owning session
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:580

When frames from two session_ids arrive while disconnected, this requeues both into the same Router stream. The consumer can send both before the first asynchronous transcript arrives, at which point _send_transcript() uses the latest frame's metadata; speech from the old session can therefore be combined with or attributed to the new session. A disconnected finalize also acknowledges the old session without removing its buffered frames, so they can be replayed after that turn ended. The existing test injects a final immediately after each send and misses the real asynchronous ordering. Partition or discard buffered audio on session/finalize boundaries and test delayed results. This violates the ASR guide's MUST for session-safe buffering and blocks merge.

[P1] Do not infer a final's audio boundary from receive time
ai_agents/agents/ten_packages/extension/speko_asr_python/client.py:237

If the provider auto-finalizes the first utterance, but audio for the next utterance is sent before that final reaches the listener, _audio_generation already includes the new audio. The final is then credited to that generation, and the next commit() returns at line 168 without sending input.commit; pending speech may never receive a final. Track an actual vendor turn/audio boundary, or commit conservatively when it cannot be established. Add a test where the next frame is sent before the preceding final arrives. This violates the ASR finalization contract and blocks merge.

[P1] Supply passing current-head TTS guarder evidence
ai_agents/agents/ten_packages/extension/speko_tts2_python/manifest.json:3

This is a new TTS extension. The PR has no comments, and the body reports a TTS guarder run at 6ceb7a530, while the current head is 89c4bc849; that run also reports five failures. The required full-suite evidence therefore does not establish a passing run at this head. Please run the full guarder at the current head and post the command, final counts, screenshot, and a downloadable non-empty PCM output with its format in a PR comment. The existing screenshot and PCM links in the body do not satisfy the required current-head passing comment gate.

[P2] Isolate late finalize acknowledgements after a timeout
ai_agents/agents/ten_packages/extension/speko_asr_python/client.py:188

After a commit times out, its waiter is discarded while the same socket remains active. If a new turn starts and its commit is pending when the first commit's delayed transcript.final arrives, _listen() resolves the new waiter with the old final. The second asr_finalize_end can then precede its own final, and the late transcript can carry the next turn's metadata. Reconnect or otherwise correlate/drain acknowledgements before accepting another commit, and test a delayed first final arriving during the second commit. This violates the ASR finalize/session contract and blocks merge.

[P2] Treat normal WebSocket closure as a normal disconnect
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:564

The WebSocket iterator ends normally for a completed code-1000/1001 close, so _listen() calls on_disconnect(None). This path converts that into a relay_error and _reset_connection() emits an ASR error even though the close was normal. Report the disconnected state and apply recovery without emitting an error for normal closes; retain error reporting for ConnectionClosedError, including incomplete handshakes. The ASR guide makes this distinction a MUST and it blocks merge.

[P2] Keep transient ASR recovery available after a short outage
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:389

A transient outage lasting through five attempts permanently latches retry_exhausted after about 11.5 seconds of backoff plus connection time. Even if the service returns, subsequent audio and trigger_connect cannot recover without restarting the extension. The design baseline expects unlimited capped-backoff retries by default; its optional ceiling must be an explicit operator setting. Make the limit configurable with unlimited as the default, or document the vendor limitation, alternatives, and test evidence for this SHOULD deviation.

[P2] Wait for non-empty PCM before measuring TTS first audio
ai_agents/agents/ten_packages/extension/speko_tts2_python/client.py:195

A zero-length binary WebSocket frame consumes first_audio and emits TTFB before any audio exists. The extension sends tts_audio_start and the metric for that frame, while the independent observer intentionally ignores it; a later real audio frame has no corrected TTFB. Ignore empty binary frames before updating first_audio and cover an empty frame followed by non-empty PCM.

[P2] Write only emitted PCM to the TTS dump
ai_agents/agents/ten_packages/extension/speko_tts2_python/extension.py:282

The recorder writes received bytes before the base aligns frames. If a request ends or is interrupted with an odd trailing byte, the base retains that byte and _begin_request() discards it, but it remains in the dump. The resulting dump differs from the actual emitted audio and contains an incomplete sample. Write aligned emitted bytes to the recorder, and assert byte equality for an odd tail on completion and interruption.

ASR checklist

  • Lifecycle: pass for inspected stop/cleanup paths.
  • Connection state: fail — normal closes emit an error.
  • Buffering: fail — retained audio can cross session and finalize boundaries.
  • Finalize: fail — delayed finals and audio sent before an auto-final can complete the wrong turn or suppress a commit.
  • Reconnect: fail — fixed retry ceiling deviates from the default recovery policy without the required justification.
  • Result shape: pass for the inspected normal result fields; session attribution fails under the buffering/finalize races above.
  • Metrics: pass for inspected connect-delay and base-managed metrics paths.
  • Tests: fail — add the race and close regressions above; the PR also reports the default auto-route guarder run is still pending.

The base class emits TTLW only from a final asr_result that arrives
after asr_finalize. When the provider had already finalized the turn
(the client skips the commit) or the Router acknowledged the commit
with an empty final, the extension completed the finalize without a
result, so no TTLW was ever sent and the guarder's test_metrics timed
out on auto-routed sessions. Empty finals stay suppressed; instead,
an acknowledged commit that no final consumed reports TTLW as the
latency to that acknowledgement. A commit timeout or disconnect still
reports none.

Log the Router request_id, route and attempt_id on session.ready so
support reports can cite them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

[P2] Attribute finalize latency to the finalize session
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:530

When a commit is acknowledged without a non-empty final, this emits TTLW before switching self.metadata to _finalize_context. If the finalize session differs from the last audio session, the metric is therefore attached to the old session while asr_finalize_end is attached to the new one. The new test actually demonstrates this: it finalizes turn-one but asserts TTLW belongs to audio-session. Emit the metric with the saved finalize metadata and test the session alongside the finalize acknowledgement. The ASR guide requires turn metrics to carry the correct session; this MUST violation blocks merge.

[P2] Preserve array item schemas in tool definitions
ai_agents/agents/ten_packages/extension/speko_llm2_python/extension.py:322

For a tool with an array parameter, this copies only its type and description and drops parameter.items. MCP tools can supply that field, and the other LLM2 adapters preserve it. As a result, the Router/provider receives an array schema without its item type; providers that require items can reject the entire completion, and others lose the constraint needed to construct valid arguments. Forward the array's item schema and add a request-payload test for an MCP-style array parameter.

[P2] Cover actual PCM formats and request isolation in the TTS local suite
ai_agents/agents/ten_packages/extension/speko_tts2_python/tests/test_audio_accounting.py:58

The recording harness captures only frame lengths, and its exercised configuration is the default 24 kHz mono. The local suite does not establish that PCM bytes and declared frame format are correct for two supported configurations, or that rapidly interleaved request IDs retain their own audio and end events; recorder tests also mock the flush rather than compare the dump with emitted bytes. These are mandatory extension-local TTS review cases and can hide channel/rate or cross-request corruption even when duration arithmetic passes. Record the actual PCM and frame properties, exercise two output configurations and interleaved requests, and compare the real dump with emitted bytes on the terminal paths.

The findings in the previous review remain applicable at head 984db543608555e34d316764e84fe807a6267f8f, including the new-extension TTS guarder evidence gate: no passing full-suite run at this head with the required screenshot and PCM has been posted in a PR comment.

ASR checklist

  • Lifecycle: pass for the inspected stop and cleanup paths.
  • Connection state: fail — normal closes emit an error, as reported previously.
  • Buffering: fail — buffered audio can cross session and finalize boundaries.
  • Finalize: fail — uncorrelated finals can suppress or complete the wrong commit.
  • Reconnect: fail — the fixed retry ceiling lacks the required SHOULD-deviation justification.
  • Result shape: pass for the inspected normal fields; session ownership fails in the reported races.
  • Metrics: fail — acknowledged-finalize TTLW can use the wrong session.
  • Tests: fail — add the reported race, normal-close, and session-specific metric regressions; the default auto-route guarder run is still pending.

Ilmak17 and others added 3 commits November 13, 2026 06:41
…sion

Address the PR TEN-framework#2401 review of speko_asr_python:

- The Router answers every input.commit with exactly one transcript.final,
  in order. Commit whenever audio was sent since the last commit and pair
  finals with outstanding commits FIFO. A timed-out commit stays
  outstanding, so its late final is reported under its own turn and does
  not complete the next finalize. Finals with no commit outstanding are
  unsolicited provider results.
- Audio held while disconnected belongs to one session. A frame from a new
  session, or a finalize while disconnected, drops the older session's
  held frames and records them as dropped audio instead of replaying them
  into the next stream.
- A normal close (1000/1001, session.closed) reports disconnected and
  reconnects without an asr_error.
- Reconnect attempts are unlimited by default with backoff capped at 4 s.
  max_reconnect_attempts sets a ceiling that latches retry_exhausted.
- The acknowledged-finalize TTLW and finalize_end are reported under the
  finalize's own metadata and session_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Array tool parameters lost their items schema, so MCP-style tools with
array arguments reached the model without element types. Forward items
like the other LLM2 adapters do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A zero-length binary frame no longer starts audio or stamps TTFB.
- The dump holds exactly the PCM sent to the graph. The unaligned tail the
  base keeps as leftover is not written.
- The local suite records frame bytes and format. It covers two output
  configurations, back-to-back requests keeping their own audio, metadata
  and audio_end, and the real dump equal to the emitted PCM on
  completion, flush and error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

[P1] Preserve session ownership when replay meets queued audio
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:664

If session A has buffered frames and session B's frame is still in audio_frames_queue when session.ready arrives, this combines both sets into the replay queue. Because the client is already ready when the consumer processes them, the session check at lines 364–380 is skipped and both sessions' speech is sent to the same Router stream. If the transcript arrives after B's frame, it is attributed to B, and the provider may combine A and B's speech. Apply the session boundary policy to queued as well as buffered frames before replay, and cover a delayed transcript after both frames have been consumed. This violates the ASR guide's MUST for session-safe buffering and blocks merge.

[P1] Distinguish automatic finals from commit acknowledgements
ai_agents/agents/ten_packages/extension/speko_asr_python/client.py:268

The first transcript.final received while a commit is pending is always removed from the commit queue, even though the provider can independently auto-finalize. For example, an automatic final can already be in flight when input.commit is sent; it then completes the commit before the Router's actual commit final arrives. asr_finalize_end can precede the final for the committed audio, and the real acknowledgement can be attributed to a subsequent turn or satisfy its commit. FIFO ordering between commit responses does not distinguish an unsolicited final from a response. Use a protocol correlation/boundary or isolate the stream so the correct final is known, and test this ordering. This violates the ASR finalize/session MUST requirements and blocks merge.

[P1] Supply passing current-head TTS guarder evidence
ai_agents/agents/ten_packages/extension/speko_tts2_python/manifest.json:3

This is a new TTS extension, and the latest reported guarder run is for 6ceb7a530, not current head ce296e0dc086b34bd5b6f2b4aa54eb5dcbe06129; it also reports five failures. No PR comment supplies the required passing full-suite evidence at this head. Please post the exact command and final pass/fail/skip counts, an inline result screenshot, and a downloadable non-empty PCM output with its sample rate, bit depth, and channel count. This is the new-extension merge gate.

[P2] Recover when a timed-out commit never produces a final
ai_agents/agents/ten_packages/extension/speko_asr_python/client.py:213

The timeout deliberately leaves the abandoned commit in _pending_commits while keeping the socket usable. If that missing final never arrives, the next turn's genuine final is consumed as the abandoned commit's response, reported with the old turn's metadata, and the new finalize times out. Later commits remain one response behind, and repeated missing responses grow the queue. This is precisely the missing-ack condition the timeout is meant to handle; FIFO only works if every earlier response eventually arrives. Reset or otherwise resynchronize the connection after an unacknowledged commit, and test a permanently missing first final followed by a successful second turn. This violates the ASR finalize/session MUST requirements and blocks merge.

ASR checklist

  • Lifecycle: pass for the inspected stop and cleanup paths.
  • Connection state: pass for the inspected handshake and normal-close paths.
  • Buffering: fail — reconnect replay can mix queued and buffered sessions.
  • Finalize: fail — automatic finals and missing commit responses can complete or attribute the wrong turn.
  • Reconnect: pass for the inspected default retry and stop paths.
  • Result shape: pass for normal fields; session attribution fails in the races above.
  • Metrics: pass for the inspected finalize metadata and connect-delay paths; turn attribution is affected by the finalize races.
  • Tests: fail — cover the replay and commit-ordering cases above; the PR body also says the default auto-route ASR guarder run is pending.

Ilmak17 and others added 2 commits November 13, 2026 07:09
Address the third PR TEN-framework#2401 review of speko_asr_python:

- Each input.commit carries a fresh commit_id. On a Router that stamps
  answers (commit_ids in session.ready, or a first answer carrying its
  commit_id), only the final with a commit's own id answers it. A final
  without an id is the provider's own endpoint and is emitted as a normal
  result, so an auto-final in flight when a commit is sent no longer
  completes that commit. A late answer to a timed-out commit is reported
  under that turn's metadata and resolves no other commit. A permanently
  missing answer shifts nothing, so the session is kept.
- A Router that does not stamp answers still pairs them in order. There a
  commit timeout raises a plain error and the session is reset, so later
  answers cannot be paired one behind.
- On session.ready, frames still queued are held to the same session
  boundary as the disconnected buffer before replay. An older session's
  held audio is recorded as dropped instead of sharing the stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Router advertises "commit_ids": true in session.ready when it stamps
every commit's answer with its commit_id. That flag alone now selects
correlated mode; without it, answers pair in order and a commit timeout
resets the session. The guessing from the first answer is removed, so an
auto-final in flight at a session's first commit can no longer flip the
session to in-order pairing.

A test covers one provider final answering two pending commits: an empty
answer for the older id, then the words for the newest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

[P1] Enforce the session boundary when only queued audio exists
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:670

If frames for sessions A and B are queued during the handshake but the consumer has not yet moved any frame into buffered_frames when session.ready arrives, this condition skips the entire session filter. The client is already ready, so the consumer also skips the disconnected-session check at lines 364–380 and sends both frames into one Router stream. A delayed transcript can combine the speech and be attributed to B. Apply the session boundary policy to queued frames even when the retained buffer is empty, and test that ordering. This violates the ASR guide's session-safe buffering MUST and blocks merge.

[P1] Do not treat an untagged final as a commit answer before capability is known
ai_agents/agents/ten_packages/extension/speko_asr_python/client.py:327

When session.ready does not advertise commit_ids, _correlated is None. If an automatic provider final is already in flight when the first commit is sent, this fallback consumes it as the commit answer, even if the Router will echo the ID on the actual answer. Finalize then ends before the committed audio's final, and an older untagged Router can also pair later turns with the wrong final. FIFO ordering of commit answers cannot distinguish an unsolicited final. Require an explicit correlation capability or use a safe stream boundary for uncorrelated Routers; add the in-flight auto-final case without the readiness flag. This violates the finalize/session MUST and blocks merge.

[P1] Supply passing current-head TTS guarder evidence
ai_agents/agents/ten_packages/extension/speko_tts2_python/manifest.json:3

This is a new TTS extension. The latest reported run is for 6ceb7a530, rather than head 12082c6f925f614a86eaaa341c87b5d9d04a839c, and reports five failures. No PR comment contains the required passing full-suite evidence for the current head. Please post the exact command and final pass/fail/skip counts, an inline result screenshot, and a downloadable non-empty PCM file from that run with its sample rate, bit depth, and channel count. This is the new-extension merge gate.

[P2] Keep late finalize timestamps within the original turn
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:757

After a correlated commit times out, its late final may arrive after audio from the next turn has been sent. For a final without segments, these lines use the current global cursor and total audio duration, not the saved commit boundary: for example, one second of turn A followed by one second of turn B yields a late A result starting at 1000 ms with B's 1000 ms duration. It also advances _final_cursor_ms to the end of B, so B's own unsegmented result can have zero duration. Save the turn's timeline bounds with the pending commit and use them for its late result; test both results' timestamps. This violates the ASR result/timeline MUST and blocks merge.

[P2] Exercise truly interleaved request IDs in the TTS local suite
ai_agents/agents/ten_packages/extension/speko_tts2_python/tests/test_audio_accounting.py:427

The new request-isolation test sends two complete requests (r1 final, then r2 final); it does not exercise appended input interleaved across IDs, such as r1 non-final, r2 final, then r1 final. The mandatory extension-local matrix requires that case to establish that pending text, PCM, metadata and end events remain with their request. Add a deterministic test through the real extension path with distinct PCM for the segments and assertions for both request IDs.

ASR checklist

  • Lifecycle: pass for inspected stop and cleanup paths.
  • Connection state: pass for inspected handshake and normal-close paths.
  • Buffering: fail — queued frames can bypass session filtering when the retained buffer is empty.
  • Finalize: fail — an unsolicited final can complete a commit before correlation is established.
  • Reconnect: pass for inspected default retry and stop paths.
  • Result shape: fail — late unsegmented finals can carry another turn's time range.
  • Metrics: pass for inspected connect-delay and finalize metadata paths; turn attribution depends on finalize ordering.
  • Tests: fail — add the queue, unadvertised-correlation, and late-timestamp regressions above; the PR body reports that the default auto-route guarder run is still pending.

@github-actions

Copy link
Copy Markdown

[P1] Enforce the session boundary when only queued audio exists
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:670

If frames for sessions A and B queue during the handshake before the consumer moves any frame into buffered_frames, this condition skips the session filter. Once ready, the consumer also skips the disconnected-session check and sends both sessions into one Router stream; a delayed transcript can combine the speech and be attributed to B. Apply the session boundary policy to queued frames even when the retained buffer is empty, and test a delayed transcript after replay. This violates the ASR session-safe buffering MUST and blocks merge.

[P1] Do not pair uncorrelated finals with commits by arrival order
ai_agents/agents/ten_packages/extension/speko_asr_python/client.py:329

When session.ready does not advertise commit_ids, an automatic provider final may already be in flight when input.commit is sent. It is then consumed as the commit response, so asr_finalize_end can precede the final for the committed audio and the real response can be attributed to a later turn. FIFO ordering of commit responses cannot distinguish an unsolicited final. Require explicit correlation or establish a safe stream boundary for uncorrelated Routers, and test an in-flight automatic final without the readiness flag. This violates the ASR finalize and session ownership MUST requirements and blocks merge.

[P1] Supply passing current-head TTS guarder evidence
ai_agents/agents/ten_packages/extension/speko_tts2_python/manifest.json:3

This is a new TTS extension. The PR body reports a guarder run at 6ceb7a530 with five failures, not a passing full-suite run at head 74ab5b59d732438c67e8c2119823821b9b286b76. No PR comment supplies the required current-head evidence. Please post the exact command and final pass/fail/skip counts, an inline screenshot of the successful run, and a downloadable non-empty PCM file from that run with sample rate, bit depth, and channel count. This is a new-extension merge gate.

[P2] Keep late finalize timestamps within the original turn
ai_agents/agents/ten_packages/extension/speko_asr_python/extension.py:757

After a correlated commit times out, its final can arrive after audio from the next turn has been sent. For a final without segments, these lines use the current global cursor and total audio duration instead of the saved commit boundary: one second of A followed by one second of B can produce an A result starting at 1000 ms with B's duration. It also advances the cursor to the end of B, so B's own result can have zero duration. Save the turn's timeline bounds with the pending commit and use them for its late result; test both results' timestamps. This violates the ASR timeline MUST and blocks merge.

[P2] Exercise interleaved request IDs in the TTS local suite
ai_agents/agents/ten_packages/extension/speko_tts2_python/tests/test_audio_accounting.py:427

This test queues two complete requests; it does not cover appended input interleaved across IDs, such as r1 non-final, r2 final, then r1 final. The mandatory extension-local matrix requires this case to establish that text, PCM, metadata, and end events stay with their requests. Add a deterministic test through the real extension path with distinct PCM for the segments and assertions for both IDs.

ASR checklist

  • Lifecycle: pass for the inspected stop and cleanup paths.
  • Connection state: pass for the inspected handshake and normal-close paths.
  • Buffering: fail — queued frames can bypass session filtering.
  • Finalize: fail — an unsolicited final can complete an uncorrelated commit.
  • Reconnect: pass for the inspected default retry and stop paths.
  • Result shape: fail — late unsegmented finals can carry another turn's time range.
  • Metrics: pass for inspected connect-delay and finalize metadata paths; turn attribution depends on finalize ordering.
  • Tests: fail — cover the queue, uncorrelated-final, and late-timestamp cases above; the PR body also reports that the default auto-route ASR guarder run is pending.

@Ilmak17

Ilmak17 commented Nov 13, 2026

Copy link
Copy Markdown
Author

Moved back to #2307: Abat gave me push access to his fork, so the same commits (head 74ab5b59d) are on that branch now and the review continues there.

@Ilmak17 Ilmak17 closed this Nov 13, 2026
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