[Backend] The silence guard can delete a quiet player from a full-length session #425
Closed
opened 2026-08-27 23:25:20 +00:00 by claude-bot
·
2 comments
No Branch/Tag specified
main
fix/hotword-list-echo
fix/table-stop-ends
feat/1328-fill-it-in
feat/1328-fill-frontend
feat/1328-fill-backend
feat/9-foundry-push
fix/9-foundry-live
feat/9-foundry-push-frontend
renovate/celery-5.x
feat/5-foundry-connect
feat/9-foundry-push-backend
feat/9-foundry-push-module
feat/5-foundry-frontend
feat/5-foundry-backend
feat/5-foundry-module
release/v4.8.0-rc1
docs/d341-convert-draft
feat/958-update-to-version
feat/958-convert-sheet
feat/958-pathbuilder-import-frontend
fix/1360-heading-order
feat/958-pathbuilder-import-backend
feat/11-weekly-digest
feat/1341-auto-update
docs/d332-superseded
chore/1345-names-cleanup
feat/1342-mentioned-pages
feat/1328-recipes
fix/1346-pgdg-key
perf/1247-shared-recording-stream
chore/132-gitleaks-allowlist
release/v4.7.1
spike/foundry-module
release/v4.7.1-rc1
docs/decisions-d332-d337
fix/bot-certifi
chore/release-4.7.0
chore/release-4.7.0-rc4
chore/deps-4.7.0
chore/release-4.7.0-rc3
fix/1280-quote-must-say-it
fix/1257-names-row-squeeze
chore/release-4.7.0-rc2
fix/1245-pg-upgrade-count
chore/release-4.7.0-rc1
feat/1257-names-in-prep
feat/1257-be
feat/1257-fe
feat/1257-rating-fixes
design/1328-fill-it-in
feat/1257-name-splits
fix/1326-redis-503
feat/1280-fact-suggestions
feat/1280-be
feat/1280-fe
design/1280-fact-suggestions
feat/1279-infobox
feat/1279-be
feat/1279-fe
feat/1321-shelf-copies
design/1279-infobox
feat/1243-table-find
feat/1257-who-rated
feat/1243-prep-drawer
feat/1243-sheet-pickers
design/1243-browse-in-place
feat/1315-hotwords-default
feat/1242-system-filters
feat/1257-more-name-styles
feat/506-leg3-hotwords
design/1242-system-filters
fix/1302-packs-polish
feat/1241-rules-library
design/1241-rules-library
feat/1245-postgres-18
integ/4.6.2
fix/1263-map-ui-2
fix/1263-map-server
fix/1263-map-ui-1
fix/1261-editor-link-menu
fix/1229-axe-4.14-name-mismatch
renovate/axllent-mailpit-1.x
renovate/eslint-monorepo
renovate/authlib-1.x
chore/release-4.6.0
chore/release-4.6.0-rc2
fix/460-rc2-screens
fix/460-rc2-server
fix/release-prerelease-exit
chore/release-4.6.0-rc1
feat/125-campaign-templates
feat/126-calendar
feat/1181-pictures-and-files
feat/1190-event-people
integ/4.6.0
feat/1191-line-of-the-night
feat/1200-starter-packs
chore/bot-multidict-and-playwright-group
fix/1202-this-week-answered-count
fix/122-124-leftovers
fix/1178-get-routes-that-write
fix/1203-journeys-check-names
design/125-campaign-templates
feat/126-calendar-engine
design/126-in-world-calendar
fix/1189-cited-stamp-in-range
fix/1189-v4-looser-check
fix/1189-richer-beats
fix/1113-export-bind-lists
fix/1154-release-notes-by-file
chore/release-4.5.0
chore/release-4.5.0-rc12
fix/1135-wiki-markup-as-text
fix/demo-usage-cost-in-window
chore/release-4.5.0-rc11
fix/1121-sheet-consistency
fix/1121-sheet-5e-terms-and-card-hp
copy/1122-busiest-pages
fix/1121-relevant-choices
fix/1121-wiki-create-npc-save-sheet-links
fix/1029-menu-stays-on-screen
docs/prototype-transcript-bin-wording
fix/1023-editor-save-row-back-scroll
fix/1020-search-exact-first
fix/1020-table-prep
fix/1020-shell-wordmark-footer-bell
fix/1020-wiki-article-party
fix/872-admin-kept-until-phone-tables
feat/l13-milestone-level-waiting
fix/1000-1001-bell-older-setup-context
fix/999-pronouns-in-prompts
fix/l19-other-class-feats
fix/1010-transcript-window-ordinary-confirm
docs/d254-d269-decisions
fix/702-member-search
fix/680-transcript-bin
fix/993-unidentified-prompts
fix/841-redis-timeouts
fix/990-binned-not-exported
fix/830-export-prep-shelf
fix/deps-audit-2026-10-01
fix/877-title-reads
fix/798-audio-received-at
build/lane11-17-j7
build/lane11-16-levelup
build/lane11-14-seams
build/lane11-14-seed
build/lane11-15-start
build/lane11-13-old-block-out
fix/980-review-r3
build/lane11-12-block
build/lane11-8-packs
fix/demo-seed-canonical-names
build/lane11-11-hot-half
docs/l11-decisions
build/lane11-7-convert
build/lane11-10-editing
fix/npm-audit-brace-expansion
build/lane11-9-sheet-page
fix/bot-pyjwt
build/lane11-6-content
fix/lane11-rank-names
build/lane11-2b-language
build/lane11-4-js-evaluator
build/lane11-3-server
build/lane11-5a-pf2e
build/lane11-5b-5e
feat/design-admin
build/lane11-2-evaluator
build/lane11-1-sheet-pieces
build/lane11-0-drawing
fix/943-quest-objectives-fk
fix/prepsheet-shelf-flake
fix/945-leftovers
build/demo-gm-copy
fix/901-email-landings
build/lane10-8-journeys
build/demo-seed-lanes-8-10
fix/887-gm-own-confirmation
build/lane9-5-j1-admin
fix/918-audience-click
fix/useasync-overtaken
ci/939-critical-path
test/937-backend-suite-speed
chore/claude-orchestration
fix/924-xp-gm-only
fix/session-stale-poll
docs/decisions-d239-d242
fix/930-pypdf
build/lane8-journeys
build/925-quest-editor
fix/894-private-page-pictures
build/lane8-p4-drafts
build/lane8-p7-map
build/lane8-p6-handouts-rules
fix/wiki-fixture-names
build/lane10-d227-test-usage
fix/rsvp-horizon-test-tuesday
fix/media-volume-owner
build/lane8-p3-editor
fix/912-unidentified-rename
build/lane8-p5-party-quests
build/lane8-handout-takeback
build/lane10-pickers
build/lane8-p2-article
build/lane8-6-map
build/lane10-7b-email-pages
build/lane8-5-handouts
build/lane10-7a-test-the-bot-pages
build/lane8-p1-pages
build/lane8-4-quests
build/lane8-2b-feeds
build/lane8-3-parties
build/lane8-2a-links
build/lane10-5-commands
build/lane9-6-seed
build/lane10-4b-reactions
build/lane9-4d-pages
build/lane9-4c-pages
build/lane10-4a-posts
build/lane10-3-email
fix/891-web-fonts
build/lane9-4b-pages
build/lane10-6-test-the-bot
build/lane10-2-notify-out
build/lane8-1-wiki-text
build/lane9-4a-frame
design/lane10-drawings
fix/j6-budget
fix/j5-phone-flake
fix/869-test-leaks
fix/843-record-start-followups
build/lane7-all
build/lane6-all
build/lane9-b2-server
chore/changelog-fragments
docs/decisions-d218
build/lane9-a-owner-overview
fix/rule-card-level-label
fix/834-retention-semantics
fix/o3c-record-without-announcement
docs/decisions-d179
docs/decisions-d131-d170
fix/851-commit-before-response
build/lane6-j3-seed
build/lane9-b1-d67-identity
fix/841-blocking-loop
build/lane9-c-backups-audit
fix/vote-race-and-start-commit
fix/recording-announcement
fix/proposal-read-leak
fix/last-admin-guard
build/lane6-p4c-planning
build/lane6-p4b-margin
build/lane6-p4a-sheet
build/lane5-j4-seed
build/lane6-base
build/lane7-pages-talk
build/lane5-pages
build/lane7-pages-safety
build/lane7-p3-safety
build/lane7-p2-notify
build/lane6-p3-planning
build/lane6-p2-history
build/lane6-p1b-items
build/lane7-p1-threads
build/lane5-p6-faces
build/lane6-p1a-sheet
build/lane5-p5-find
build/lane5-p4a-board
build/lane5-p4b-scenes
build/lane5-p3-live
build/lane5-base
build/lane5-p2-cards
docs/lane5-decisions
build/review-r2-wide
fix/review-r1
build/lane5-p1-scenes
build/demo-seed
build/lane4-journeys-j5
build/lane4-page-review-b
build/lane4-page-review-a
build/lane4-page-read
build/lane4-journeys
build/lane4-page-sess2
build/lane4-page-sess1
build/lane4-notes
build/lane4-discord-on-approval
build/lane4-scheduling
build/lane4-versions
build/760-session-number
build/lane4-decisions
build/797-erase-no-regenerate
build/test-race-audit
build/lane3-completions
build/794-dark-only
build/796-titles-scroll
build/791-retention-anchor
build/a11y-sweep
build/shell-followups
build/retention-preview
build/753-backup-readback
docs/d91-d93
build/lane3-j1
build/ci-harness-paths-selftest
fix/useauth-test-race
build/ci-harness-paths
build/777-ai-settings-read
build/lane3-settings-c
build/lane3-settings-b
build/lane3-j6
build/lane3-sessionform
build/lane3-settings-a
build/lane3-members
build/lane3-thisweek
build/lane3-story
chore/87-align-python-3.12
v4.8.0-rc1
v4.7.1
v4.7.1-rc1
v4.7.0
v4.7.0-rc4
v4.7.0-rc3
v4.7.0-rc2
v4.7.0-rc1
v4.6.2
v4.6.2-rc4
v4.6.2-rc3
v4.6.2-rc2
v4.6.2-rc1
v4.6.1
v4.6.1-rc1
v4.6.0
v4.6.0-rc2
v4.6.0-rc1
v4.5.3
v4.5.3-rc1
v4.5.2
v4.5.2-rc1
v4.5.1
v4.5.1-rc2
v4.5.1-rc1
v4.5.0
v4.5.0-rc12
v4.5.0-rc11
v4.5.0-rc10
v4.5.0-rc9
v4.5.0-rc8
v4.5.0-rc7
v4.5.0-rc6
v4.5.0-rc5
v4.5.0-rc4
v4.5.0-rc3
v4.5.0-rc2
v4.5.0-rc1
v4.4.1
v4.4.0
v4.3.0
v4.2.3
v4.2.2
v4.2.1
v4.2.0
v4.1.2
v4.1.1
v4.1.0
v4.0.1
v4.0.0
v3.11.4
v3.11.3
v3.11.2
v3.11.1
v3.11.0
v3.10.0
v3.9.1
v3.9.0
v3.8.0
v3.7.0
v3.6.1
v3.6.0
v3.5.0
v3.4.1
v3.4.0
v3.3.0
v3.2.0
v3.1.0
v3.0.0
v2.1.0
v2.0.0
v1.1.0
v1.0.0
v0.12.0
v0.11.0
v0.10.0
v0.9.1
v0.3.3
v0.9.0
v0.8.0
v0.3.2
v0.3.1
v0.3.0
v0.7.0
v0.6.0
v0.5.0
v0.4.0
v0.2.1
v0.2.0
v0.1.1
v0.1.0
Labels
Clear labels
backend
Issue related to the backend
bot
Issue related to the bot
bug
Something is not working
duplicate
This issue or pull request already exists
enhancement
New feature
frontend
Issue related to the frontend
help wanted
Need some help
invalid
Something is wrong
question
More information is needed
recording
Issue related to the recording system
security
Security hardening / vulnerability work
wontfix
This won't be fixed
No labels
backend
bot
bug
duplicate
enhancement
frontend
help wanted
invalid
question
recording
security
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Quest-Board#425
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?
Severity: CRITICAL. Blocks the v4.0.0 merge. Found while verifying #348's acceptance criteria before closing it.
The defect
drop_silent_tracks(app/services/audio_service.py:1305) runs unconditionally on every session (app/tasks/reminder_tasks.py:2158) and drops any track whose_peak_amplitudefalls below the floor. That function samples the file rather than scanning it:Reads are capped at 16,000 frames — one second — while the spacing between them scales with file length. So coverage collapses as sessions get longer:
Stated exactly: the guard only guarantees detection for a speaker whose track contains one contiguous non-silent stretch longer than the window spacing — ~145 s on a 77-minute session. Anything shorter is caught only if a 1-second probe happens to land on it.
Why this is the failure the guard exists to prevent
Sol_Invictus spoke 163 seconds out of 4,639 in the 2026-08-26 session. That is above 145 s in total, but only safe if it was one unbroken block. A quiet player's speech is not one block — it is a few words every several minutes, which is precisely the distribution this sampling cannot see.
The consequence is silent: the track is dropped before transcription, the player vanishes from the transcript, the summary is generated without them, and the pipeline reports success. The GM is told nothing.
The test does not test this
test_one_brief_utterance_is_enough_to_keep_a_track(tests/test_silent_tracks.py:76) cites this exact production incident in its docstring — "Sol_Invictus in the 2026-08-26 session spoke for 163 seconds out of 4639" — and then builds a 60-second file. At that length windows are 1.9 s apart and 1 s wide, so coverage is total and the assertion cannot fail regardless of the bug.It is green, and it validates nothing. Same shape as the double corrected in
6ba6202: a test that models an impossible case will eventually assert that the impossible is fine.Not the bot-side guard
The bot's own drop logic is sound and is not in scope here. It uses
speech_bytes_written()— a running counter of decoded audio accumulated during capture (bot/questboard_bot/cogs/recording.py:296) — against a 0.5 s floor, so it has no sampling blind spot, and it reports dropped users throughuncaptured_member_ids.bot/tests/test_recording.py:1006covers the 163-of-4639 case properly.This issue is only about the backend's second line of defence, which is currently more dangerous than the thing it defends against.
Acceptance criteria
uncaptured_member_ids/ attendance surface the bot-side path uses, rather than a log lineNotes
SPEAKER_MISSING_MIN_SPEECH_SECONDS,audio_service.py:1246). Reusing that measure would remove the sampling question rather than tune it.Fixed in
aa0b6c9.has_audible_speechreads the file sequentially and stops at the first sample above the floor. No sampling, so no length-dependent degradation — and faster in the case that matters, since anyone who speaks ends the loop as soon as they do. Only a genuinely silent track is read to the end, which is the one file worth being certain about.The bug was worse than this issue described
I wrote it up as "coverage collapses on long files", which is true but understates it. Measuring the old implementation against realistic speech shapes, survival came down to whether a player's speech happened to align with the probe positions:
Eighty seconds of speech deleted while six seconds survives. Not monotonic in how much someone spoke, and not a threshold anyone chose — an alignment artefact between two evenly-spaced sequences. Sol_Invictus surviving the real session was luck.
I nearly shipped the same broken test
My first replacement used 40 utterances of 4 seconds at real session length. It looks like the harder case, and it passes — but so does the old code, because evenly-spaced speech resonates with evenly-spaced probes. A test built from it would have passed before and after the fix and proved nothing, which is exactly what the test this issue criticises did.
I caught it by rebuilding the old implementation and running the candidate fixtures against it, rather than assuming a longer file was automatically a better test. The shapes that ship (20 × 4s, 8 × 1s, one word late in an hour) are ones measured to fail before the fix.
Criteria
The two GM-visibility criteria are not done and I am not closing this. They are the same problem as #424 — the pipeline knowing something the GM cannot see — and better solved there than bolted on here. The data-loss half, which is what made this a merge blocker, is fixed.
1,227 passing before, 1,233 after.
Verified against acceptance criteria. Two were met, two were not — the second pair is fixed in
540b0a2.Criteria
Detection does not degrade with file length —
has_audible_speechscans sequentially and stops at the first audible sample. No probing, no spacing that grows with the file. It is also faster in the case that matters: anyone who speaks at all ends the loop when they do, and only a genuinely silent track is read to the end.A test at realistic session length with speech scattered as many short utterances — three of them, on 4,639-second files:
test_a_quiet_player_survives_a_full_length_session,test_speech_scattered_as_single_seconds_survives,test_a_single_word_late_in_a_long_session_is_still_found.And they do fail against the old implementation. I did not take the docstrings' word for it — I restored the pre-fix sampler (32 windows, reads capped at 16,000 frames) into
audio_serviceand re-ran the file: those three fail, the other twelve pass. Given this issue exists becausetest_one_brief_utterance_is_enough_to_keep_a_trackcited a real incident and then built a 60-second file, that check seemed worth doing rather than assuming.→ [x] A dropped track reaches the GM through the same
uncaptured_member_ids/ attendance surface — was not met. Fixed in540b0a2.→ [x] #348's fourth criterion holds for backend-dropped tracks too — was not met. Same fix.
What was missing
drop_silent_tracksreturned its silent list into alogger.infoand nothing else (reminder_tasks.py:2207-2213).uncaptured_member_idswas built solely frompresence.json(reminder_tasks.py:2454-2463), which carries only the members the bot declined to upload.So the backend guard was half a guard. It stopped Whisper inventing speech under a quiet person's name — #348's concern — and then let that person disappear from the transcript with a worker log as the only trace, which is this issue's concern. The GM gets a summary written without them and a pipeline reporting success.
The gap sat exactly where the guard is most needed:
drop_silent_tracksexists for recordings the current bot did not produce — an older session, a hand-placed directory, a bot a version behind — and those are precisely the cases with no bot-side report to fall back on.Track.owner_idis already the Discord user id, so the fix is a merge, not new plumbing. Merged rather than assigned, mirroring the bot's own merge at recording.py:975: the two paths reach the same state differently and either can find someone the other did not.Extracted as
merge_uncaptured_membersrather than left inline —process_audiohas no test harness, and building one to assert four lines would have been disproportionate.Note for #348
Its first criterion — "
_peak_amplitudescans the full file, not the first 0.5 s" — is unmet as worded: it still samples five 64 KB windows (recording.py:374-400). But it is no longer the drop decision. That now usesspeech_bytes_written(), an exact running counter with no sampling blind spot, and_peak_amplitudesurvives only as a log diagnostic — its docstring says so. The letter is unmet; the intent is superseded by something stronger. Flagging rather than quietly ticking it.1,296 → 1,300 passing, lint clean. Closing.