[Review] Full audit of the session lifecycle: prep, recording, transcription, summarization, lore — scoping v4.0.0 #319
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Tracking issue for a full review of everything touching the session lifecycle, ahead of a re-scoped v4.0.0. The FoundryVTT pillar currently sitting on v4.0.0–v4.3.0 moves to v5.x.
Why
Quest Board is being prepared as a hosted product. The core loop — scheduling, session planning, recording, summarization, lore management — has to be bulletproof without hands-on work from the operator or the customer. Two things forced the review:
Scope
Backlog issues to be pulled into the new v4.0.0: #121, #122, #123, #124, #125, #126, #128.
Headline finding so far — recording capture has no session clock
Confirmed against production data. Per-speaker WAV tracks are not on a shared timeline. Each track is that speaker's own speech, concatenated with the silence between their utterances deleted, so a track's duration equals that speaker's total talk time rather than the session's wall-clock duration. Whisper timestamps are therefore track-relative, and
merge_attributed_transcriptsorts values from six mutually incomparable clocks onto one axis.Evidence — session
56cc4dee-99c0-470a-bd90-18e7621b4194(2026-08-12 01:00–04:00 UTC), stored transcript, 1904 lines, 0 non-monotonic, 0 unparsed:Max timestamp in the entire transcript is 00:54:47 for a multi-hour session, and the per-speaker durations sum to ~2.08 h — approximately the real session length, which is the signature of speech-only concatenation.
Mechanism, in
bot/questboard_bot/cogs/recording.py:_session_bytesis documented (line 118) as "Bytes on the first speaker's timeline" and advances only for packets whoseuid == next(iter(self._writers))(lines 183-184, 204-205) — one arbitrary user, whose own track only advances while they transmit.SilenceGeneratorSinkthat would fill gaps in real time; the bot never uses it.7ddff16("Fix per-speaker timestamp alignment", Apr 2026) claims to track wall-clock position. It does not — that is the original misdiagnosis, andtest_recording.py:534-548locks the broken model in.This affects every multi-speaker recording Quest Board has ever produced, not just the 08-11 session.
Status
Review in progress. A written report will land in
docs/.internal/first; issues get filed against the new v4.0.0 milestone once the findings are agreed.Correction to the issue body, plus the recommended fix
Deeper analysis against the pinned library source corrected two claims in the body above. The root cause and the production evidence are unchanged — the corrections are about mechanism, and they matter because one of them would send someone down a dead end.
Corrections
opus.py:162-182), and sequence numbers pause during silence — so inter-spurt gaps generate zero FakePackets. Re-enabling them cannot fix this bug. There is also a documented reason the skip exists: commit02898b3records DAVE-deferred packets causing a 148× PLC explosion (340 MB for 12 s of audio). Do not "fix" this by accepting FakePackets, and disregard the body's suggestion that wiring upSilenceGeneratorSinkis the answer.recording.py:163-166, not 156-161. And the reference user is the first user whose packet reached the sink — theuser is Nonedrop (161-162) and DTX-first packets can each make that someone other than the first person to speak.One further note on evidence: the transcript's zero non-monotonic timestamps proves nothing, since the backend sorts by construction. The load-bearing evidence is the max track end of 00:54:47 against a multi-hour session, plus impossible sustained speech density (Idani at one line per 2.6 s for their whole track).
Recommended fix — wall-clock anchoring in the sink
Give
PerUserPCMSinkat0(the sametime.monotonic()asrec.started_at, with an injectable clock for tests). Per packet:At
close(), tail-pad every track toduration_s. Delete_session_bytes, the reference-uid checks, and the first-packet pad — the gap logic subsumes all three. Placement error is bounded by jitter-buffer delay + 250 ms per spurt and is non-cumulative over a 4-hour session. Loss, mutes, leave/rejoin and reconnects all degrade to silence automatically.Two required companions:
waveto headerless.s16le, converting withffmpeg -f s16le -ar 48000 -ac 2 -i. Gap-filled 48 kHz stereo hits WAV's 4 GB RIFF cap at ~6 h (the current default cap), and headerless also allows sparse-seek gaps.recording.py:170) — a stale decoder garbles audio after a rejoin.Deferred: RTP-timestamp anchoring
The sink does receive
.timestamp/.ssrc/.sequence(libraryopus.py:33-45), and per-SSRC wall-anchored wrapped-diff placement would be frame-accurate. But it assumes every client advances its timestamp across silence — and if any client does not, it silently recreates this exact bug. Ship wall-clock anchoring, logts_delta_vs_wall_deltainAUDIO_DIAGfor one release, and adopt RTP anchoring only if the data justifies it.Regression test (pure unit, no Discord needed)
Fake-clock-injected sink; spurts at t=0–1 s and t=60–61 s for user A, t=120 s for user B;
close()at t=300 s. Assert both WAVs are 300 s ±300 ms with speech bytes at exactly the right byte offsets. This fails today in three independent ways, and it extends the existing_FakeDecoderpattern so it runs in CI as-is. Note thattest_recording.py:534-548currently asserts the broken model and must be rewritten.Tripwires so this can never regress silently
close()logs the expected/written ratio per user — one grep proves the fix on the first real sessionduration_seconds, before spending a GPU run (process_audioalready holdsduration_secondsatreminder_tasks.py:1987-1994and ignores it)duration_secondsseconds_capturedvselapsed_secondsbecomes a real-time post-deploy pass/failReview complete — report landed
Written up at
docs/.internal/session-lifecycle-review-2026-08.md, with the six full audits (≈2,550 lines,file:lineevidence throughout) indocs/.internal/session-review-2026-08/.No issues filed yet — scope gets agreed first, then they go up against a new v4.0.0 milestone.
Headline additions since the last comment
VAD is off in production. There is no
vad_configrow, soVAD_DEFAULT_ENABLED = Falseapplies. That creates a hard sequencing constraint: fixing capture makes every track run the full wall-clock length, taking ASR input from ~2 track-hours to ~17.5, and re-arming Whisper's hallucination-on-silence failure, which is dormant today only because the silence was being deleted. The capture fix and a silence strategy must ship together, or the fix is a regression. The current-30 dBnoise floor is also too aggressive for Discord per-user tracks (which are digitally silent at −∞ dB when nobody transmits) and will cut quiet speech — validate against a real recording before enabling.Counts: 2 critical + 2 high in capture, 2 high in transcription/merge, 2 critical + 2 high in the AI pipeline, 3 critical + 4 high in data durability, 8 major UX findings plus ~30 silent-failure sites.
Three data-loss criticals, none related to the timeline bug:
task_acks_late/task_reject_on_worker_lostanywhere: a routinedocker compose up -d --buildmid-transcription strands the session atprocessingforever, with both retry endpoints 409-ing. SQL-only escape. Eleven such stuck states were enumerated.HANDOFF_MARKERon backend-unreachable, no retry, swept after 60 min).Privacy:
audio_service.py:524-528logsstr(data)[:500]at INFO — that repr opens with segmenttextandspeaker, in direct violation of the module docstring at line 24. Verified on prod: 1 line in retained worker logs.Hosted economics: AssemblyAI Universal async + Claude Sonnet 5, VAD mandatory — ≈$0.95/session, ≈$4.11/customer-month at weekly play. Break-even vs owned hardware is ~60–75 groups once your time and redundancy are priced honestly. Price per group, not per seat.
Proposed tiers
.s16lecontainer + SSRC decoder reset; enable VAD at a corrected floor; the two duration-invariant guards; remove the PII log line; rewritetest_recording.py:534-548(it currently asserts the broken model) and add the wall-clock regression test.Five open decisions are listed in §8 of the report.
Milestones restructured and issues filed
97 new issues (#320–#416) across seven new milestones. The FoundryVTT pillar moved to v5.x to free the v4 line for this work.
Milestone renumbering
The new line
The speculative backlog drops from 14 open issues to 5.
Decisions encoded in the scope
The hotfix is nine issues, not one. #320 is the capture fix; #321 (headerless
.s16le) and #322 (SSRC decoder reset) are marked as required companions, not nice-to-haves — #320 alone hits WAV's 4 GB RIFF ceiling at the 6-hour cap, because gap-filled tracks become wall-clock length. #323 carries the VAD coupling and sets the noise floor to −45 dB, with an explicit instruction to validate against a real recording before production, since cutting quiet speech would be a new way to lose data.No provider commitment. v4.2.0 builds the abstraction; #360 is explicitly decision-support, evaluating candidates against the golden corpus at list rates rather than promotional ones. #352–#354 make self-hosted a measured first-class target including a CPU-only path, with published hardware profiles and accuracy numbers. This also strengthens the v4.0.0 architecture argument: because chronology is a code-side
sorted()and attribution is code-validated, a small local model loses beats to validation rather than silently producing a confidently wrong summary. That property is what makes a CPU tier honest.The UX overhaul follows the stated process. #361 the brief, #362 the 5–10 wireframe concepts, #363 the 3–5 full mockups explicitly framed for comparison and mix-and-match. #363 requires realistic sample data including long text, many items, and empty states — most of the audit's UX findings only surface with real content.
Ordering constraint worth repeating: #398 (
acks_late+ watchdog) must land after #397 (re-submission guard). Enablingacks_latemeans tasks get redelivered after worker loss, and until re-submission is guarded a redelivery clobbers GM-edited transcripts, summaries and curated highlights.Not filed, deliberately
WikiArticle.jsx:1857), a single missing CSS class that makes the page usable at all.One near-duplicate was merged before filing: a
process_audio-specificacks_lateissue and its systemic generalisation became #398.Next: the v3.11.5 hotfix.
Closing — this review's purpose is complete.
It existed to scope a re-planned v4.0.0 and move the FoundryVTT pillar to v5.x. Both happened, and the resulting plan has now shipped:
The two failures that forced the review are both addressed. Chronology is now a
sorted()over beats validated in code rather than a model behaviour, and attribution is a lookup keyed on the file a track was sent as — #342 removed the echo-based join entirely, on the argument that any such join is unverifiable in principle.The review's own discipline held up under use: the "not filed, deliberately" section was right to leave the unverified Discord ephemeral-defer behaviour alone rather than inventing certainty, and the ordering constraint it flagged (#398 must follow #397) still stands for whoever picks up v4.1.0.
Its work product is the milestones. Nothing is tracked here that is not tracked better elsewhere.
claude-bot referenced this issue2026-09-06 00:33:37 +00:00