Skip to content

fixes: surface TTS API errors, plug session leak, clip mic overdrive - #602

Merged
yisding merged 3 commits into
mainfrom
agent/bug-hunt-fixes
Jul 22, 2026
Merged

fixes: surface TTS API errors, plug session leak, clip mic overdrive#602
yisding merged 3 commits into
mainfrom
agent/bug-hunt-fixes

Conversation

@yisding

@yisding yisding commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes six verified bugs found in a multi-agent audit of the codebase. Each finding was adversarially verified (3 independent reviewers per finding) before fixing, and every fix lands with a regression test confirmed to fail without it.

High severity

  • session: Session.stop() never unregistered the armed emergency-export exporter, so the exporter closure (holding a strong reference to the Session) stayed pinned in the process-wide export registry after a clean stop — an unbounded leak of stopped Sessions in long-running servers. stop() now unregisters after _mark_closed(); the unregister is idempotent and uninstalls the shared excepthook/atexit hooks when it drains the last entry.
  • tts (OpenAI + ElevenLabs HTTP): on a streamed 4xx/5xx, the HTTPStatusError handler's exc.response.text raised httpx.ResponseNotRead because the streamed body was never read — the real API error escaped as a stream error and no provider Error event reached the journal. The body is now read while the stream is still open (await response.aread() before raise_for_status(); reading in the handler would hit StreamClosed since the context manager closes the stream first).

Low severity

  • transports: LocalTransport converted float32 mic buffers with (arr * 32767).astype(np.int16) and no clip, so overdriven samples (>1.0) sign-flipped into opposite-polarity clicks feeding AEC/VAD/STT. Now clips to int16 range like the resampler sites.
  • runtime: FrozenJournalSnapshot.latest_sequence returned the degraded-mode out-of-band marker's sequence (-1) for a degraded in-memory postmortem view, breaking cursor math across the stop() boundary. Now skips negative markers, matching the SQLite path's MAX(sequence) behavior and the "0 when empty" contract.
  • cli: validate report raised TypeError on a report whose duration_s is JSON null. Now handled.

Test plan

  • New regression tests for all six fixes, each verified to fail without its fix — including real streamed-HTTP error tests via httpx.MockTransport for the two TTS bugs (the existing MagicMock fakes can't reproduce streaming semantics)
  • uv run pytest tests/session tests/transports tests/tts tests/runtime tests/cli -m 'not integration_external' — 2340 passed
  • uv run ruff check . and uv run ruff format --check . — clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved validation report handling for missing or invalid durations.
    • Prevented journal failure markers from affecting reported sequence numbers.
    • Ensured sessions fully clean up emergency export handlers when stopped.
    • Clipped overdriven microphone audio to safe PCM limits.
    • Improved error reporting for streamed ElevenLabs and OpenAI TTS responses.
  • Tests

    • Added regression coverage for duration handling, journal sequences, session cleanup, audio clipping, and streamed TTS errors.

Fix six verified bugs found in a multi-agent audit:

- session: unregister the armed emergency-export exporter on clean
  stop() so stopped Sessions no longer stay pinned in the process-wide
  export registry
- tts: read streamed error bodies before raise_for_status() in OpenAI
  and ElevenLabs HTTP synthesis so HTTP errors surface as
  HTTPStatusError with a provider Error event instead of escaping as
  httpx.ResponseNotRead
- transports: clip float32 mic samples before the int16 cast in
  LocalTransport so overdriven input saturates instead of wrapping
- runtime: make FrozenJournalSnapshot.latest_sequence skip negative
  out-of-band markers so degraded postmortem views keep the live
  sequence contract
- cli: handle null duration_s in validate report instead of raising
  TypeError

Each fix lands with a regression test verified to fail without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yisding, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e213918b-63b9-4641-b75c-4e5fde846011

📥 Commits

Reviewing files that changed from the base of the PR and between 66d00f3 and 36b0b32.

📒 Files selected for processing (11)
  • src/easycat/cli/validate.py
  • src/easycat/config/_factory.py
  • src/easycat/runtime/journal_memory.py
  • src/easycat/runtime/journal_views.py
  • src/easycat/tts/elevenlabs_tts.py
  • src/easycat/tts/openai_tts.py
  • tests/cli/test_validate_report_cli.py
  • tests/runtime/test_journal.py
  • tests/tts/test_tts_cancellation.py
  • tests/tts/test_tts_elevenlabs.py
  • tests/tts/test_tts_openai.py
📝 Walkthrough

Walkthrough

The changes harden validation output, degraded journal snapshots, session teardown, microphone PCM conversion, and streamed TTS error handling. Regression tests cover null durations, degraded markers, exporter cleanup, audio saturation, and provider HTTP errors.

Changes

Report formatting

Layer / File(s) Summary
Duration normalization and CLI coverage
src/easycat/cli/validate.py, tests/cli/test_validate_report_cli.py
Invalid or missing durations render as 0.00s, while numeric values retain two-decimal formatting; null-duration CLI behavior is tested.

Runtime state and lifecycle cleanup

Layer / File(s) Summary
Journal sequence preservation
src/easycat/runtime/journal_views.py, tests/runtime/test_journal.py
Snapshots ignore negative degraded markers when calculating the latest live sequence.
Emergency exporter teardown
src/easycat/session/_session.py, tests/session/test_session_lifecycle_teardown.py
Clean session shutdown unregisters emergency exporters and removes the shared exception hook.

Audio input conversion

Layer / File(s) Summary
Microphone PCM saturation
src/easycat/transports/local.py, tests/transports/test_local_transport.py
Microphone samples are clipped to signed int16 limits before conversion, with overdriven input coverage.

Streamed TTS error handling

Layer / File(s) Summary
Provider streamed-error handling
src/easycat/tts/elevenlabs_tts.py, src/easycat/tts/openai_tts.py
Error response bodies are read before status handling in both streaming synthesis paths.
Streamed-error test coverage
tests/tts/test_tts_cancellation.py, tests/tts/test_tts_elevenlabs.py, tests/tts/test_tts_openai.py
HTTP stream fakes and regression tests verify status errors and emitted provider error events.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: claude

Poem

I’m a rabbit with patches to queue,
Clipping sound and fixing streams anew.
Journals keep pace, hooks hop away,
Null times now render just right today.
TTS errors speak clear in the dew.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes several core fixes in the changeset, including TTS error handling, session cleanup, and microphone clipping.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/bug-hunt-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@charliecreates charliecreates 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.

I found one blocking correctness issue in degraded in-memory journal snapshot sequence accounting; see the inline comment.

Comment thread src/easycat/runtime/journal_views.py Outdated
return self._records[-1].sequence if self._records else 0
# Skip out-of-band markers (e.g. the degraded marker at sequence -1) so
# the postmortem value matches the live InMemoryRingBuffer counter.
return max((r.sequence for r in self._records if r.sequence >= 0), default=0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With a valid InMemoryRingBuffer(capacity=1), a degraded transition appends the sequence=-1 marker into a full deque and evicts the only nonnegative record. This max(...) then returns 0 even though the live ring's _seq remains 1 (and can be higher after overflow markers), so post-stop latest_sequence still diverges from the live cursor contract. Preserve/pass the live sequence counter into FrozenJournalSnapshot rather than deriving it solely from retained records, and add a capacity=1 regression test.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/easycat/cli/validate.py`:
- Around line 854-857: Update _format_duration to convert numeric inputs safely,
rejecting negative, NaN, and infinite values in favor of "0.00s". Catch
OverflowError when converting excessively large integers, and preserve the
existing boolean/non-numeric fallback and two-decimal formatting for valid
finite non-negative durations.

In `@src/easycat/tts/elevenlabs_tts.py`:
- Around line 266-270: Update the streamed response checks in both
elevenlabs_tts.py lines 266-270 and openai_tts.py lines 150-154 to use the
response’s non-success condition rather than is_error, and await
response.aread() before raise_for_status(). This must cover redirects and all
other non-2xx responses so the exception handler can safely access the response
body.

In `@tests/tts/test_tts_cancellation.py`:
- Around line 47-48: Update the test-double method raise_for_status to include
the explicit -> None return annotation, preserving its existing no-op behavior.

In `@tests/tts/test_tts_elevenlabs.py`:
- Around line 433-435: Update the helper’s __aiter__ method to explicitly return
AsyncIterator[bytes], and add the required typing import. Preserve the existing
iteration over self._chunks and yielded byte chunks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e73e84b5-a71f-40de-8524-2e07ff824f41

📥 Commits

Reviewing files that changed from the base of the PR and between 437b0e0 and 66d00f3.

📒 Files selected for processing (13)
  • src/easycat/cli/validate.py
  • src/easycat/runtime/journal_views.py
  • src/easycat/session/_session.py
  • src/easycat/transports/local.py
  • src/easycat/tts/elevenlabs_tts.py
  • src/easycat/tts/openai_tts.py
  • tests/cli/test_validate_report_cli.py
  • tests/runtime/test_journal.py
  • tests/session/test_session_lifecycle_teardown.py
  • tests/transports/test_local_transport.py
  • tests/tts/test_tts_cancellation.py
  • tests/tts/test_tts_elevenlabs.py
  • tests/tts/test_tts_openai.py

Comment thread src/easycat/cli/validate.py Outdated
Comment thread src/easycat/tts/elevenlabs_tts.py Outdated
Comment thread tests/tts/test_tts_cancellation.py Outdated
Comment thread tests/tts/test_tts_elevenlabs.py Outdated
@yisding
yisding merged commit 2939e5b into main Jul 22, 2026
14 checks passed
@yisding
yisding deleted the agent/bug-hunt-fixes branch July 22, 2026 12:48
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