[Backend] Surface the four invisible post-session tasks, including the party-wide recap email #380
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?
Impact: HIGH
Found in the August 2026 session lifecycle review (#319).
What the user experiences
One summary event fans out six post-session tasks. Two of them — audio processing and lore-proposal generation — have real status fields and are shown to the GM. The other four have no status field and no UI trace whatsoever: the campaign-storyline rebuild, the journal-entry generation, the session-title-suggestion generation, and — most notably — an email sent to the entire party. If any of these four silently fails, or simply hasn't run yet, nothing in the product tells anyone.
Evidence
webapp/backend/app/services/summary_events.py:70-80fans out all six tasks from one summary event.webapp/backend/app/schemas/session.py:161-170exposes onlyaudio_processing_status/_errorandlore_generation_status/_error— the other four tasks have nothing.webapp/frontend/src/pages/SessionDetail.jsx:100-106,92-98show the two tasks that do have status fields with clear human labels; nothing on the page acknowledges the other four exist.webapp/frontend/src/components/TitleSuggestions.jsx:49— the title-suggestion fetch failure is separately swallowed (see the silent-failures issue in this milestone), compounding the fact that there is no status field to check in the first place.Why it matters for a hosted product
A party-wide email fires today with zero UI trace anywhere in the product — nobody can tell it happened, is happening, or failed to happen, from either the web app or Discord.
Proposed fix
Add status columns for the storyline rebuild, journal entry, title suggestions, and recap email to the session schema (
webapp/backend/app/schemas/session.py:161-170), and render them as a single "Post-session processing" checklist onSessionDetail. This is the audit's P9.Acceptance criteria
SessionDetailrenders a "Post-session processing" checklist showing the state of all six post-session tasks, not just the two that exist today.Picking this up as v4.3.0 phase 4 (#514). Decision: one nullable JSONB column
post_session_taskson sessions (a single migration) holding{task: {status, error, at}}for the storyline rebuild, journal entry, title suggestions and recap email, written at fan-out and at each task's start, finish and failure; exposed on the session response; a "Post-session processing" checklist on the session page covering all six tasks, with failed and not-yet-run visibly different and the recap email reading queued / sent / failed.Done in PR #526 (auto-merging on green); ships with v4.3.0.
sessions.post_session_tasks(JSONB, nullable, migration0b1c2d3e4f5a) carries{task: {status, error, at}}for the four tasks that reported nowhere: the storyline rebuild, the journal entry, the next-session title suggestions and the recap email. The other two of the six keep their own columns and are not duplicated, so each fact has one source.Every write goes through
app/services/post_session_tasks.pyas a singleCOALESCE(post_session_tasks,'{}') || :patchstatement. That is load-bearing rather than stylistic: the four tasks run concurrently in separate workers, and a read-modify-write from four processes loses updates, the one it loses being somebody's failure.mark()never raises, since bookkeeping must not be able to fail the work it describes.Queued marks are written at fan-out from both call sites, driven off a new
summary_events.fanout_task_names()so the fan-out list and the status map cannot drift (there is a test for exactly that). A refused enqueue is recordedfailed, notqueued, the #413 failure mode, and worse here because there is no retry button. On a summary edit the recap-email key is left alone rather than reset to "queued" for a send that will never happen.Absent is not failed, throughout: a NULL column (fan-out predating the release) and a missing key both read as "no record", and
skipped(journal entries off, campaign archived, storyline hand-edited) is a distinct state fromfailed, with its reason attached. In the twollm_task-decorated bodies the skip reason is written on the way past and the decorator's sentinel re-raised, so the SKIP policy still runs.generate_session_title_suggestionsgained an optionalsession_id: it generates for a campaign, but a GM looks for "did this run?" on the session whose summary set it off. The manual regenerate path omits it and records nothing rather than stamping an unrelated session.UI:
PostSessionChecklist.jsxon the session page, all six rows, renders nothing until something has been recorded. The recap row says Queued / Sent / Failed. Failure reasons are GM-only (they can carry an SMTP message or an exception name); the status word is shown to everyone. Tests: 19 intest_post_session_tasks.py, 3 intest_recap_email.pyagainst the real SMTP harness, 11 component tests.