Session title suggestions (#23) never appear: trigger is on a dead code path, and there is no surface for recurring sessions #278

Closed
opened 2026-08-05 15:30:47 +00:00 by claude-bot · 2 comments
Contributor

Reported from live use: no title suggestions offered for an upcoming recurring session. Investigation found two independent defects, either of which alone would produce the symptom.

Verified against prod (v3.11.0, containers built 2026-07-27, i.e. after #23 shipped 2026-07-17). LLM is configured (app_settings.llm_config present and encrypted).


Defect 1 — the generation trigger sits on a code path that is dead under the default retention mode

generate_session_title_suggestions.delay() is called from exactly one place:
webapp/backend/app/routers/sessions.py:695, at the end of approve_audio.

approve_audio requires audio_processing_status == ready (sessions.py:607) and
transitions the session to trashed.

But apply_post_processing_audio_retention (webapp/backend/app/tasks/reminder_tasks.py:2360)
runs on the success path of process_audio, and when the effective audio retention mode is
delete_after_processing it sets the session straight to the terminal approved state
(reminder_tasks.py:2402), skipping ready and the whole GM trash/grace flow. Its own docstring
says so explicitly.

delete_after_processing is the default (settings_service.py:82,
_DEFAULT_AUDIO_RETENTION_MODE), and neither prod campaign sets an override
(campaigns.audio_retention_mode is NULL for both).

Consequence: approve_audio is never reachable, so the task never fires. Confirmed on prod —
all 6 transcribed sessions are audio_processing_status = 'approved' with audio_trashed_at
NULL (never trashed), and both campaigns have
next_session_title_suggestions_generated_at = NULL
— the task has never run to completion for
either, despite two qualifying sessions completing since #23 shipped.

Fix direction: fire generate_session_title_suggestions.delay(campaign_id) from the point the
transcript+summary are actually saved (end of process_audio), not from the GM approval step —
approval is an audio-retention concern, not a summary-availability one. Keep the approve_audio
call as well for the non-default retention modes, or move it to a single shared helper so both
paths converge.

Defect 2 — no suggestion surface exists for a recurring session

TitleSuggestions is rendered in exactly one place, the manual new-session form
(webapp/frontend/src/pages/CampaignDetail.jsx:2307), and it is explicitly suppressed when the
"make recurring" toggle is on ({!makeRecurring && <TitleSuggestions … />}, line 2306).

Recurring occurrences are not created through that form at all — the materialize_session_series
Beat task inserts them with title=series.title_template (reminder_tasks.py:4489). And the GM
inline edit form on SessionDetail.jsx (title/description, line 424) has no suggestion chips.

Consequence: for a session that came from a series there is nowhere in the UI that offers a title,
even if suggestions were stored. The prod series (biweekly, Tuesdays 20:00, from 2026-08-11) has an
empty title_template, so its materialized occurrences (2026-08-12 and 2026-08-26 UTC) have empty
titles with no way to fill them from suggestions.

Fix direction: render TitleSuggestions in the SessionDetail GM edit form — that is where a
GM titles a session that already exists, which is the only path for a series occurrence. Worth
also reconsidering the !makeRecurring suppression: a title template arguably wants suggestions
too, though that is a weaker case since the template is reused across occurrences.


Acceptance

  • A completed session with a summary causes suggestions to be regenerated under the default
    retention mode, with no GM approval step.
  • A GM can pick a suggested title for a series-materialized session from SessionDetail.
  • Backend test covering the delete_after_processing path firing the task.
  • Frontend test covering the chips in the SessionDetail edit form.

Workaround until fixed

Open + New Session (leave "recurring" unchecked) → ↻ Regenerate → wait → Refresh to get
chips, then copy a title into the recurring session's edit form. Nothing needs to be created.

Labels to apply: bug, backend, frontend

Reported from live use: no title suggestions offered for an upcoming recurring session. Investigation found **two independent defects**, either of which alone would produce the symptom. Verified against prod (v3.11.0, containers built 2026-07-27, i.e. after #23 shipped 2026-07-17). LLM is configured (`app_settings.llm_config` present and encrypted). --- ## Defect 1 — the generation trigger sits on a code path that is dead under the default retention mode `generate_session_title_suggestions.delay()` is called from exactly one place: `webapp/backend/app/routers/sessions.py:695`, at the end of `approve_audio`. `approve_audio` requires `audio_processing_status == ready` (`sessions.py:607`) and transitions the session to `trashed`. But `apply_post_processing_audio_retention` (`webapp/backend/app/tasks/reminder_tasks.py:2360`) runs on the success path of `process_audio`, and when the effective audio retention mode is `delete_after_processing` it sets the session **straight to the terminal `approved` state** (`reminder_tasks.py:2402`), skipping `ready` and the whole GM trash/grace flow. Its own docstring says so explicitly. `delete_after_processing` is the **default** (`settings_service.py:82`, `_DEFAULT_AUDIO_RETENTION_MODE`), and neither prod campaign sets an override (`campaigns.audio_retention_mode` is NULL for both). Consequence: `approve_audio` is never reachable, so the task never fires. Confirmed on prod — all 6 transcribed sessions are `audio_processing_status = 'approved'` with `audio_trashed_at` NULL (never trashed), and **both campaigns have `next_session_title_suggestions_generated_at = NULL`** — the task has never run to completion for either, despite two qualifying sessions completing since #23 shipped. **Fix direction:** fire `generate_session_title_suggestions.delay(campaign_id)` from the point the transcript+summary are actually saved (end of `process_audio`), not from the GM approval step — approval is an audio-retention concern, not a summary-availability one. Keep the `approve_audio` call as well for the non-default retention modes, or move it to a single shared helper so both paths converge. ## Defect 2 — no suggestion surface exists for a recurring session `TitleSuggestions` is rendered in exactly one place, the manual new-session form (`webapp/frontend/src/pages/CampaignDetail.jsx:2307`), and it is explicitly suppressed when the "make recurring" toggle is on (`{!makeRecurring && <TitleSuggestions … />}`, line 2306). Recurring occurrences are not created through that form at all — the `materialize_session_series` Beat task inserts them with `title=series.title_template` (`reminder_tasks.py:4489`). And the GM inline edit form on `SessionDetail.jsx` (title/description, line 424) has no suggestion chips. Consequence: for a session that came from a series there is nowhere in the UI that offers a title, even if suggestions were stored. The prod series (biweekly, Tuesdays 20:00, from 2026-08-11) has an empty `title_template`, so its materialized occurrences (2026-08-12 and 2026-08-26 UTC) have empty titles with no way to fill them from suggestions. **Fix direction:** render `TitleSuggestions` in the `SessionDetail` GM edit form — that is where a GM titles a session that already exists, which is the *only* path for a series occurrence. Worth also reconsidering the `!makeRecurring` suppression: a title template arguably wants suggestions too, though that is a weaker case since the template is reused across occurrences. --- ## Acceptance - [ ] A completed session with a summary causes suggestions to be regenerated under the **default** retention mode, with no GM approval step. - [ ] A GM can pick a suggested title for a series-materialized session from `SessionDetail`. - [ ] Backend test covering the `delete_after_processing` path firing the task. - [ ] Frontend test covering the chips in the `SessionDetail` edit form. ## Workaround until fixed Open **+ New Session** (leave "recurring" unchecked) → **↻ Regenerate** → wait → **Refresh** to get chips, then copy a title into the recurring session's edit form. Nothing needs to be created. Labels to apply: bug, backend, frontend
Author
Contributor

Audit follow-up — scope is wider than this issue as filed

A full sweep of LLM feature triggers (12 LLM features, 21 Celery enqueue sites, 8 in-request LLM call sites) changes the picture. Two additions and one correction.

Correction: the retention default is confirmed in force on prod, not hypothetical

app_settings on prod contains only bot_api_key, bot_url, discord_bot_token, llm_config, lore_pipeline_mode, whisper_configno audio_retention_policy row — and both campaigns have audio_retention_mode NULL. So _DEFAULT_AUDIO_RETENTION_MODE = delete_after_processing is what's running, and has been since recording started.

Addition 1: approve_audio gates four things, not two

Beyond the two in the issue body, the same dead endpoint also carries:

  • Highlight publication (sessions.py:625-629) — extract_highlights runs inline in process_audio and writes rows with approved=False; approve_audio is the only thing that flips them to approved=True, and list_session_highlights filters non-GMs to approved=True (sessions.py:729-730). The player-facing quote board (#116) is therefore permanently empty on a default install.
  • The session_summary_approved bot event (sessions.py:665-679) — the bot posts a draft embed on session_summarised with 👍⚠️ feedback reactions and only replaces it on approval. Prod: 5 of 6 transcribed sessions have summary_discord_message_id set and none was ever replaced. Players have been reading uncorrected AI drafts with the feedback prompt still attached.
  • Journal entries (sessions.py:694) — the only other trigger is backfill_journal_entries, wired to worker_ready in a bare try/except: pass (reminder_tasks.py:4508-4515). Journal entries appear when a Celery worker next restarts, not after a session. (Moot on this instance — journal_entry_enabled is false on both campaigns.)

Also worth noting the frontend gates transcript editing on status === "ready" (SessionDetail.jsx:357), so a GM on the default mode can't correct a transcript either. Summary editing (SessionDetail.jsx:303) is ungated — which is why a GM can produce a corrected summary and then have no way to publish it.

Addition 2: a third, independent defect in #23

Even with the trigger fixed, the task would mostly generate from nothing:

  • _generate_session_title_suggestions_async selects context from Session.status == SessionStatus.completed (reminder_tasks.py:4287). But process_audio and approve_audio both run while the session is still confirmedupdate_campaign_storyline compensates for exactly this with .in_([completed, confirmed]) and the comment "Include confirmed — process_audio fires before auto_complete_sessions" (reminder_tasks.py:3970-3979). The title task never got that fix.
  • On any LLM error the handler sets suggestions = [] (:4326) then unconditionally writes next_session_title_suggestions = [] with a fresh generated_at (:4328-4331). A transient Ollama timeout blanks a previously-good set and stamps it as freshly generated.

Revised fix direction

Moving one .delay() is not the fix. Three separable pieces:

  1. Split the state. sessions.audio_processing_status currently encodes both "where is the WAV" and "has the GM signed off on the content" — note the enum comments (models/session.py:43-44): trashed means GM approved, approved means audio permanently deleted. Content approval needs its own field, reachable in every retention mode.
  2. One session_summary_available(session_id) fan-out, called from process_audio (after commit), from session_service.update_session when "summary" is in the update, and from the canonical-name summary refresh (sessions.py:341). It enqueues lore proposals, journal, title suggestions, and the storyline rebuild. Collapses 3 mutually-unaware fan-out sites into 1 and fixes the mis-wiring as a side effect. Today only the storyline hears about manual summary edits.
  3. Fix the two #23 data bugs above (status window, don't clobber on failure).

Admin UI copy needs updating too: the mode is labelled "Delete after processing (recommended)" (Admin.jsx:769) and warns only "you won't be able to re-process the recording" (Admin.jsx:884). It should say the approval step, transcript editing, the Discord re-post, and highlight publication all go with it.

Still open

Both prod campaigns have highlights_max_quotes = 5 and highlights_in_discord = true, but zero session_highlights rows exist on any session — so extraction is producing nothing, separately from the publication gate. extract_highlights never raises (returns [] on error) and the delete-then-reinsert at reminder_tasks.py:2116-2120 silently wipes prior rows. Needs its own investigation.

Other mis-wirings the audit turned up are not part of this issue and should be filed separately: generate_lore_entry_summary has one enqueue site (campaigns.py:3287) despite a docstring claiming entry-creation also triggers it (verified — entries from proposal approval, shelf promotion, and canonical-name auto-create never get a summary); and propose_lore_relationships fires on proposal generation but reads proposed_by_bot == False, so it never sees the entries the session just produced.

## Audit follow-up — scope is wider than this issue as filed A full sweep of LLM feature triggers (12 LLM features, 21 Celery enqueue sites, 8 in-request LLM call sites) changes the picture. Two additions and one correction. ### Correction: the retention default is confirmed in force on prod, not hypothetical `app_settings` on prod contains only `bot_api_key`, `bot_url`, `discord_bot_token`, `llm_config`, `lore_pipeline_mode`, `whisper_config` — **no `audio_retention_policy` row** — and both campaigns have `audio_retention_mode` NULL. So `_DEFAULT_AUDIO_RETENTION_MODE = delete_after_processing` is what's running, and has been since recording started. ### Addition 1: `approve_audio` gates four things, not two Beyond the two in the issue body, the same dead endpoint also carries: - **Highlight publication** (`sessions.py:625-629`) — `extract_highlights` runs inline in `process_audio` and writes rows with `approved=False`; `approve_audio` is the *only* thing that flips them to `approved=True`, and `list_session_highlights` filters non-GMs to `approved=True` (`sessions.py:729-730`). The player-facing quote board (#116) is therefore permanently empty on a default install. - **The `session_summary_approved` bot event** (`sessions.py:665-679`) — the bot posts a draft embed on `session_summarised` with 👍⚠️❌ feedback reactions and only replaces it on approval. Prod: 5 of 6 transcribed sessions have `summary_discord_message_id` set and **none** was ever replaced. Players have been reading uncorrected AI drafts with the feedback prompt still attached. - **Journal entries** (`sessions.py:694`) — the only other trigger is `backfill_journal_entries`, wired to `worker_ready` in a bare `try/except: pass` (`reminder_tasks.py:4508-4515`). Journal entries appear when a Celery worker next restarts, not after a session. (Moot on this instance — `journal_entry_enabled` is false on both campaigns.) Also worth noting the frontend gates **transcript editing** on `status === "ready"` (`SessionDetail.jsx:357`), so a GM on the default mode can't correct a transcript either. Summary editing (`SessionDetail.jsx:303`) is ungated — which is why a GM can produce a corrected summary and then have no way to publish it. ### Addition 2: a third, independent defect in #23 Even with the trigger fixed, the task would mostly generate from nothing: - `_generate_session_title_suggestions_async` selects context from `Session.status == SessionStatus.completed` (`reminder_tasks.py:4287`). But `process_audio` and `approve_audio` both run while the session is still `confirmed` — `update_campaign_storyline` compensates for exactly this with `.in_([completed, confirmed])` and the comment *"Include confirmed — process_audio fires before auto_complete_sessions"* (`reminder_tasks.py:3970-3979`). The title task never got that fix. - On any LLM error the handler sets `suggestions = []` (`:4326`) then **unconditionally** writes `next_session_title_suggestions = []` with a fresh `generated_at` (`:4328-4331`). A transient Ollama timeout blanks a previously-good set and stamps it as freshly generated. ### Revised fix direction Moving one `.delay()` is not the fix. Three separable pieces: 1. **Split the state.** `sessions.audio_processing_status` currently encodes both "where is the WAV" and "has the GM signed off on the content" — note the enum comments (`models/session.py:43-44`): `trashed` means *GM approved*, `approved` means *audio permanently deleted*. Content approval needs its own field, reachable in every retention mode. 2. **One `session_summary_available(session_id)` fan-out**, called from `process_audio` (after commit), from `session_service.update_session` when `"summary"` is in the update, and from the canonical-name summary refresh (`sessions.py:341`). It enqueues lore proposals, journal, title suggestions, and the storyline rebuild. Collapses 3 mutually-unaware fan-out sites into 1 and fixes the mis-wiring as a side effect. Today only the storyline hears about manual summary edits. 3. **Fix the two #23 data bugs** above (status window, don't clobber on failure). Admin UI copy needs updating too: the mode is labelled "Delete after processing (recommended)" (`Admin.jsx:769`) and warns only *"you won't be able to re-process the recording"* (`Admin.jsx:884`). It should say the approval step, transcript editing, the Discord re-post, and highlight publication all go with it. ### Still open Both prod campaigns have `highlights_max_quotes = 5` and `highlights_in_discord = true`, but **zero** `session_highlights` rows exist on any session — so extraction is producing nothing, separately from the publication gate. `extract_highlights` never raises (returns `[]` on error) and the delete-then-reinsert at `reminder_tasks.py:2116-2120` silently wipes prior rows. Needs its own investigation. Other mis-wirings the audit turned up are not part of this issue and should be filed separately: `generate_lore_entry_summary` has one enqueue site (`campaigns.py:3287`) despite a docstring claiming entry-creation also triggers it (verified — entries from proposal approval, shelf promotion, and canonical-name auto-create never get a summary); and `propose_lore_relationships` fires on proposal *generation* but reads `proposed_by_bot == False`, so it never sees the entries the session just produced.
rbrooks referenced this issue from a commit 2026-08-05 17:49:35 +00:00
Author
Contributor

Fixed and deployed in v3.11.1 (PR #280, merged as 01c9f86).

What shipped

Content approval is now its own state (sessions.content_approved_at / content_approved_by_id) with its own endpoint, POST /sessions/{id}/approve, gated on having a summary rather than on audio state. audio_processing_status goes back to meaning only where the audio file is, and its enum comments no longer claim approval semantics. POST /sessions/{id}/audio/approve remains as a deprecated shim.

The audio-trash transition survives as a conditional side effect, fired only when the audio is still ready, so retain-mode installs keep their previous behaviour exactly.

Also landed: the shared summary_events.on_session_summary_available() fan-out replacing three mutually-unaware enqueue sites, and the two additional title-suggestion defects (the completed-only status window, and clobbering good suggestions on a transient LLM failure).

Verified on the live deployment

Dev had no session with a summary, so the regression was seeded and driven through the real HTTP API:

Scenario Result
Audio already deleted (approved) + summary — the regression 200; content_approved_at set, correct GM, audio state untouched
Double approve 409 "already approved"
Transcript but no summary 409 "no summary to approve"
Deprecated shim on a ready session 200 — and correctly moved audio to trashed

That last row is the one that matters for self-hosters: the conditional audio-trash still fires for retain-mode installs.

Prod deployed 2026-08-05 19:02 UTC. Migration d9e0f1a2b3c4 → e0f1a2b3c4d5 applied, all seven containers healthy, bot handshake clean (backend 3.11.1, bot API contract v1). The backfill correctly wrote nothing — no session on this instance had audio_trashed_at, because none had ever genuinely been approved. All eight transcribed sessions now show as approvable.

Known gap

The Discord summary re-post could not be tested: the dev instance has no DISCORD_BOT_TOKEN and its campaign has no guild/channel. The session_summary_approved publish block was moved verbatim from approve_audio, with an unchanged payload and an unchanged bot handler (which already guards a null summary_discord_message_id), so what changed is reachability — and reachability is proven. Still, the first real approval on prod is the first live exercise of that path.

Closing. The empty quote board is tracked separately in #279.

Fixed and deployed in **v3.11.1** (PR #280, merged as `01c9f86`). ## What shipped Content approval is now its own state (`sessions.content_approved_at` / `content_approved_by_id`) with its own endpoint, `POST /sessions/{id}/approve`, gated on having a summary rather than on audio state. `audio_processing_status` goes back to meaning only where the audio file is, and its enum comments no longer claim approval semantics. `POST /sessions/{id}/audio/approve` remains as a deprecated shim. The audio-trash transition survives as a **conditional** side effect, fired only when the audio is still `ready`, so retain-mode installs keep their previous behaviour exactly. Also landed: the shared `summary_events.on_session_summary_available()` fan-out replacing three mutually-unaware enqueue sites, and the two additional title-suggestion defects (the `completed`-only status window, and clobbering good suggestions on a transient LLM failure). ## Verified on the live deployment Dev had no session with a summary, so the regression was seeded and driven through the real HTTP API: | Scenario | Result | |---|---| | Audio already deleted (`approved`) + summary — **the regression** | 200; `content_approved_at` set, correct GM, audio state untouched | | Double approve | 409 "already approved" | | Transcript but no summary | 409 "no summary to approve" | | Deprecated shim on a `ready` session | 200 — **and** correctly moved audio to `trashed` | That last row is the one that matters for self-hosters: the conditional audio-trash still fires for retain-mode installs. Prod deployed 2026-08-05 19:02 UTC. Migration `d9e0f1a2b3c4 → e0f1a2b3c4d5` applied, all seven containers healthy, bot handshake clean (`backend 3.11.1, bot API contract v1`). The backfill correctly wrote nothing — no session on this instance had `audio_trashed_at`, because none had ever genuinely been approved. All eight transcribed sessions now show as approvable. ## Known gap The Discord summary re-post could not be tested: the dev instance has no `DISCORD_BOT_TOKEN` and its campaign has no guild/channel. The `session_summary_approved` publish block was moved **verbatim** from `approve_audio`, with an unchanged payload and an unchanged bot handler (which already guards a null `summary_discord_message_id`), so what changed is reachability — and reachability is proven. Still, the first real approval on prod is the first live exercise of that path. Closing. The empty quote board is tracked separately in #279.
Sign in to join this conversation.
No milestone
No project
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#278
No description provided.