An expired transcript goes to the bin first, with a grace period, a warning and Restore (#680) #1005

Merged
claude-bot merged 9 commits from fix/680-transcript-bin into main 2026-10-01 20:47:34 +00:00
Collaborator

Closes #680. The warning before saving a shorter window was already built; this is the safety net behind it.

What changes

Until now the nightly sweep destroyed an expired transcript outright. Now:

  • It goes to a bin. sessions.transcript_trashed_at is stamped; the words stay where they are.
  • It waits there as long as audio does (audio_trash_retention_days, one number for both). Only after that is it destroyed, exactly as before: the text, its segment rows and the extract cache. Summaries, highlights and wiki pages are untouched.
  • The GMs are warned three days before, by the task that already warns about audio. A session whose recording and transcript are both due gets one warning naming both. Nothing new goes to Discord.
  • A GM can restore it from the session's Review tab (POST /api/sessions/{id}/transcript/restore, audited).
  • Lengthening the window lets them back out. A GM who shortened it by mistake and put it back gets every transcript back, without restoring one by one.
  • A binned campaign's transcripts are left alone, as its audio is, so a restored campaign comes back whole.

While a transcript is in the bin

retention.readable_transcript() is the one check. A binned transcript is withheld from the session payload (a GM is told it is in the bin and until when), both transcript searches, the transcript tools, every model prompt, and the campaign export; an import of a session marked binned brings no transcript. An approved summary stays as written. tests/test_transcript_bin_readers.py is an allowlist in the style of the wiki's: a new raw read of a transcript under app/ fails until it is classified.

Member erasure reads the raw column on purpose: it scrubs a binned transcript exactly like a live one, and moves no clock (#798's lesson).

Migration

b680d1e2f3a4 on a798c0d1e2f3: two nullable timestamps, transcript_trashed_at and transcript_deletion_warned_at. No backfill. Downgrade drops both. Single head.

Screens

  • Review tab: "The transcript is in the bin / It is deleted for good on Wed, Oct 7. Until then nobody can read or search it." with Restore; then "The transcript is restored".
  • Settings → Recording, retention & AI, Admin → Recording & retention and Setup: the words that said a transcript has no bin now say what happens. The confirmation dialog keeps its serious tier (D12); whether it should drop to the ordinary one is question R2.
  • New catalogue row IS-465; IS-313 reworded.

Decisions taken in the build

  • Any GM can restore, where audio's restore is administrators-only: question R1.
  • A restore doesn't change the window, so under an unchanged window the next sweep bins it again with a fresh grace. The page says so.
  • The warning reuses the audio warning's notification kind rather than adding one.
  • The demo seed has no binned transcript: with auto-release it would only persist under a window that also bins the demo's other old transcripts.

Checked

  • Full backend suite: 6643 passed, 14 skipped. Full vitest: 3577 passed; eslint 0 errors. After merging main: the export, wiki, retention and bin tests, 695 passed.
  • Rendered in an e2e stack at 390×844 (touch) and 1440, light and dark: the bin state and Restore on Review, the restored state and the transcript folds returning after reload, the player's Read tab (nothing about a transcript or a bin), and the hints on Settings and Admin. No overflow, console errors or non-2xx calls. The Restore button is 67×44 at 390.
  • Not seen in a browser: the confirmation dialog, because the test data has no transcript old enough to trigger it. Its wording is covered by CampaignSettingsRecording.test.jsx.
  • ruff 0.4.4 clean; changelog, version sync and every coverage/lane*.py pass. No bot API change.

Left for later

The prototype pages still draw the old "no bin" wording (lane1c, lane2b, lane7, lane8), and webapp/CLAUDE.md still says the sweep has three passes; it has four.

🤖 Generated with Claude Code

Closes #680. The warning before saving a shorter window was already built; this is the safety net behind it. ## What changes Until now the nightly sweep destroyed an expired transcript outright. Now: - **It goes to a bin.** `sessions.transcript_trashed_at` is stamped; the words stay where they are. - **It waits there as long as audio does** (`audio_trash_retention_days`, one number for both). Only after that is it destroyed, exactly as before: the text, its segment rows and the extract cache. Summaries, highlights and wiki pages are untouched. - **The GMs are warned three days before**, by the task that already warns about audio. A session whose recording and transcript are both due gets one warning naming both. Nothing new goes to Discord. - **A GM can restore it** from the session's Review tab (`POST /api/sessions/{id}/transcript/restore`, audited). - **Lengthening the window lets them back out.** A GM who shortened it by mistake and put it back gets every transcript back, without restoring one by one. - A binned campaign's transcripts are left alone, as its audio is, so a restored campaign comes back whole. ## While a transcript is in the bin `retention.readable_transcript()` is the one check. A binned transcript is withheld from the session payload (a GM is told it is in the bin and until when), both transcript searches, the transcript tools, every model prompt, and the campaign export; an import of a session marked binned brings no transcript. An approved summary stays as written. `tests/test_transcript_bin_readers.py` is an allowlist in the style of the wiki's: a new raw read of a transcript under `app/` fails until it is classified. **Member erasure reads the raw column on purpose**: it scrubs a binned transcript exactly like a live one, and moves no clock (#798's lesson). ## Migration `b680d1e2f3a4` on `a798c0d1e2f3`: two nullable timestamps, `transcript_trashed_at` and `transcript_deletion_warned_at`. No backfill. Downgrade drops both. Single head. ## Screens - Review tab: "The transcript is in the bin / It is deleted for good on Wed, Oct 7. Until then nobody can read or search it." with Restore; then "The transcript is restored". - Settings → Recording, retention & AI, Admin → Recording & retention and Setup: the words that said a transcript has no bin now say what happens. The confirmation dialog keeps its serious tier (D12); whether it should drop to the ordinary one is question R2. - New catalogue row IS-465; IS-313 reworded. ## Decisions taken in the build - **Any GM can restore**, where audio's restore is administrators-only: question R1. - A restore doesn't change the window, so under an unchanged window the next sweep bins it again with a fresh grace. The page says so. - The warning reuses the audio warning's notification kind rather than adding one. - The demo seed has no binned transcript: with auto-release it would only persist under a window that also bins the demo's other old transcripts. ## Checked - Full backend suite: 6643 passed, 14 skipped. Full vitest: 3577 passed; eslint 0 errors. After merging main: the export, wiki, retention and bin tests, 695 passed. - Rendered in an e2e stack at 390×844 (touch) and 1440, light and dark: the bin state and Restore on Review, the restored state and the transcript folds returning after reload, the player's Read tab (nothing about a transcript or a bin), and the hints on Settings and Admin. No overflow, console errors or non-2xx calls. The Restore button is 67×44 at 390. - **Not seen in a browser:** the confirmation dialog, because the test data has no transcript old enough to trigger it. Its wording is covered by `CampaignSettingsRecording.test.jsx`. - ruff 0.4.4 clean; changelog, version sync and every `coverage/lane*.py` pass. No bot API change. ## Left for later The prototype pages still draw the old "no bin" wording (`lane1c`, `lane2b`, `lane7`, `lane8`), and `webapp/CLAUDE.md` still says the sweep has three passes; it has four. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The nightly sweep no longer destroys a transcript the night its window
runs out. It stamps sessions.transcript_trashed_at and keeps the words;
a later pass destroys them once the bin's grace has passed (audio's own
audio_trash_retention_days) and the window still reaches them, and lets
them back out if the window was lengthened. While binned a transcript is
served to nobody, found by no search, left out of an export and sent to
no model; a GM can restore it, the GMs are warned three days ahead, and a
member erase still reaches it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into fix/680-transcript-bin
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 33s
CI / Feedback site tests (pull_request) Successful in 55s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m6s
CI / Does this change touch the recording path? (pull_request) Successful in 29s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend lint (ruff) (pull_request) Successful in 1m14s
CI / Docker image build (pull_request) Successful in 3m0s
CI / Bot tests and audit (pull_request) Successful in 4m4s
CI / Frontend (shard 2/2) (pull_request) Successful in 13m0s
CI / End-to-end journeys (phone) (pull_request) Successful in 9m42s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 9m55s
CI / Frontend (shard 1/2) (pull_request) Successful in 17m33s
CI / End-to-end journeys (desktop) (pull_request) Successful in 17m57s
CI / Backend migration, tests, and audit (pull_request) Successful in 23m3s
09dde28fc9
Sign in to join this conversation.
No description provided.