Skip to content

feat(asr): add stepfun asr - #2364

Open
diyuyi-agora wants to merge 28 commits into
mainfrom
feature/yuyidi/step_asr
Open

diyuyi-agora wants to merge 28 commits into
mainfrom
feature/yuyidi/step_asr

Conversation

@diyuyi-agora

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@diyuyi-agora

Copy link
Copy Markdown
Contributor Author

ASR design review(re-review @ 06a6db7)

针对上一轮 P1:recovery flush 在无锁下改写 self.metadata、以及 timed-out 边界 _prepare_finalize_context 重新写入 _finalize_contexts 且无清理。

静态核对(06a6db7d)

原问题 结论
§11.1 recovery flush 与 in-flight TTFW/TTLW 竞态 已修复:send_audio() / _handle_audio_frame 帧 metadata 更新、on_result 临时 metrics 上下文均经 _result_context_lock;recovery _flush_buffered_frames → _consume_buffered_frame → send_audio() 走同一路径。test_recovery_flush_preserves_in_flight_ttfw_session 覆盖。
timed-out 后队列边界仍注册 _finalize_contexts 已修复:_prepare_finalize_context(..., register_context=False) 跳过 re-register;_handle_finalize 在 _completed_finalizes + is_finalize_timed_out 分支 finally 执行 _finalize_contexts.pop;正常 _finish_finalize(timed_out=True) 亦 pop。test_finalize_timeout_seals_queued_turn_before_next_finalize 断言 "final-a" not in ext._finalize_contexts。

ASR checklist

项 结果
Package/addon/manifest/graph 命名;manifest 导入 asr-interface pass
Config 校验与 config: 脱敏日志 pass
无效 config 单次 FATAL、阻止连接 pass
start_connection() 握手后才 connected pass
连接失败/关闭关闭 base 连接状态 pass
expected / unexpected / stop / config swap 区分 pass
单一 reconnect owner、有界 backoff、stop/fatal 取消 pass
旧 client stale callback 忽略 pass
send/finalize/stop/config swap 不与同一 client 竞态 pass
缓冲策略有界、session-safe、recovery 后 flush pass
AudioFrame 解锁;send_audio 返回值准确 pass
AudioTimeline 用户音频与注入 silence pass
ASR result 形状 / final·locked / timestamps pass
session_id 在 metadata;vendor 在 asr_info pass
Finalize 必 echo finalize_id/session_id(含 timeout/断开) pass
同 session 至少两轮 audio→finalize pass(测试覆盖)
框架 severity 与 vendor_info 分离 pass
connect delay / TTFW / TTLW metadata pass
vendor_metadata 与日志脱敏 pass
Dump 原始 PCM 无 finalize silence 污染 pass
stop/deinit 取消并 await 后台任务 pass
Standalone tests pass(本地 72 pytest)
ASR guarder pass(本地 12)
task format / task check / task lint N/A(本次未复跑)

Findings(P1/P2 merge blockers)

无。

REVIEW PASS

YiminW and others added 9 commits October 21, 2026 06:18
* feat(tts-guarder): cover more vendors in subtitle timing checks

Add vendor timing hooks for Cartesia, ElevenLabs, ByteDance and the
ByteDance duplex client. Each hook captures provider word times at its
own client boundary and hands them to the shared checker, so the
timestamps a vendor converts can be compared with the original data:
Cartesia and ElevenLabs through the client's websocket dependency,
ByteDance at the session response handler.

The checker keeps only results that carry spoken text, because a terminal
result may hold a synthetic placeholder word that marks the end of a
request instead of describing speech. The guide and the timing notes now
describe the registered observers and the coverage levels.

* fix(cartesia-tts): retain subtitle sequence until terminal result

The per-request sequence counter was released on audio completion and on
error, but the vendor's final timestamp can arrive after that release.
The terminal subtitle then restarted from zero, so consumers saw the
sequence move backwards. The counter is now kept until the terminal
subtitle result has been emitted.

Bump the extension version to 0.8.6.

* fix(tts-guarder): keep ElevenLabs word separators in the timing hook

The hook trimmed trailing whitespace from every alignment packet, so a
space that separates two words was dropped from the reference words and
the comparison against TEN output drifted. Trailing whitespace is now
treated as padding only when it reaches past the audio already delivered,
which mirrors how the extension builds the same timestamps.

* fix(tts-guarder): measure subtitle ranges by the spoken words

The ElevenLabs client appends padding whitespace after the last spoken
word and that padding can reach past the delivered audio, so the range
check rejected a request the provider had finished normally. Ranges and
word counts are now measured up to the last word that carries text, and
the vendor hook reports provider words untouched.

* fix(cartesia-tts): release sequence counters of finished requests

A vendor error can end a request before its final timestamp arrives, so the
per-request counter is kept until the terminal subtitle. Interrupted or
failed requests never receive one, which left their entries behind for the
lifetime of the extension.

An interrupted request now releases its counter with the audio_end event,
and the counters of earlier requests are dropped when the next request
starts. A late terminal timestamp for the live request can therefore still
continue its sequence instead of restarting at zero.

* test(cartesia-tts): cover subtitle sequence and audio-end accounting

The state-machine fakes never enqueue subtitle words, so nothing asserted
the sequence counters or the audio-end fields. The new cases drive the
transcription consumer, the request path, and the interrupted path, then
check the emitted results, the released counters, exactly one audio_end
with the duration of the emitted PCM bytes, and the event interval
measured on a controlled clock.

* fix(tts-guarder): read provider word times at the transport boundary

The ByteDance hooks built their expected times from the converted TEN words
and from the extension's sentence bookkeeping, so a conversion defect moved
the reference and the compared output together and was accepted. Both
references are now built from the provider's own word fields and from the
audio bytes counted at its sentence boundaries: the session's frontend
messages for ByteDance, and the client's server messages for the duplex
variant, which also removes the private extension state it used to read.

Also in this area:
- ElevenLabs alignment times are relative to each request, so the origin is
  reset with the rest of the per-request capture instead of carrying the
  previous request's first-character offset.
- Cartesia spacing mirrors the client again: one space at each timestamp
  group boundary and no space around CJK or punctuation, matching the
  client's own spacing fixtures.

The duplex provider sends turn-relative times and the extension aligns each
sentence's first word with that sentence's audio start. The reference
mirrors that alignment, and SUBTITLE_TIMING.md now records what that costs.

* test(tts-guarder): pin hook spacing, origins, and sentence positions

The hooks build the reference that the live subtitle case compares against
TEN output, so their spacing rules and request bookkeeping are now checked
without a provider connection: the Cartesia spacing fixtures from the
client's own tests, a per-request ElevenLabs origin, and the ByteDance
sentence positions and word offsets.

* feat(tts-guarder): cover the ElevenLabs text-to-dialogue protocol

The extension selects the text-to-dialogue client for models outside the v2
family. The timing hook now follows that choice: it installs the observer on
the module the config selects, reads the snake_case message fields, and
builds the reference the way TTD reports it, with character times restarting
at zero in every chunk and continuing from the previous chunk's text.

The subtitle case runs once per provider configuration, so a vendor can keep
a TTS config and add an alignment-enabled TTD config next to it. The
conftest also drops the removed _pytest.config.Notset import so the suite
loads on the pytest shipped with the current build image.

* test(tts-guarder): report the subtitle bounds in the range diagnostic

A rejected subtitle range only said it was outside the PCM timeline. The
diagnostic now carries the result start, duration, spoken end, and the PCM
window, which is enough to tell a provider tail overshoot from a shifted
conversion without adding a probe.

* feat(tts-guarder): cover the Azure TTS subtitle case

`azure_tts` is a C++ extension that lives in the conversational agent tree,
so its runs copy this guarder there and keep the subtitle configuration in
that tree. Adding it to the supported list lets the case run once that
configuration is present, with the generic alignment checks: the Speech SDK
boundary is inside the compiled extension and has no Python hook point.

The docs record the copy-and-run workflow, the coverage level, and where the
configuration lives.
…2362)

* feat(asr): enhance Soniox ASR websocket client with API key support

* feat(asr): upgrade soniox asr to 0.6.4

---------

Co-authored-by: hanhandi <1540984562@qq.com>
* feat(bridge): add Agora Mind TEN bridge extension

* test(bridge): cover Agora Mind message translation

* fix(bridge): match repository black formatting

* fix(bridge): declare object metadata schema
Preserve finalize/audio order across reconnect buffers, complete finalize
after fatal errors, keep originating session metadata for late finals,
and harden reconnect/error/journal bookkeeping with contract tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep reconnect retries under one owner, stamp session metadata on
server-VAD spans, claim finalize completion before awaits, separate
journal ownership from delivery for TTFW, and fix standalone test paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
hanhandi-git and others added 13 commits October 21, 2026 06:18
Prevent cross-session empty finalize claims, retain in-flight replay
mappings, invalidate connect attempts on stop, and unify normal-close
handling with contract coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wait for in-flight connects before recovery retry, keep open-span
session ownership across finalize, retain confirmed replay spans
before remapping, and sanitize config validation errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
Seal queued audio after finalize timeout, retire confirmed replay
spans on replacement finals, preserve normal-close after startup,
and lock result/finalize context across awaited metrics output.

Co-authored-by: Cursor <cursoragent@cursor.com>
Require vendor commit on local VAD session seals, journal frames before dump,
and route on_data finalize under the result-context lock.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep finalize boundaries in input order, call the base on_data hook,
serialize live audio metadata with result metrics, and tolerate replay
finals without optional word timestamps.

Co-authored-by: Cursor <cursoragent@cursor.com>
Inherit audio session context when finalize omits metadata, acknowledge
empty finalize_id, and keep structured WS rejections on a single fatal path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Defer omitted finalize metadata until preceding audio is consumed, and
avoid restoring stale session context over live input during finalize-end.

Co-authored-by: Cursor <cursoragent@cursor.com>
…paths

Serialize asr_finalize_end with result context lock and add regressions for
metadata-less finalize when the consumer is delayed or in a fatal state.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep finalize emit metadata within the dequeued boundary, prefer queued or
discarded context over stale journal spans on fatal/timeout paths, and pin
asr_result ids per transcript segment with regression tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hold result context only for metadata updates during audio send, count
finalize deadlines from enqueue time, backfill session_id for partial
finalize metadata, and add blocked-send and trace-id regressions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Serialize send_audio metadata updates with result context, avoid
re-registering finalize contexts after timeout seal, and add recovery-flush
metrics and context cleanup regressions.

Co-authored-by: Cursor <cursoragent@cursor.com>
@diyuyi-agora
diyuyi-agora force-pushed the feature/yuyidi/step_asr branch from cdc08fd to afbd434 Compare October 21, 2026 06:20
@TEN-framework TEN-framework deleted a comment from github-actions Bot Oct 21, 2026
@TEN-framework TEN-framework deleted a comment from github-actions Bot Oct 21, 2026
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

Use block-level protected-access suppressions so black does not split
inline comments and break CI lint.

Co-authored-by: Cursor <cursoragent@cursor.com>
@diyuyi-agora
diyuyi-agora force-pushed the feature/yuyidi/step_asr branch from ce0c07a to a61238c Compare October 21, 2026 07:28
@github-actions

Copy link
Copy Markdown
  1. [P1] Handle pending VAD commit failures as transport failures — recognition.py:778–782

    With server_vad=true, finalize after speech_stopped but before transcription completion reaches these direct sends. They bypass _pump_or_reconnect()'s exception handling, so a socket failure escapes through _handle_finalize() into _audio_frame_consumer(), which latches _fatal and discards all subsequent audio. Even ConnectionClosedOK becomes a fatal ASR error. Apply the common transport classification/recovery handling to these sends, and test normal and abnormal closes through the input consumer. Merge blocker: ASR guide §§9.2/10.

  2. [P2] Convert dropped PCM bytes to milliseconds — extension.py:917–919

    Disconnected-buffer overflow passes a byte count to AudioTimeline.add_dropped_audio(), whose argument is a duration in milliseconds. At the required 16 kHz mono PCM16 format, dropping 3,200 bytes records 3,200 ms instead of 100 ms. Recovered result and word timestamps consequently run 3.1 seconds late per such frame. Convert using the actual audio format and add an overflow/recovery regression asserting emitted timestamps; the standalone tests currently have no timestamp/timeline assertions. Merge blocker: §§6.3–6.4.

  3. [P2] Saturate retry delay before evaluating the power — reconnect_manager.py:34

    Retries are unlimited by default, but failure 1,025 evaluates 0.5 * 2**1024 before applying min(), raising OverflowError. This escapes _reconnect_loop() and ends recovery, so transcription cannot resume when the service returns. Cap the exponent or saturate before multiplication, including the equivalent expression in handle_reconnect(). Add a delay regression beyond 1,024 failures; the current budget test stops at 50 and never calls next_delay(). Merge blocker: §9.2.

ASR checklist

Area Result
Lifecycle PASS on static inspection
Connection state PASS on static inspection of handshake/status paths
Buffering FAIL — dropped duration uses bytes (#2)
Finalize FAIL — pending VAD commit failure latches fatal (#1)
Reconnect FAIL — transport classification and backoff overflow (#1, #3)
Result shape FAIL — timestamps include inflated dropped duration (#2)
Metrics PASS on static inspection of connect delay and TTFW/TTLW metadata
Tests FAIL — regressions above are missing; runtime execution N/A

These issues remain present at a61238c. Reviewed against the checked-out ASR guide and published ten_ai_base 0.7.62 (6189b87), since the base package is absent locally. Python/JSON parsing and patch application checks passed; the overflow and byte-to-duration arithmetic were independently checked. PR code, scripts, and tests were not executed, and dependencies were not installed. Existing passing guarder evidence references an earlier head.

@github-actions

This comment was marked as outdated.

@github-actions

Copy link
Copy Markdown
  1. [P1] Handle pending VAD commit failures through transport recovery — recognition.py:783–787

    With server_vad=true, a finalize received after speech_stopped but before the final transcript reaches these direct sends. A socket failure bypasses _pump_or_reconnect() and escapes into _audio_frame_consumer(), which sets _fatal and discards all subsequent audio. Even ConnectionClosedOK becomes a fatal ASR error. Route these sends through the common close classification/recovery path and add normal/abnormal close regressions through the input consumer. Merge blocker: ASR guide §§9.2 and 10 require normal closes without ASR errors and recovery for transient failures.

  2. [P2] Convert dropped PCM bytes to milliseconds — extension.py:917–919

    Disconnected-buffer overflow passes a byte count to AudioTimeline.add_dropped_audio(), which takes milliseconds. At 16 kHz mono PCM16, dropping 3,200 bytes records 3,200 ms instead of 100 ms. Recovered result and word timestamps consequently advance 3.1 seconds too far for each such frame. Convert using the configured audio format and test overflow followed by recovery against the emitted timestamps. Merge blocker: §§6.3–6.4 require correct buffering and timeline accounting.

  3. [P2] Saturate the backoff before calculating the exponential — reconnect_manager.py:34

    Retries are unlimited by default, but failure 1,025 evaluates 0.5 * 2**1024 before min() can cap it, raising OverflowError. The exception exits _reconnect_loop(), leaving recovery stopped when the service returns. Cap the exponent or saturate before multiplication, including the equivalent expression in handle_reconnect(). Add a regression that evaluates delays beyond 1,024 failures; the existing unlimited-budget test only increments the counter 50 times. Merge blocker: §9.2 requires the reconnect policy to continue until success, stop, fatal failure, or its configured ceiling.

ASR checklist

Area Result
Lifecycle PASS on static inspection of cleanup paths
Connection state PASS on static inspection of handshake/status paths
Buffering FAIL — dropped duration uses bytes (#2)
Finalize FAIL — pending VAD send failure permanently disables transcription (#1)
Reconnect FAIL — transient/normal close handling and backoff overflow (#1, #3)
Result shape FAIL — recovered timestamps include inflated dropped duration (#2)
Metrics PASS on static inspection of connect delay and TTFW/TTLW paths
Tests FAIL — the three regression cases above are missing; runtime execution N/A

Reviewed at 3cac629. These three previously reported issues remain present. Base contracts were checked against published ten_ai_base 0.7.62 (6189b87), since that package is absent from the checkout. All 11 added Python files and six added JSON files parse; patch application and runner shell syntax checks pass. The overflow and PCM-duration arithmetic were independently verified. PR code, scripts, and tests were not executed, and dependencies were not installed.

@github-actions

Copy link
Copy Markdown

Review: StepFun ASR (stepfun_asr_python)

Reviewed against docs/ai/L1/L2/asr_plugin_design_review.md. The base ten_ai_base/asr.py was not in the review checkout, so findings that depend on base internals come from reading the diff and the guide only.

Merge blockers (MUST)

  1. Fatal vendor error reports disconnected while the socket is still open. extension.py on_error() calls on_disconnected(FATAL) for fatal types but never shuts down recognition. is_connected() keeps returning True, so the reported state is wrong (§5.1; §10: an error event does not prove the connection is closed). When the server closes the socket later, the finally in _receive_loop calls on_close(), and that calls on_disconnected() a second time. Close the transport when latching fatal and report disconnected once.

  2. A normal close during the handshake reports disconnected twice and schedules reconnect twice. In recognition.start(), the ConnectionClosedOK and _normal_close branches call callback.on_close(), which runs on_disconnected() and _schedule_reconnect(). start() then returns into start_connection(), which takes the normal_close branch into _handle_connect_failure(report_error=False). That calls on_disconnected() again, record_failed_attempt(), and _schedule_reconnect() again. One root cause now has two reconnect triggers (§5.3, §9.1). Keep a single owner: start() should raise or return a status instead of calling on_close() itself.

  3. Any exception in the input consumer is permanently fatal. The overridden _audio_frame_consumer catches every Exception, sets _fatal = True and sends FATAL input_processing. Recoverable cases end up here:

    • an odd-length frame (recognition.send_audio raises ValueError)
    • malformed frame metadata JSON
    • a dumper I/O error
    • a bug in any finalize helper

    After that, every frame is dropped and the session never reconnects. §9.2 reserves FATAL for auth, config and the retry ceiling. Drop and log bad frames instead.

  4. Any exception while handling a received message is fatal. _receive_loop passes any non-websocket Exception to on_error(..., "invalid_response"), and invalid_response is in the fatal set. One malformed JSON frame, or any exception raised in on_result -> send_asr_result (for example a missing words[0]["start"]), disables ASR for the rest of the session. risk_blocked, which is per-content moderation, is also permanently fatal. Please justify this from the vendor docs, or downgrade to NON_FATAL and skip or reconnect.

  5. No guarder or standalone run evidence. The PR body is empty. §13.2 requires task test-extension and task asr-guarder-test results for the en, zh, invalid and dump configs. The results that matter most here are two finalize cycles in one session, dump byte equality, and reconnect.

Should fix

  1. Unrelated changes. soniox_asr_python/tests/configs/property_{en,zh}.json switch Soniox to the jp region and add language_hints_strict. That changes another vendor guarder baseline and belongs in a separate PR. In integration_tests/asr_guarder/tests/test_connection_status.py, about 50 lines of reformatting surround the one real change (adding stepfun_asr_python). Please keep only that line so the file still matches task format.

  2. Heavy reliance on base-class private internals. The extension overrides _audio_frame_consumer and calls _handle_audio_frame, iterates audio_frames_queue._queue, and writes buffered_frames, buffered_frames_size and sent_buffer_length directly. It also reads recognition._current from the extension. This breaks quietly when the base changes, and it accounts for most of the complexity in the 1.2k-line extension. The documented pattern is simpler: flush explicitly from on_open (§6.3.4) and save the finalize context before super().on_data() (§8.2).

  3. _flush_buffered_frames can lose frames. It pops a frame and then calls send_audio. If that returns False (a send failure, or a disconnect between the is_connected() check and the send), the frame is neither re-queued nor counted. Also, send_audio writes to the dumper even when delivered is False. If the base re-buffers rejected frames, the dump records those bytes twice and fails byte equality (§12.1).

  4. Duplicate errors for one root cause. A non-fatal vendor error during the handshake sends NON_FATAL from on_error. start() then raises ConnectionError(session_error), and _handle_connect_failure sends a second NON_FATAL error with the same message (§10).

  5. Reconnect is unbounded by default. reconnect_max_attempts defaults to 0, which means unlimited, so the default config never hits the escalate-to-fatal ceiling from §9.2. Please use a finite default.

  6. Auth is classified by substring. _classify_connect_error treats any message containing 401 or 403 as fatal auth, which also matches IDs, ports or sizes. Use _extract_http_status or the structured error type instead.

  7. Undeclared or dead code. Please declare or remove these:

    • on_open sends an asr_ready data message that is not declared in manifest.json or asr-interface.json.
    • _check_complete (asr_check_complete_end) is never called.
    • ReconnectManager.handle_reconnect is marked legacy and used only by tests.
  8. Minor.

    • _normalize_language only maps en, zh, ja and ko. Other languages are emitted un-normalized (§7.1).
    • The README "As of / Codebase commit ... (with local changes)" lines will go stale.
    • _sanitize_config_error calls errors() and discards the result.
    • manifest.json has no trailing newline.

ASR checklist

Item Status
Lifecycle Pass. super() hooks are called, and an init failure latches fatal with one error.
Connection state Fail (items 1, 2)
Buffering Partial. Keep mode is bounded at 10 MiB and the buffer is flushed on open. A failed send can lose a frame or dump it twice (item 8).
Finalize Pass from reading the code. Timeout fallback, the empty-commit ack, and the finalize_id/session_id echo are unit-tested. Guarder evidence still needed.
Reconnect Fail (items 2, 3, 4, 10)
Result shape Pass. session_id is under metadata and vendor fields are under asr_info. Language normalization is incomplete (item 13).
Metrics Pass. connect_delay is sent after session.updated, and TTLW is cleared for VAD and timed-out finals.
Tests Unit tests are extensive. Guarder evidence missing (item 5).

@github-actions

Copy link
Copy Markdown
  1. [P1] Handle pending VAD commit failures through transport recovery — recognition.py:628–630

    With server_vad=true, finalize after speech_stopped but before transcription completes reaches _queue_commit() outside _pump_or_reconnect()'s exception handler. A socket failure therefore escapes into _audio_frame_consumer(), which sets _fatal and discards subsequent audio. Even ConnectionClosedOK becomes a fatal ASR error. Put this send inside the common transport classification/recovery path and add consumer-level regressions for normal and abnormal closes during this commit. Merge blocker: ASR guide §§9.2/10 require recovery for transient failures and no ASR error for normal closes.

  2. [P1] Preserve finalize boundaries while preceding audio is buffered — extension.py:748–750

    If speech arrives while disconnected, the base handler puts it in buffered_frames. This flush then returns without sending it, but _handle_finalize() still calls recognition.commit(), whose boundary only includes already-sent bytes. For an otherwise empty stream, it immediately acknowledges an empty turn. After recovery, the buffered speech is appended without its finalize marker; with the default server_vad=false, it has no commit until another finalize/session boundary arrives. An omitted finalize metadata object also echoes an empty/stale session because neither metadata resolver reads buffered_frames. Preserve the boundary and originating metadata through recovery, with the existing timeout fallback. Add disconnected audio → finalize → reconnect coverage with no further input, asserting the final result and session echo. Merge blocker: §§6.3/8.1–8.2 require session-safe buffering and correct finalize/session association.

  3. [P2] Retain an unsent frame when the recovery flush fails — extension.py:939–944

    The flush removes a frame and decrements the buffered byte count before checking delivery. If the recovered socket fails on its first append, send_audio() returns False, but that entirely unsent frame is never restored. The next connection therefore skips speech that the keep-buffer policy had retained, without recording a dropped duration. Keep an entirely unsent frame pending in FIFO order until successful delivery, and test failure on the first recovery append followed by another successful reconnect. Merge blocker: §§6.3–6.4 require correct buffering and audio-timeline accounting.

  4. [P2] Saturate backoff before evaluating the exponential — reconnect_manager.py:34

    Retries are unlimited by default, but failure 1,025 evaluates 0.5 * 2**1024 before min() can cap the delay, raising OverflowError. This escapes _reconnect_loop() and terminates recovery, so transcription cannot resume when the service returns. Cap the exponent or saturate before multiplication, including the equivalent expression in handle_reconnect(). Add a regression that evaluates delays beyond 1,024 failures; the current budget test stops at 50 and never calls next_delay(). Merge blocker: §9.2 requires the reconnect policy to continue until success, stop, fatal failure, or its configured ceiling.

ASR checklist

Area Result
Lifecycle PASS — static inspection of cleanup paths
Connection state PASS — static inspection of handshake/status paths
Buffering FAIL — finalize boundary loss and failed-flush audio loss (#2, #3)
Finalize FAIL — transport classification and disconnected turn/session handling (#1, #2)
Reconnect FAIL — pending commit failure, failed flush, backoff overflow (#1, #3, #4)
Result shape PASS — static inspection of required fields, word conversion, metadata placement
Metrics PASS — static inspection of normal connect-delay and TTFW/TTLW paths
Tests FAIL — the failure scenarios above are missing; runtime execution N/A

Reviewed head 869e716. The base package is absent locally, so lifecycle, buffering, timeline, and schema contracts were checked against published ten_ai_base 0.7.62 (6189b87). Static parsing passed for all 12 added Python files, six JSON files, and the TOML file; runner shell syntax and git apply --check also passed. The backoff overflow was independently reproduced with the arithmetic expression. PR code, tests, and scripts were not executed, and dependencies were not installed.

This branch has not been deployed

No deployments
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.

4 participants