Skip to content

feat(tts): add kotoba_tts_python extension (Kotoba TTS on fal) - #2357

Draft
q-cheng wants to merge 9 commits into
TEN-framework:mainfrom
q-cheng:feat/kotoba-tts-python
Draft

q-cheng wants to merge 9 commits into
TEN-framework:mainfrom
q-cheng:feat/kotoba-tts-python

Conversation

@q-cheng

@q-cheng q-cheng commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

Adds kotoba_tts_python, a TTS extension for Kotoba Japanese text-to-speech served on fal (Kotoba-Technologies/kotoba-tts).

  • WebSocket implementation on AsyncTTS2BaseExtension, built on the official kotoba-sdk client.
  • Streams LLM text as sentences: deltas are buffered per request_id and each complete sentence becomes one service request; the remainder is sent on text_input_end.
  • One reused connection with reconnect after the service idle timeout; a sentence is retried once when the connection drops before any audio for it arrived.
  • Flush cancels the in-flight response (response.cancel), drops buffered text and reports tts_audio_end with INTERRUPTED.
  • PCM16 mono at 8 / 16 / 24 kHz (params.sample_rate), tts_audio_end duration computed once from emitted bytes, event interval from tts_audio_start.
  • Fatal errors: rejected key (fal closes with 1008 Unauthorized), unsupported language / sample rate. Non-fatal: worker and transport failures, a 429 on the handshake (rate_limited, retried once), a text the service rejects (invalid_text, not retried); the next request starts on a fresh connection.
  • One terminal sequence (_finalize) shared by completion, flush and shutdown: error, tts_audio_end, usage, state transition, dump flush. Each step records its completion, so a flush or stop that interrupts the worker completes only what is left; a request stays registered until its dump is closed, so on_stop can finish a dump that is still being written after the next request has started.
  • TTFB, request and usage metrics; per-request PCM dumps (unique file per request id, truncated on open).
  • Diagnostics never carry credentials: the key is fingerprinted, every params value and every URL query value is masked, userinfo is stripped.

Configuration

"params": {
  "key": "${env:KOTOBA_TTS_KEY}",
  "language": "ja",
  "speaker_id": "male-01",
  "sample_rate": 16000
}

See ai_agents/agents/ten_packages/extension/kotoba_tts_python/README.md.

Language. The public kotoba-tts app serves Japanese (language: "ja"). The guarder's built-in texts are English; the run below passes because the Japanese voice reads them, but the intended input is Japanese. The extension-local tests use Japanese text.

Tests

Extension-local tests (tests/) run against an in-process fake of the Kotoba service (tests/fake_kotoba_server.py) so the real SDK client is exercised: lifecycle, append / interleaved / late input, exact audio-end accounting under a controlled clock, flush before and after audio, flush while the send or handshake is blocked, double flush with a pending cancel drain, stray audio after cancel, flush racing the terminal path (events landing in time, and a hung worker taken over with usage still reported), flush during streaming with the dump write held, stop during a dump flush that outlasts the join bound, a next request overlapping the previous request's dump flush then stop, flush and stop while the write itself is suspended, late old-session cleanup with a replacement connection, idle close reported before the next text, reconnect without replay, failing status callbacks, split-sample alignment, unusable dump path, colliding and stale dump files, a 2,000-character input and a 30-sentence backlog with flush and recovery, endpoint secrets under arbitrary query names, init failure across several deltas, transient and persistent 429 on the handshake, service-side text rejection, error classification and secret redaction in logs and event metadata, metrics, dump.

tests/bin/start
111 passed, 1 xfailed

task lint-extension EXTENSION=kotoba_tts_python: 10.00/10. black -l 80 clean.

Outside the extension directory, the PR adds kotoba_tts_python to the TTS guarder's WebSocket allowlist (tests/bin/start, test_connection_status.py) so test_connection_status runs for it.

Guarder evidence

Extension: kotoba_tts_python, HEAD fd719c3a7fb74a97dc72ffc2894083e41723e89e

task tts-guarder-test EXTENSION=kotoba_tts_python
16 passed, 1 skipped (subtitle alignment: the service exposes no word timestamps)

Screenshot and .pcm output (16 kHz, 16-bit, mono) attached below.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T06:50:49.413592Z 57ede87 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@q-cheng

q-cheng commented Oct 15, 2026 •

Copy link
Copy Markdown
Author

Guarder evidence — kotoba_tts_python

  • Extension directory: ai_agents/agents/ten_packages/extension/kotoba_tts_python
  • Tested PR HEAD: fd719c3a7fb74a97dc72ffc2894083e41723e89e
  • Command (from ai_agents/, inside ghcr.io/ten-framework/ten_agent_build:0.7.14, with a real KOTOBA_TTS_KEY):
task tts-guarder-test EXTENSION=kotoba_tts_python
  • Result: 16 passed, 1 skipped (389.62 s). test_connection_status runs and passes: this PR lists the extension in the guarder's WebSocket allowlist (tests/bin/start, test_connection_status.py), since it reports connecting / connected / disconnected. The one skip is test_subtitle_alignment: the service exposes no word timestamps, so subtitle alignment is not a supported capability.

Downloads:

Note: the public kotoba-tts app serves Japanese. The guarder's built-in texts are English and pass because the Japanese voice reads them; the extension-local tests use Japanese text.

Screenshot of the run summary follows in the next comment.

@github-actions

Copy link
Copy Markdown

[P1] Wait for the finishing worker before acknowledging flush

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:320

_end_request() sets request.ended = True before awaiting error reporting and _send_audio_end(). With dumping enabled, a flush can arrive while that worker awaits recorder.flush(). This early return lets the base emit tts_flush_end and clear metadata while the separate worker continues; it subsequently sends the old tts_audio_end after the flush boundary. Its later usage reset can also consume counters from a newly started request. Serialize or await the finishing worker before completing flush, with a regression that blocks the final dump write and flushes during it.

[P1] Complete the required guarder evidence before merge

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment promises a screenshot in a subsequent comment, but none is attached in the current comments. The PCM download is accessible and non-empty; the log alone does not satisfy the new-extension screenshot gate. Also, test_connection_status was skipped because the vendor is absent from the harness allowlist, although this extension implements connection-status callbacks and its local tests assert those events; that is not an absent capability. Attach the redacted command/result screenshot and provide passing connection-status evidence for the current head. These are merge blockers under the repository's TTS guarder evidence requirements.

[P2] Propagate cancellation while awaiting the previous response drain

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:364

Flush request A, start B while A's cancellation drain is pending, then flush B. Cancelling B's worker propagates cancellation into the awaited drain, but this handler swallows CancelledError. connect() therefore continues, reuses the incompletely drained socket, and submits B despite cancellation. Because _stop_worker() awaits that worker, the second flush can then wait for B's first audio or the full response timeout (30 seconds by default). Re-raise caller cancellation and ensure an interrupted drain cannot leave a reusable socket containing A's response; cover this double-flush ordering deterministically.

[P2] Flush request usage counters before starting the next request

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:327

The normal/error terminal path calls send_usage_metrics(), which resets the base's request counters, but cancellation only sends audio end. The base flush clears request state and metadata without resetting these counters. Consequently, flushing A after some audio and then completing B reports A+B's input_characters and recv_audio_duration under B's metadata, while A receives no final usage. Emit/reset A's request usage during cancellation and assert that B's usage contains only B's text and emitted PCM.

[P2] Capture the audio interval origin at audio-start emission

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:287

now is captured before awaiting send_tts_ttfb_metrics(), then reused as audio_started_at. Any delay sending that metrics event is therefore included in request_event_interval_ms, even though tts_audio_start has not been sent yet. Capture a fresh monotonic timestamp at the audio-start call. Add the required controlled-clock assertions for success, partial-error and interrupted paths; the current non-negative/positive interval assertions cannot establish the required start-to-end boundary.

[P2] Add the mandatory transport shutdown and ownership tests

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_reconnect.py:16

Beyond the regressions above, the local suite does not exercise flush/shutdown while handshake or send is blocked, failure in the disconnect-reporting callback, or cancellation of old-session work after a replacement connection exists. These are explicit merge-gating cases in ten-tts-review/references/tests.md; completed-handshake flushes and sequential reconnect tests do not cover them. Add controllable transport/callback blockers and assert bounded cleanup, no old audio or terminal event after flush, and preservation of the replacement connection for a successful subsequent request.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f86171c01d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/config.py Outdated
Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py Outdated
Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py Outdated
Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py Outdated
Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py Outdated
@q-cheng
q-cheng marked this pull request as draft October 16, 2026 02:05
Kotoba TTS (Japanese) over the fal-hosted WebSocket API, built on
AsyncTTS2BaseExtension and the official kotoba-sdk client. Sentence
buffering for streamed LLM text, one reused connection with reconnect,
cancel on flush, PCM16 at 8/16/24 kHz, fatal/non-fatal error mapping
(including fal's 1008 Unauthorized close), TTFB and usage metrics,
per-request dumps.

Extension-local tests run against an in-process fake of the service
(tests/fake_kotoba_server.py); tests/configs_fake lets the TTS guarder
run without a service key.
@q-cheng
q-cheng force-pushed the feat/kotoba-tts-python branch from f86171c to 4c8ef0a Compare October 16, 2026 02:39
@github-actions

Copy link
Copy Markdown

[P1] Mark the response active before awaiting the text send

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:277

If flush cancels the worker while session.synthesize(text) is suspended after writing to the socket, CancelledError bypasses except Exception and _response_active remains false. cancel() then skips cancellation/draining, so the next request can reuse a socket containing the interrupted response and emit its audio under the new request ID. Mark the operation active before sending, or discard the session when sending is cancelled; test cancellation with a blocked send.

[P1] Await the terminalizing worker before acknowledging flush

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:320

_end_request() sets request.ended before awaiting error delivery, recorder flushing, and audio-end delivery. A flush during those awaits takes this early return without stopping or joining the separate worker. The base can therefore emit tts_flush_end and clear metadata before the worker emits the old request's terminal event; its later usage reset can also consume a new request's counters. Track terminal delivery separately and await or cancel the worker before completing flush.

[P1] Supply current guarder evidence and the required screenshot

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment cites f86171c, whereas the current head is 4c8ef0a, and the promised result screenshot is still absent. The linked PCM is accessible and non-empty, but it belongs to that older run. Also, connection-status coverage was skipped because of the harness allowlist even though this extension implements that capability. The new-extension evidence gate requires a passing run for the current head, a redacted command/result screenshot, downloadable PCM with playback settings, and justified capability skips. This remains a merge blocker.

[P2] Propagate caller cancellation while awaiting a response drain

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:364

Flush A, start B while A's cancellation drain is pending, then flush B. Cancelling B propagates cancellation into the awaited drain, but this handler swallows it and lets connect() continue on the incompletely drained socket. B can submit text despite cancellation, and the second flush waits for that worker to reach audio or its response timeout. Re-raise caller cancellation and ensure an interrupted drain leaves no reusable stale session; add a deterministic double-flush regression.

[P2] Redact retained credentials and signed URLs before logging configuration

ai_agents/agents/ten_packages/extension/kotoba_tts_python/config.py:110

to_str() masks only key and api_key. A signed/custom base_url, or credentials supplied in retained unknown parameters such as token or authorization, remain in the representation logged by on_init(). This writes those credentials to runtime logs even when sensitive handling is enabled. Sanitize URL credentials and sensitive retained parameter values, and cover those inputs in the redaction tests.

[P2] Emit and reset interrupted usage before accepting the next request

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:327

Cancellation sends audio end without calling send_usage_metrics(), unlike normal/error completion. The base flush clears state and metadata but does not reset its character and audio-byte counters. After flushing A and completing B, B's request usage therefore contains A+B, while A receives no final usage. Emit/reset A's usage before cancellation returns and assert that the following request contains only its own text and emitted PCM.

[P2] Capture the event-interval origin at audio-start emission

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:287

now is captured before awaiting send_tts_ttfb_metrics() and then reused as audio_started_at. Any metrics-delivery delay is included in request_event_interval_ms before tts_audio_start has been emitted, violating the required start-to-end boundary. Capture a fresh monotonic timestamp at audio-start emission and add controlled-clock assertions for success, partial-error, and interrupted completion; the existing positive/non-negative checks cannot detect this.

[P2] Start TTFB timing at the vendor text request

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:245

This timestamp precedes both the awaited request-metrics emission and the lazy generator's connection/authentication handshake. First requests and reconnects therefore report local metrics backpressure and connection setup as synthesis TTFB. Capture the origin at the client's actual text-send boundary and carry it back to the extension; independently delay handshake, metrics delivery, and first audio in the test.

[P2] Add the mandatory blocked-transport and connection-ownership cases

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_reconnect.py:16

The local suite does not exercise shutdown/flush during a blocked handshake or send, failure while reporting a disconnect, or cancellation of old-session work after a replacement connection exists. These are explicit merge-gating cases in .agents/skills/ten-tts-review/references/tests.md; completed-handshake flushes and sequential reconnects do not cover them. Add controllable transport/callback blockers and assert bounded cleanup, no stale audio/end after flush, and preservation of the replacement connection for a successful next request.

…ounting, redaction)

- Mark the response active before the text send and drop the socket when
  the send is interrupted, so a flush cannot leave a response on a socket
  the next request reuses.
- Flush joins a worker that is already emitting its terminal events
  (bounded), and audio_end_sent is set only after the event is out, so no
  event of the old request lands after tts_flush_end and there is exactly
  one audio_end per request.
- Propagate caller cancellation out of the cancel drain and drop the
  socket when a drain is interrupted (double flush no longer waits on the
  response timeout).
- Emit usage metrics on interrupt so the next request's counters are
  clean; start the event interval at tts_audio_start emission; measure
  TTFB from the text send and report handshake time as connect_delay;
  count bytes only after the frame is sent; a dump write failure no longer
  fails the request.
- Redact every retained parameter value and URL credentials in to_str().
- Status callbacks that raise are logged, never propagated.
- Tests: cancellation under blocked send / slow handshake / double flush /
  stray audio / terminal race / hung worker / usage isolation / stop while
  blocked / failing callback; accounting under a controlled clock;
  redaction; dump failure. 65 passed.

Also list kotoba_tts_python in the TTS guarder WebSocket allowlist so
test_connection_status runs for it.
@github-actions

Copy link
Copy Markdown

[P1] Supply guarder evidence for the current head and finish the screenshot upload

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The evidence comment identifies 4c8ef0a, while the current head is dc2c879. Its screenshot is still an empty ![Uploading …]() placeholder. The PCM download is accessible and non-empty, but belongs to the older run, which also skipped connection-status coverage despite this extension supporting it. The new-extension gate in .agents/skills/ten-tts-review/references/guarder.md therefore remains a merge blocker. Run the full suite against the current head, including connection status, and attach the completed result screenshot and that run's PCM with playback settings.

[P2] Sanitize the endpoint before returning vendor metadata

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:132

For a custom endpoint such as the credential-bearing URL already used in test_params.py, this returns the original userinfo and signature. The base's update_metadata() only applies JSON-key redaction; it does not sanitize a URL stored under url. Consequently, request metrics, connection events, audio events, and their logs expose those credentials even though config.to_str() masks them. Return a sanitized endpoint snapshot and assert that emitted metadata contains neither URL credentials nor query secrets.

[P2] Handle dump-directory failures without stranding the request queue

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:207

With dumping enabled, an unwritable dump_path, or a path that names an existing file, makes os.makedirs() raise before _request and its worker are installed. The exception escapes request_tts(); the base queue logs it but leaves _processing_request_id active. This request receives no terminal event, and subsequent request IDs remain buffered until a flush. Catch dump setup failures and either disable recording or finish the request with an error. Add a regression that fails directory creation and then completes another request.

[P2] Report remote idle closure without waiting for another text request

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:248

After reporting connected, the client does not observe the SDK receiver's termination while idle. When the service closes an idle socket, _session remains set and the base stays CONNECTED; disconnection is reported only when a later synthesis attempts to use the dead session, or during shutdown. This leaves connection-status consumers with stale availability for the entire idle period. Observe transport termination, retire only that session, and report disconnected promptly. Test that an idle close produces the event before sending any subsequent text.

[P2] Exercise actual blocked writes and replacement-connection ownership

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_cancellation.py:62

hold_response_index stalls the server response after accepting the text; it does not suspend AsyncTTSSession.synthesize() or the underlying write. Thus this test does not exercise the new except CancelledError send-cleanup branch. The suite also lacks old-session cancellation after a replacement is already connected. These remain mandatory transport cases under .agents/skills/ten-tts-review/references/tests.md. Gate the actual send coroutine, cover flush and shutdown during the write, and assert that delayed old-session cleanup preserves the replacement and cannot publish stale audio.

[P2] Complete exact accounting coverage for error and interrupted termination

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_accounting.py:90

The controlled-clock test covers successful completion only. The partial-error and interrupted cases here assert duration but never assert the matching start-to-end interval; the other flush test only checks that it is positive. All audio fixtures also contain complete, millisecond-aligned samples, leaving alignment and rounding accounting untested. The mandatory local-test matrix requires exact assertions for these paths. Add controlled elapsed-time checks for error/interruption and split-sample or sub-millisecond chunks, checking duration against the PCM actually emitted and ensuring no residual sample crosses requests.

…etup, idle close, alignment)

- vendor_metadata() returns the endpoint with credentials and signed query
  values masked, so no event metadata carries URL secrets.
- A dump_path that cannot be created disables recording for the request
  instead of raising out of request_tts() and stranding the base queue.
- A watcher per connection observes the SDK receiver ending while idle and
  reports disconnected promptly, retiring only that session.
- Only whole samples are emitted and counted; a trailing half sample is
  held until the next chunk and never crosses requests.
- Tests: the actual write is gated and flushed / stopped while suspended;
  late old-session cleanup preserves the replacement connection; idle
  close is reported before the next text; controlled-clock interval on
  error and interrupt paths; split-sample chunks; unusable dump_path;
  endpoint secrets absent from event metadata. 74 passed.
@github-actions

Copy link
Copy Markdown

[P1] Refresh guarder evidence for the current head

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment identifies dc2c879, while the current head is 9b460c3. Its attached screenshot actually shows 15 passed, 2 skipped, including test_connection_status SKIPPED, contradicting the stated 16 passed, 1 skipped. The PCM download is accessible and non-empty, but also identifies the older run. This remains a merge blocker under the repository's new-extension evidence gate: run the full guarder suite against the current head, including connection status, and attach that run's matching screenshot and PCM with playback settings.

[P2] Sanitize SDK error messages before forwarding them

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:100

For a signed or credential-bearing custom endpoint that fails DNS resolution or refuses the connection, SDK 0.2.3 raises APIError("Could not reach <raw URL>: ..."). This branch preserves that URL verbatim; it reaches disconnect vendor_info, retry logs, and the final error message. Neither redacted_base_url() nor the base's metadata redaction sanitizes those strings, so endpoint credentials leak on an ordinary transport failure despite being masked on successful requests. Sanitize SDK diagnostics before logging/emitting them and add a failed-connection regression using a credential-bearing URL.

[P2] Retire a closed session after its active response finishes

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:271

If the server sends response.done followed by a clean close while the consumer is still draining queued events, the receiver finishes with _response_active true and this watcher exits without retiring the session. synthesize() subsequently consumes the successful done/sentinel, clears _response_active, and returns without checking the closed receiver. Consequently, _session and connection status remain connected until another text request or shutdown. Recheck transport termination when the active response finishes, and test an immediate close after done while audio consumption is delayed.

[P2] Exercise old-session cleanup after the replacement is connected

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_cancellation.py:495

This test delays the server's cancelled response, but B's connect() first awaits A's drain. The drain times out, closes A's SDK session and cancels its receiver before B's connection is created. A later server write therefore cannot exercise client cleanup against an already-connected replacement, so the test does not protect the session-identity guards it claims to cover. The mandatory transport matrix requires this ordering: use a controllable session/cleanup gate to release old-session cleanup or cancellation after the replacement connects, then assert that the replacement remains usable and emits no stale audio/events.

[P2] Cover dump finalization on large streams and partial errors

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_dump.py:22

The dump-enabled tests cover small successful responses and early flushes; none forces output past the writer's 64 KiB automatic-flush threshold or enables dumping during a vendor failure after partial audio. Thus the mandatory long-stream and error-terminal dump checks are absent, leaving buffered-tail preservation and cleanup during an outstanding write unverified. Add deterministic large-stream/slow-writer cases and a partial vendor error, asserting that finalized files exactly match emitted PCM and that a subsequent request remains isolated.

…-driven tests

- Drop the two redundant `except CancelledError: raise` clauses that
  pylint W0706 flagged; cancellation already passes through `except
  Exception`. Keep the intent as comments.
- Remove the unused `connected` property and `SentenceBuffer.clear()`.
- Tests from the coverage and matrix audit: unterminated tail sent at
  text_input_end, completed-id history eviction, unexpected negotiated
  format is fatal, unexpected worker exception ends the request and
  recovers, failing on_connecting / on_connected callbacks are logged
  only, blank / zero config fields and boolean-for-float are rejected,
  dump finalized on the error path, very fast response. 85 passed; pylint
  10.00/10.
@github-actions

Copy link
Copy Markdown

[P1] Refresh guarder evidence for the current head and attach the screenshot

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment identifies 9b460c3, while the current head is 57ede877. It says the screenshot follows in another comment, but no current comment contains that screenshot. The linked PCM downloads successfully and is non-empty, but belongs to the older run. This remains a merge blocker under .agents/skills/ten-tts-review/references/guarder.md: run the full guarder suite against the current head and attach its result screenshot and matching PCM, identifying the SHA and playback settings.

[P2] Sanitize SDK diagnostics before forwarding them

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:100

For a credential-bearing custom endpoint that fails DNS resolution or refuses the connection, SDK 0.2.3 raises APIError("Could not reach <raw URL>: ..."). This branch preserves that URL verbatim, and connect() forwards it in disconnect vendor_info; retries also log it, and terminal errors expose it again. Sanitizing vendor_metadata() does not sanitize these strings, so URL passwords and signed query values leak on an ordinary transport failure. Sanitize diagnostics before logging or emitting them, and add a failed-connection regression using a credential-bearing endpoint.

[P2] Retire a closed transport after its active response finishes

ai_agents/agents/ten_packages/extension/kotoba_tts_python/kotoba_tts.py:267

If the server sends response.done and then closes cleanly while the consumer is still draining queued audio, the SDK receiver ends while _response_active is true. This watcher exits without retiring the session. synthesize() then consumes the successful done/sentinel, clears _response_active, and returns without rechecking the receiver. Consequently, _session and connection status remain connected until another request or shutdown. Recheck transport termination when the response finishes, preserving session ownership, and cover an immediate close after done with delayed audio consumption.

[P2] Finalize the recorder when flush takes over terminal cleanup

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:395

With dumping enabled, let the worker enter _end_request() and stall in send_tts_audio_end() longer than _TERMINAL_JOIN_SECONDS, then flush. _join_terminal_worker() cancels the worker before it reaches _close_recorder(). This branch emits the replacement end event, clears _request, and returns without flushing the recorder; shutdown can no longer find it. For a short request, all emitted PCM is still buffered and the dump is lost entirely. Close the recorder on the takeover path, including cancellation-safe cleanup. Extend the existing hanging-terminal test with dumping enabled and assert exact finalized PCM.

[P2] Exercise old-session cleanup after the replacement connects

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_cancellation.py:495

This test delays the server's cancelled response, but B's connect() first awaits A's drain. That drain times out, closes A's SDK session, and cancels its receiver before B's connection is created. A later server write therefore cannot exercise client cleanup against an already-connected replacement. The mandatory transport matrix in .agents/skills/ten-tts-review/references/tests.md remains uncovered. Gate old-session cleanup or cancellation itself, release it after B connects, and assert that B remains usable with no stale audio or disconnect events.

[P2] Cover dump finalization while an automatic write is pending

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_dump.py:22

The dump-enabled local tests use short responses or interrupt after a few chunks; none reaches the writer's 64 KiB automatic-flush boundary or holds an actual write pending during finalization. The new partial-error test also uses only 3,200 bytes. This leaves the required large-stream and cancellation-under-load behavior unprotected, particularly whether buffered tails survive completion or flush while a write is outstanding. Add deterministic output beyond that threshold with a controlled slow writer, assert finalized files exactly match emitted PCM, and verify a subsequent request stays isolated.

@q-cheng
q-cheng marked this pull request as ready for review October 16, 2026 06:39

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57ede877a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py Outdated
@@ -0,0 +1,72 @@
{
"type": "extension",
"name": "kotoba_tts_python",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Attach current live guarder evidence for Kotoba

I inspected the supplied PR comments for this new TTS extension, but none provides current-HEAD evidence for the full live task tts-guarder-test EXTENSION=kotoba_tts_python run: the final counts, result screenshot, downloadable non-empty PCM, and its sample format are all absent. Add those artifacts before merging so real-service compatibility is established for the reviewed commit.

AGENTS.md reference: AGENTS.md:L65-L68

Useful? React with 👍 / 👎.

Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py Outdated
Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py Outdated
Comment thread ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py Outdated
@q-cheng
q-cheng marked this pull request as draft October 16, 2026 06:51
…s, dump lifecycle, metrics)

- Scrub endpoint credentials from every diagnostic string the client logs
  or emits (SDK errors quote the URL they tried); init errors are built
  from pydantic's error types only, never rejected input values; config
  type errors name the type, not the value; unexpected exceptions emit
  only their type.
- A request that fails because initialization failed now gets a
  zero-duration ERROR terminal event before finish_request.
- The handshake delay is attributed to the request that opened the
  connection even when that request ends without audio.
- A server close right after response.done retires the session and
  reports disconnected once the response is consumed.
- The recorder is finalized on the flush takeover path and when the app
  stops during terminal cleanup; _close_recorder flushes until the
  PCMWriter buffer is empty so no tail is lost behind a pending write.
- Tests: connection refused on a credential-bearing endpoint, malformed
  property with a marker value, init-failed terminal event, connect_delay
  attribution, close after done, old/new connection ordering proof,
  takeover and stop-during-flush dumps, 96 KB dump with a slowed writer
  and flush under load. 92 passed; pylint 10.00/10.
@github-actions

Copy link
Copy Markdown

[P1] Refresh guarder evidence for the current head and attach its screenshot

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment identifies 57ede877, while the current head is 5765cce6. Its linked log reports 16 passed and 1 skipped, and the PCM downloads successfully (929,280 bytes), but both identify the older run. The promised result screenshot is also absent from the current issue and review comments. This remains a merge blocker under the new-extension evidence gate: run the full guarder suite against the current head and attach that run's command/result screenshot and matching PCM, identifying the tested SHA and playback settings.

[P2] Preserve recorder ownership when terminal cleanup is cancelled

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:537

_close_recorder() clears request.recorder before awaiting the flush. For a large response, an automatic write can still be pending while later emitted PCM remains in the writer's buffer. If shutdown arrives during terminal cleanup and that write exceeds _TERMINAL_JOIN_SECONDS, _join_terminal_worker() cancels the worker and its awaited flush; the subsequent _close_recorder() in on_stop() sees None, so the buffered tail is never written. Retain ownership until finalization completes and make pending-write cleanup survive cancellation. Extend the shutdown test with an outstanding automatic write held beyond the join timeout, and assert the finalized file equals all emitted PCM.

[P2] Complete usage cleanup when flush takes over a terminal worker

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:421

Flush can arrive after _send_audio_end() sets audio_end_sent, while its later send_usage_metrics() is suspended. If the join times out, the worker is cancelled before the base helper reaches metrics_reset(). This branch then skips _send_audio_end() because the audio-end flag is already set and returns without completing usage cleanup; base flush clears request state but leaves those counters intact. The next request consequently reports the interrupted request's characters/audio in its own usage. Track terminal-event delivery and usage completion separately, and ensure takeover emits/resets the old request's usage exactly once. Add a regression that blocks usage delivery through the join timeout and then completes a second request.

[P2] Cover the required vendor rejection and recovery cases locally

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_error_msg.py:122

The local error tests cover authentication, unsupported language, transport failures, and worker_unavailable, but none exercises quota/rate limiting or a vendor rejection of otherwise valid text input. These are mandatory cases in the extension-local error matrix; invalid configuration tests do not cover service-side input rejection. Add deterministic failures for both categories and assert severity/vendor information, bounded retry, exactly one terminal event, clean request counters, and a successful following request.

…imits and text rejections

The worker, flush and shutdown each ran a different subset of the terminal
sequence, and a flush or stop that cancelled the worker partway could leave
usage counters unsent, lose the tail of a dump, or orphan a request whose dump
was still being written after the next request had started.

- extension.py: a single _finalize() runs error, audio_end, usage,
  finish_request and the dump flush under a per-request lock; each step sets
  its flag right after its await, so a caller that takes over repeats only
  what was cut off. Requests stay in _finishing until their dump is closed and
  on_stop joins all of them. Flush waits on a `settled` event (events out)
  instead of the whole worker, so it never waits on the dump. The recorder is
  detached only after its flush, and each flush is shielded so a cancelled
  caller does not cancel the writer's task. TTFB reads the clock before any
  await. The connect-delay value is parked on the request before its send.
- kotoba_tts.py: a 429 handshake maps to code rate_limited (retryable);
  request-level codes (invalid_text, text_too_long) are not retried.
- tests: handshake_status_count knob; tests for transient and persistent
  429, service-side text rejection, usage after a flush takeover, stop during
  a gated dump flush, a next request overlapping the previous dump flush then
  stop, and flush waiting on terminal events that land in time.
- remove the stray .coverage data file.
@github-actions

Copy link
Copy Markdown

[P1] Refresh guarder evidence for the current PR head

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment identifies 5765cce6, while the current head is 32ad1a1b. The screenshot is accessible, the log reports 16 passed and 1 skipped, and the PCM downloads successfully (1,003,520 bytes), but this evidence still identifies the older implementation. This remains a merge blocker under the new-extension evidence gate. Run the full guarder suite against the current head and attach that run's command/result screenshot and matching PCM, identifying the tested SHA and playback settings.

[P2] Prevent queued requests from starting during shutdown cleanup

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:150

With dumping enabled, keep P in a slow terminal dump flush, have A currently synthesizing and B buffered, then stop. This snapshot includes P and A, but the base input loop keeps running until super().on_stop(). While shutdown awaits P, A can complete and its finish_request() releases B. B starts outside the snapshot; the later unconditional self._request = None also prevents base flush from finding it. Its worker is never explicitly stopped and its recorder is never finalized, losing buffered PCM. Block new request starts as soon as shutdown begins and retain ownership of every worker until cleanup completes. Add a regression with a slow previous dump and a queued successor that starts while shutdown waits.

[P2] Give each dump a collision-resistant filename

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:274

Distinct valid request IDs such as a/b and a_b both resolve to kotoba_tts_dump_a_b.pcm. PCMWriter appends to existing files, so sequential requests concatenate their audio into one dump; overlapping finalization can also write both requests to that file. Reusing an ID in a later extension session has the same problem. The resulting dump no longer equals the PCM emitted for one request. Use a filename that preserves distinct request identities and distinguishes new request instances, and cover colliding sanitized IDs plus reuse of an existing dump directory.

[P2] Cover long text through the extension adapter

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_state_machine.py:24

The adapter tests send only a few short sentences; the hard-limit test exercises SentenceBuffer alone with 23 characters. No local case drives text beyond the actual 240-character hard boundary, or many queued text segments, through the extension while synthesis is slow. Consequently, preservation and draining of long input, bounded segment buffering, and cancellation/recovery with a text backlog remain unverified. These are required by the local robustness matrix. Add deterministic long single-delta and streamed-delta cases that assert complete ordered vendor text, bounded buffering, no replay after flush, and successful subsequent synthesis.

…hed request; refuse requests once stopping

- on_stop: a request that never reached its end (no text_input_end) gets its
  worker stopped and its dump closed, without tts_audio_end; the base class
  emits none on shutdown either and the guarder's append test relies on it.
  Requests already in their terminal path are still completed.
- on_stop sets _stopping first and request_tts refuses new input while it is
  set, so a request the base class releases during the shutdown waits (A
  flushing its dump, B completing, C queued) cannot start behind the snapshot;
  the snapshot is re-read until no request is left.
- test: test_request_released_during_shutdown_never_starts.
@github-actions

Copy link
Copy Markdown

[P1] Refresh guarder evidence for the current PR head

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment identifies 5765cce6, while the current head is da754268. Its screenshot is accessible, the log reports 16 passed and 1 skipped, and its PCM downloads successfully (1,003,520 bytes), but that run predates the terminal-path and shutdown changes. This remains a merge blocker under the new-extension evidence gate. Run task tts-guarder-test EXTENSION=kotoba_tts_python against the current head with real service credentials, and attach that run's command/result screenshot and matching downloadable PCM, identifying the tested SHA and playback settings.

[P2] Give each request instance a collision-resistant dump filename

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:298

With dumping enabled, distinct valid request IDs such as a/b and a_b both resolve to kotoba_tts_dump_a_b.pcm. PCMWriter opens files in append mode, so sequential requests concatenate their PCM into one dump; overlapping finalization can also interleave their writes. Reusing an ID after restarting with the same dump directory has the same effect. The file therefore no longer represents one request's emitted audio. Include a collision-resistant request identity and a unique request-instance suffix, and cover sanitized-ID collisions and an existing dump directory.

[P2] Cover long text through the extension adapter

ai_agents/agents/ten_packages/extension/kotoba_tts_python/tests/test_state_machine.py:24

The adapter tests use only a few short sentences; the hard-limit test exercises SentenceBuffer alone with 23 characters. No local test drives text beyond the default 240-character hard boundary, or a substantial queued text backlog, through the extension while synthesis is slow. This leaves long-input preservation, buffering bounds, and cancellation/recovery with pending text unverified, contrary to the required local robustness matrix. Add deterministic long single-delta and streamed-delta cases asserting complete ordered vendor text, bounded buffering, no replay after flush, and successful subsequent synthesis.

@q-cheng

q-cheng commented Oct 16, 2026

Copy link
Copy Markdown
Author

Round 4: terminal path restructured

Codex's re-review of 5765cce6 listed five remaining items. Rather than patch each window, this round replaces the three partial cleanups (worker, flush, shutdown) with one terminal sequence and adds the missing error cases. Commits 32ad1a1b and da754268.

# Finding Change
1 Usage counters leak when the join times out before send_usage_metrics _finalize() records each step (error_sent, audio_end_sent, usage_sent, finished) right after its await. A flush or stop that cancels the worker completes only the steps still unset, so usage is always emitted exactly once for the request that owns it. Test: test_flush_takeover_after_audio_end_still_reports_usage.
2 Recorder detached before its flush, so a cancelled cleanup loses PCM _close_recorder() detaches the writer only after the flush loop, and each flush() is shielded so a cancelled caller does not cancel the writer's own task (which has already taken the bytes out of the buffer). Test: test_stop_during_gated_dump_flush_completes_the_file.
3 A finishing worker becomes unreachable once _request is replaced by the next request while its dump is still writing Requests stay in _finishing from the start of their terminal path until the dump is closed; on_stop joins all of them, not only _request. Flush waits on a per-request settled event (events out) instead of the whole worker, so it never waits on, or cancels, a dump flush. Test: test_next_request_during_previous_dump_flush_then_stop.
4 TTFB includes the awaited connect-delay metric send The clock is read in _emit_audio() before any await. The connect-delay value is parked on the request before its send so an interrupted send is repeated, not lost. Controlled-clock tests unchanged and passing.
5 No tests for quota / rate limit or service-side text rejection A 429 handshake maps to rate_limited (non-fatal, retried once on a fresh socket); request-level codes (invalid_text, text_too_long) are non-fatal and not retried. Fake server knob handshake_status_count. Tests: test_transient_rate_limit_on_handshake_is_retried_silently, test_persistent_rate_limit_is_non_fatal_and_recoverable, test_text_rejected_by_the_service_is_non_fatal_and_not_retried.

Flush and shutdown can run concurrently (the runtime dispatches on_data and on_stop as independent tasks), so _finalize() holds a per-request lock; a second entrant finds every step done. The interaction of worker, flush, shutdown and next request at every await is written out in the extension's design notes and was checked against the final code.

Two more items from the same pass:

  • Shutdown emits no terminal events for a request that never reached its end (the base class emits none either); the first live run of this round showed the guarder's append test relies on that.
  • Agora's bot on the first push of this round: a request released by the base class while on_stop waits (A flushing its dump, B completing, C queued) could start behind the snapshot. on_stop now sets _stopping first, which request_tts honours, and loops until no request is left. Test: test_request_released_during_shutdown_never_starts.
  • The stray .coverage data file is removed from the package.

Verification on da754268: tests/bin/start 100 passed, 1 xfailed, five consecutive runs identical; pylint with the TEN rc file 10.00/10; black -l 80 clean. Live guarder on da754268: 16 passed, 1 skipped (389.98 s); evidence comment above updated. For the record, the first live run on this HEAD failed test_invalid_text_handling because two of its 27 fal handshakes did not complete within the test's 5 s window (no error from fal, the other 25 took 0.6 to 1.0 s); nothing in that path changed this round, and the rerun passed. The SDK's handshake timeout is 15 s, so a shorter, configurable connect timeout with the existing one-retry path is a possible follow-up, not part of this PR.

…t per init-failed request; unique dump files; long-input tests

- config: _redact_url masks every query value and the userinfo instead of
  matching a list of sensitive names (access_token, X-Amz-Signature and any
  other name are covered); _SENSITIVE_KEYS removed, params values were
  already all masked.
- extension: the init-failed branch of request_tts runs after the
  finished-id check and records the id after its single error and
  audio_end(ERROR), so further deltas of the same request only release the
  base queue instead of repeating the terminal events.
- extension: dump files are named <sanitized id>_<sha256[:8] of the raw id>
  and truncated on open, so ids that sanitize alike never share a file and a
  stale file is never appended to.
- tests: URL redaction unit table, endpoint secrets under arbitrary query
  names, init failure across deltas, colliding dump ids, stale dump file,
  and test_long_input.py (2,000+ character input, 60 streamed deltas under
  backlog, flush under a 30-sentence backlog with recovery).
@github-actions

Copy link
Copy Markdown

[P1] Refresh the guarder evidence and attach the required screenshot

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment identifies da754268, while the current head is 6bd6e2b0. Its log reports 16 passed and 1 skipped, and its PCM downloads successfully (1,244,160 bytes), but these artifacts identify the older run. The comment also promises a subsequent screenshot, which is absent from the current issue comments, review comments, and reviews. This remains a merge blocker under the new-extension evidence gate. Run task tts-guarder-test EXTENSION=kotoba_tts_python against the current head with real service credentials, and attach that run's redacted command/result screenshot and matching downloadable PCM, identifying the tested SHA and playback settings.

[P2] Let active-request flush finish before waiting for dump I/O

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:486

With dumping enabled, flush an actively streaming request while an automatic PCM write is still pending or the final file write is slow. _interrupt() awaits _finalize() here, including its _close_recorder() call, which awaits the disk write without a time bound. The settled event and _TERMINAL_JOIN_SECONDS only avoid this wait when flush finds a request already terminalizing. On this active-request path, the base cannot emit tts_flush_end or reopen its input gate until the dump finishes, so slow or stalled debug storage stalls subsequent text processing despite synthesis already being cancelled. Run dump cleanup in a tracked terminal task and let flush wait only for terminal-event settlement, retaining the shutdown join. Add a regression that holds the dump write pending, flushes during streaming, and checks that tts_flush_end arrives before releasing the write and that shutdown still finalizes the exact PCM.

@q-cheng

q-cheng commented Oct 16, 2026

Copy link
Copy Markdown
Author

Round 5: four findings on da754268, fixed in 6bd6e2b0

# Finding Change
1 Custom URL credentials can leak through the query-name list _redact_url() no longer uses a name list: every query value is masked (access_token=***, X-Amz-Signature=***), userinfo becomes ***@, key names stay visible. The list _SENSITIVE_KEYS is gone; params values were already all masked. Tests: test_redacted_base_url_masks_every_query_value (unit, parametrized) and test_event_metadata_never_carries_endpoint_secrets now carries access_token and X-Amz-Signature values and asserts none reach any event payload.
2 Initialization failure emits duplicate terminal events per delta The init-failed branch now runs after the finished-id check and records the id with _remember_completed() after its one error and one audio_end(ERROR); later deltas of the same id take the late-input path, which only releases the base queue. Test: test_init_failure_reports_each_request_once_across_deltas (three deltas, one error, one end, next request still reported).
3 PCM dump filenames collide; PCMWriter appends dump_file_name() = sanitized id + 8 hex of a SHA-256 of the raw id, and the file is truncated when the recorder opens. Tests: test_colliding_request_ids_get_separate_dump_files (a/b vs a_b, each file exact) and test_existing_dump_file_is_replaced_not_appended.
4 Long-input coverage tests/test_long_input.py: one 2,000+ character input segmented into 40 sentences and spoken in order on one connection; 60 streamed deltas forming 30 sentences, order preserved under backlog; 30 sentences queued then flushed while the first streams: one INTERRUPTED end before tts_flush_end, at most two texts reached the service, no late frames, next request recovers on the same socket.

Verification: tests/bin/start 110 passed, 1 xfailed, three consecutive runs identical; pylint 10.00/10; black -l 80 clean. Live guarder on 6bd6e2b0: 16 passed, 1 skipped (387.57 s); evidence comment above updated.

With dumping enabled, flushing an actively streaming request awaited the dump
close inside _finalize, so a slow debug disk delayed tts_flush_end and held
the base class's input gate although synthesis was already cancelled.

- The dump close runs as a tracked task on the request (_close_dump), started
  once every terminal event is out; flush returns at `settled` on both of its
  paths. The request stays in _finishing until the task completes.
- on_stop joins the worker, runs _finalize, joins the dump task (bounded by
  _TERMINAL_JOIN_SECONDS), then closes the dump inline for anything left: it
  is the one caller that waits for the file. _join_terminal_worker becomes
  _join_bounded and serves both tasks.
- test: test_flush_during_streaming_does_not_wait_for_dump_io.
@github-actions

Copy link
Copy Markdown

[P1] Refresh guarder evidence for the current head and attach the screenshot

ai_agents/agents/ten_packages/extension/kotoba_tts_python/manifest.json:3

The owner's evidence comment identifies 6bd6e2b0, while the current head is fd719c3a, which changes dump cleanup and flush behavior. The linked log reports 16 passed and 1 skipped, and its PCM downloads successfully (926,720 bytes), but those artifacts belong to the older implementation. The promised result screenshot is also absent from the current issue comments, review comments, and reviews. This remains a merge blocker under the new-extension evidence gate. Run task tts-guarder-test EXTENSION=kotoba_tts_python against the current head with real service credentials, then attach that run's redacted command/result screenshot and matching downloadable PCM, identifying the tested SHA and playback settings.

[P2] Handle cancellation of the joined worker during shutdown

ai_agents/agents/ten_packages/extension/kotoba_tts_python/extension.py:613

If a worker stalls in its terminal events, flush starts _wait_settled(), and shutdown starts shortly afterward, on_stop() waits for that same worker here. Flush's earlier timeout calls _stop_worker() and cancels it. Shielding prevents cancellation from propagating into the worker, but cancellation of the worker itself still raises CancelledError in this await; neither the timeout handler nor except Exception catches it. Consequently, shutdown exits before recorder/client/base cleanup, and the runtime's stop wrapper treats the escaping cancellation as an uncaught exception and calls os._exit(1). Treat cancellation of the joined task as a completed join while preserving cancellation directed at shutdown itself. Add a regression that gates a terminal event, starts flush before stop, and verifies that flush takeover does not abort shutdown and that dumps and transport are closed.

@q-cheng

q-cheng commented Oct 16, 2026

Copy link
Copy Markdown
Author

Round 6: flush no longer waits on dump I/O

Finding: with dumping enabled, flush of an actively streaming request awaited the dump close inside _finalize, so a slow or stalled debug disk delayed tts_flush_end and held the base class's input gate although synthesis was already cancelled. Fixed in fd719c3a.

  • The dump close runs as a tracked task on the request (_close_dump), started once every terminal event is out. Flush returns at settled on both of its paths. The request stays in _finishing until the task completes.
  • Shutdown keeps the join: worker, then _finalize, then the dump task (bounded by _TERMINAL_JOIN_SECONDS), then an inline _close_recorder for anything left. It is the one caller that waits for the file, and a cancelled task leaves the recorder attached so the inline close joins the writer's still-running flush.
  • Test: test_flush_during_streaming_does_not_wait_for_dump_io holds the first dump write, flushes while streaming, asserts tts_flush_end within the flush budget while the write is still held, runs a second request to completion meanwhile, releases the write, and checks both PCM files are exact after shutdown.

Verification: tests/bin/start 111 passed, 1 xfailed, three consecutive runs identical; pylint 10.00/10; black -l 80 clean. Live guarder on fd719c3a: 16 passed, 1 skipped (389.62 s); evidence comment above updated.

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.

1 participant