fix(bot): two recording-path defects — false lag warning (#565) and audio lost on a failed conversion (#520) #617

Merged
claude-bot merged 2 commits from fix/565-520-recording-path into main 2026-09-12 18:01:09 +00:00
Collaborator

Two independent bot-only fixes, bundled because both touch cogs/recording.py and would otherwise conflict on the same function.

#565 — the live panel reported every track six times too short

recording_status.py divided both of its byte counters by a private _BYTES_PER_SECOND = 192_000 (48 kHz stereo). Since #176 the sink converts to 16 kHz mono in the capture path, so bytes_written() and speech_bytes_written() have both counted 32,000 bytes/second since v4.1.0.

Every track therefore read at a sixth of its true length, and the panel's tripwire (track_seconds < elapsed_seconds - 60, judged past two minutes) fired for every speaker on every session — telling a GM to stop and report a healthy recording, and making a real clock loss (#320, #327) indistinguishable from the false alarm.

The conversion now takes its rate from rec.sink.BYTES_PER_SECOND rather than restating it, so the status helper cannot drift from the sink that produced the bytes. No API contract change — field names and meanings are as documented, only the numbers become right. Three docstrings left stale by #176 are corrected alongside.

#520 — a failed conversion deleted the audio it never reached

_process unlinked every remaining entry of raw_track_files in a finally. On the success path that list is already empty (each track is released as its WAV is written, or when it is dropped as silent), so the sweep only ever ran after a failure — and deleted exactly the raw tracks that failure had left unconverted. Those files are the only copy of that speaker's audio.

Keeping them was not sufficient on its own, because the two startup passes would then have fought over the same session:

  • _retry_pending_handoffs (setup_hook) runs first and would have posted the half-converted directory as though it were whole, with no duration_seconds on it to drive the backend's coverage guards (#421);
  • recover_interrupted_recordings would then have found the handoff marker and dropped the surviving raw tracks as already submitted — so the audio kept would have been deleted on the next start anyway.

So a half-converted directory is now marked not-to-retry, leaving recovery as the only pass that will touch it, and it records the two things only the interrupted run knows: the session's true length (the surviving tracks understate it whenever the longest one had already converted) and the names of the speakers already converted, who have no raw track left to be named from. Recovery reads both back, adds to the directory rather than replacing it, and marks itself the same way while it is filling it.

Behaviour change worth a second look

A session left half-converted is no longer auto-posted in its partial form by the handoff retry; it waits for the next bot start, when recovery completes it and posts the whole thing. That is a deliberate trade — a partial transcript that #397's guard would then refuse to replace is worse than a short wait — but it is a change in when a failed session reaches the backend.

Tests

439 passed (bot suite). Each new test was confirmed to fail against the pre-fix source:

  • #565: seconds derive from the sink's own rate; the tripwire stays quiet for a track level with the clock.
  • #520: conversion failing on the second of three tracks leaves the third (and the second) on disk while the first's WAV is kept; the half-converted directory is left for recovery, not for retry; recovery finishes such a directory without losing the earlier run's speakers or its measured length; a recovery that itself dies partway leaves no postable fragment.

One test drafted for this PR was dropped because it passed against the unfixed source too — it asserted nothing this change affects.

Closes #565
Closes #520

🤖 Generated with Claude Code

Two independent bot-only fixes, bundled because both touch `cogs/recording.py` and would otherwise conflict on the same function. ## #565 — the live panel reported every track six times too short `recording_status.py` divided both of its byte counters by a private `_BYTES_PER_SECOND = 192_000` (48 kHz stereo). Since **#176** the sink converts to 16 kHz mono *in the capture path*, so `bytes_written()` and `speech_bytes_written()` have both counted 32,000 bytes/second since v4.1.0. Every track therefore read at a sixth of its true length, and the panel's tripwire (`track_seconds < elapsed_seconds - 60`, judged past two minutes) fired for every speaker on every session — telling a GM to stop and report a healthy recording, and making a real clock loss (#320, #327) indistinguishable from the false alarm. The conversion now takes its rate from `rec.sink.BYTES_PER_SECOND` rather than restating it, so the status helper cannot drift from the sink that produced the bytes. **No API contract change** — field names and meanings are as documented, only the numbers become right. Three docstrings left stale by #176 are corrected alongside. ## #520 — a failed conversion deleted the audio it never reached `_process` unlinked every remaining entry of `raw_track_files` in a `finally`. On the success path that list is already empty (each track is released as its WAV is written, or when it is dropped as silent), so the sweep only ever ran after a failure — and deleted exactly the raw tracks that failure had left unconverted. Those files are the only copy of that speaker's audio. Keeping them was not sufficient on its own, because the two startup passes would then have fought over the same session: - `_retry_pending_handoffs` (setup_hook) runs first and would have posted the half-converted directory as though it were whole, with no `duration_seconds` on it to drive the backend's coverage guards (#421); - `recover_interrupted_recordings` would then have found the handoff marker and dropped the surviving raw tracks as already submitted — so the audio kept would have been deleted on the next start anyway. So a half-converted directory is now marked not-to-retry, leaving recovery as the only pass that will touch it, and it records the two things only the interrupted run knows: the session's **true length** (the surviving tracks understate it whenever the longest one had already converted) and the **names of the speakers already converted**, who have no raw track left to be named from. Recovery reads both back, adds to the directory rather than replacing it, and marks itself the same way while it is filling it. ### Behaviour change worth a second look A session left half-converted is no longer auto-posted in its partial form by the handoff retry; it waits for the next bot start, when recovery completes it and posts the whole thing. That is a deliberate trade — a partial transcript that #397's guard would then refuse to replace is worse than a short wait — but it is a change in when a failed session reaches the backend. ## Tests `439 passed` (bot suite). Each new test was confirmed to fail against the pre-fix source: - #565: seconds derive from the sink's own rate; the tripwire stays quiet for a track level with the clock. - #520: conversion failing on the second of three tracks leaves the third (and the second) on disk while the first's WAV is kept; the half-converted directory is left for recovery, not for retry; recovery finishes such a directory without losing the earlier run's speakers or its measured length; a recovery that itself dies partway leaves no postable fragment. One test drafted for this PR was dropped because it passed against the unfixed source too — it asserted nothing this change affects. Closes #565 Closes #520 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The live Recording panel divided both of its byte counters by a private
`_BYTES_PER_SECOND = 192_000` (48 kHz stereo). Since #176 the sink converts
to 16 kHz mono *in the capture path*, so `bytes_written()` and
`speech_bytes_written()` have both counted 32,000 bytes/second since v4.1.0.
Every track therefore read one sixth of its true length, and the panel's
tripwire (`track_seconds < elapsed_seconds - 60`, judged past two minutes)
fired for every speaker on every session — advising a GM to stop and report a
healthy recording, and making a real clock loss (#320, #327) indistinguishable
from the false alarm.

The conversion now takes its rate from `rec.sink.BYTES_PER_SECOND` rather than
restating it, so the status helper cannot drift from the sink that produced
the bytes. No API contract change: field names and meanings are as documented,
only the numbers become right.

Also corrects three docstrings/comments left stale by #176 — the sink's
summary line, `audio_files`, and the "resample" comment in `_process`, which
has written a WAV header rather than re-encoding since the same change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(bot): keep the audio a failed conversion never reached (#520)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m37s
CI / Docker image build (pull_request) Successful in 20s
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Bot/backend version sync (pull_request) Successful in 37s
CI / Bot tests and audit (pull_request) Successful in 2m35s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m16s
CI / Backend migration, tests, and audit (pull_request) Successful in 8m40s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 21m44s
26e4af1410
`_process` unlinked every remaining entry of `raw_track_files` in a `finally`.
On the success path that list is already empty — each track is released as its
WAV is written, or when it is dropped as silent — so the sweep only ever ran
after a failure, and deleted exactly the raw tracks that failure had left
unconverted. Disk full partway through the loop was the worst case: the tracks
not yet reached were intact, and were deleted anyway. Those files are the only
copy of that speaker's audio.

They are now left on disk, which is what lets `recover_interrupted_recordings`
finish the session at the next start (#399) — but that alone would not have
held, because the two startup passes would then have fought over the same
session:

- `_retry_pending_handoffs` runs first (setup_hook) and would have posted the
  half-converted directory as though it were whole, with no duration on it to
  drive the backend's coverage guards (#421);
- `recover_interrupted_recordings` would then have found the handoff marker
  and dropped the surviving raw tracks as already submitted.

So a directory left half-converted is now marked not-to-retry, which leaves
recovery as the only pass that will touch it, and it records two things only
the interrupted run knows: the session's true length (the surviving tracks
understate it whenever the longest one had already converted) and the names of
the speakers already converted, who have no raw track left to be named from.
Recovery reads both back, adds to the directory rather than replacing it, and
marks itself the same way while it is filling it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch fix/565-520-recording-path 2026-09-12 18:01:10 +00:00
Sign in to join this conversation.
No description provided.