[Ops] Close the eleven manual-intervention stuck states across session, lore, draft, and workbench pipelines #416

Closed
opened 2026-08-25 20:44:57 +00:00 by claude-bot · 4 comments
Contributor

Severity: HIGH

Found in the August 2026 session lifecycle review (#319).

Across the session-recording, lore-generation, wiki-draft, and Workbench pipelines, there are eleven distinct ways a piece of in-progress work can get permanently stuck in a state that only a database edit, filesystem edit, or console action can unstick — each one is a guaranteed support ticket in a hosted product whose stated bar is "zero operator intervention." Several already have dedicated defect issues covering their root cause; this issue tracks the ones that don't, and exists so the full list is visible and closed out together rather than piecemeal.

Evidence — the eleven enumerated stuck states, with cross-references to issues that already cover the root cause where one exists:

  • audio_processing_status='processing' forever (worker killed mid-task, or a pre-DB-context exception, or an enqueue failure after intake commits) — root cause tracked in "[Backend] Add Celery acks_late and a watchdog...".
  • lore_generation_status='pending' from a lost/failed enqueue — root cause tracked in "[Backend] Enqueue before committing lore_generation_status=pending...".
  • lore_generation_status='extracting'/'matching' after a worker SIGKILL — the API would accept a re-trigger, but webapp/frontend/src/pages/SessionDetail.jsx:108 (LORE_STATUS_IN_PROGRESS) hides the re-run button for every in-progress state, so it's stuck from the user's chair even though the backend isn't fully blocking it. Not yet covered elsewhere — needs a UI fix (show a re-run option after some elapsed time) independent of the backend watchdog.
  • Wiki draft stuck at generating after a worker crash — only self-service exit is discard_draft, which throws away all iteration work. Not yet covered elsewhere — needs its own failure path/watchdog coverage, or the discard-only escape needs a less destructive alternative.
  • Workbench GenerationResult stuck pending forever — the frontend gives up polling at ~60s (webapp/frontend/src/components/GeneratorPanel.jsx:40-41, MAX_POLLS = 40 × POLL_INTERVAL_MS); self-serviceable by re-running (at the cost of paying for tokens again), but reads as "the tool ate my generation." Not yet covered elsewhere.
  • Bot-side stranded session dir with no marker — root cause tracked in "[Bot] Stop the startup sweep from deleting recordings it told the user were saved".
  • Orphaned handed-off audio dirs — root cause tracked in "[Ops] Reconcile orphaned audio directories against the database".
  • Approved-then-edited summary cannot be re-posted to Discord — content_approved_at 409s re-approval (webapp/backend/app/routers/sessions.py:635-639), and there is no un-approve action, so a post-approval summary edit never reaches Discord. Not yet covered elsewhere.
  • Cancelled/completed sessions are one-way — no un-cancel/un-complete endpoint exists; a mis-click on "Mark completed" (webapp/frontend/src/pages/CampaignDetail.jsx:1075) fires the notes-harvest and Discord fan-out irreversibly. Data isn't lost, but the state transition can't be undone. Not yet covered elsewhere.
  • Co-GM campaign journal 500 — root cause tracked in "[Backend] Fix the campaign journal 500 for campaigns with two GMs".
  • Vote embeds with lost Redis mappings — root cause tracked in "[Bot] Surface a visible failure when a Discord reaction vote isn't recorded".

Failure scenario
Any one of the "not yet covered elsewhere" items above happens in isolation and turns into a support ticket with no self-service resolution — for example, a wiki draft's generation worker is killed mid-run, the draft is stuck at generating, and the GM's only available action destroys the iteration work they were part-way through rather than letting them retry.

Proposed fix
For each stuck state without an existing owner, add either (a) a watchdog-driven terminal-state transition (piggybacking on the cross-pipeline watchdog tracked in "[Backend] Build a generic stuck-task watchdog..."), or (b) where the backend already isn't the blocker (the lore in-progress re-run button case), a frontend fix to stop hiding the retry action indefinitely. For the one-way state transitions (approved summary, completed/cancelled sessions), add an explicit, audited un-approve/un-complete/un-cancel action rather than leaving the only escape as impossible or destructive.

Acceptance criteria

  • Every one of the eleven enumerated stuck states has either a dedicated fix-tracking issue or an explicit resolution captured in this issue.
  • The lore in-progress UI no longer hides the retry action indefinitely — it becomes available after a reasonable elapsed threshold.
  • A wiki draft stuck at generating has a non-destructive recovery path that doesn't require discarding iteration work.
  • An approved-then-edited summary can be re-posted to Discord, or an explicit un-approve action exists.
  • A mis-clicked "Mark completed"/"Cancel" has some recovery path short of manual database intervention.
  • None of the eleven require a manual SQL/filesystem/console intervention as their only resolution once this work lands.
**Severity: HIGH** Found in the August 2026 session lifecycle review (#319). Across the session-recording, lore-generation, wiki-draft, and Workbench pipelines, there are eleven distinct ways a piece of in-progress work can get permanently stuck in a state that only a database edit, filesystem edit, or console action can unstick — each one is a guaranteed support ticket in a hosted product whose stated bar is "zero operator intervention." Several already have dedicated defect issues covering their root cause; this issue tracks the ones that don't, and exists so the full list is visible and closed out together rather than piecemeal. **Evidence** — the eleven enumerated stuck states, with cross-references to issues that already cover the root cause where one exists: - `audio_processing_status='processing'` forever (worker killed mid-task, or a pre-DB-context exception, or an enqueue failure after intake commits) — root cause tracked in "[Backend] Add Celery acks_late and a watchdog...". - `lore_generation_status='pending'` from a lost/failed enqueue — root cause tracked in "[Backend] Enqueue before committing lore_generation_status=pending...". - `lore_generation_status='extracting'/'matching'` after a worker SIGKILL — the API would accept a re-trigger, but `webapp/frontend/src/pages/SessionDetail.jsx:108` (`LORE_STATUS_IN_PROGRESS`) hides the re-run button for every in-progress state, so it's stuck from the user's chair even though the backend isn't fully blocking it. **Not yet covered elsewhere** — needs a UI fix (show a re-run option after some elapsed time) independent of the backend watchdog. - Wiki draft stuck at `generating` after a worker crash — only self-service exit is `discard_draft`, which throws away all iteration work. **Not yet covered elsewhere** — needs its own failure path/watchdog coverage, or the discard-only escape needs a less destructive alternative. - Workbench `GenerationResult` stuck `pending` forever — the frontend gives up polling at ~60s (`webapp/frontend/src/components/GeneratorPanel.jsx:40-41`, `MAX_POLLS = 40` × `POLL_INTERVAL_MS`); self-serviceable by re-running (at the cost of paying for tokens again), but reads as "the tool ate my generation." **Not yet covered elsewhere.** - Bot-side stranded session dir with no marker — root cause tracked in "[Bot] Stop the startup sweep from deleting recordings it told the user were saved". - Orphaned handed-off audio dirs — root cause tracked in "[Ops] Reconcile orphaned audio directories against the database". - Approved-then-edited summary cannot be re-posted to Discord — `content_approved_at` 409s re-approval (`webapp/backend/app/routers/sessions.py:635-639`), and there is no un-approve action, so a post-approval summary edit never reaches Discord. **Not yet covered elsewhere.** - Cancelled/completed sessions are one-way — no un-cancel/un-complete endpoint exists; a mis-click on "Mark completed" (`webapp/frontend/src/pages/CampaignDetail.jsx:1075`) fires the notes-harvest and Discord fan-out irreversibly. Data isn't lost, but the state transition can't be undone. **Not yet covered elsewhere.** - Co-GM campaign journal 500 — root cause tracked in "[Backend] Fix the campaign journal 500 for campaigns with two GMs". - Vote embeds with lost Redis mappings — root cause tracked in "[Bot] Surface a visible failure when a Discord reaction vote isn't recorded". **Failure scenario** Any one of the "not yet covered elsewhere" items above happens in isolation and turns into a support ticket with no self-service resolution — for example, a wiki draft's generation worker is killed mid-run, the draft is stuck at `generating`, and the GM's only available action destroys the iteration work they were part-way through rather than letting them retry. **Proposed fix** For each stuck state without an existing owner, add either (a) a watchdog-driven terminal-state transition (piggybacking on the cross-pipeline watchdog tracked in "[Backend] Build a generic stuck-task watchdog..."), or (b) where the backend already isn't the blocker (the lore in-progress re-run button case), a frontend fix to stop hiding the retry action indefinitely. For the one-way state transitions (approved summary, completed/cancelled sessions), add an explicit, audited un-approve/un-complete/un-cancel action rather than leaving the only escape as impossible or destructive. **Acceptance criteria** - [ ] Every one of the eleven enumerated stuck states has either a dedicated fix-tracking issue or an explicit resolution captured in this issue. - [ ] The lore in-progress UI no longer hides the retry action indefinitely — it becomes available after a reasonable elapsed threshold. - [ ] A wiki draft stuck at `generating` has a non-destructive recovery path that doesn't require discarding iteration work. - [ ] An approved-then-edited summary can be re-posted to Discord, or an explicit un-approve action exists. - [ ] A mis-clicked "Mark completed"/"Cancel" has some recovery path short of manual database intervention. - [ ] None of the eleven require a manual SQL/filesystem/console intervention as their only resolution once this work lands.
Author
Contributor

Picking this up. PR #449 covers four of the five states that had no existing owner; the issue stays open for the fifth plus the resolution log below.

Status of all eleven

Six were cross-referenced to root-cause issues; three of those have now shipped:

Stuck state Owner Status
audio_processing_status='processing' forever #398 Shippedacks_late + hourly watchdog
lore_generation_status='pending' from a lost enqueue #413 Open
Bot-side stranded session dir #399 Shipped
Orphaned handed-off audio dirs (reconciliation issue) Open
Co-GM campaign journal 500 #411 Open
Vote embeds with lost Redis mappings (bot vote-failure issue) Open

The five with no owner:

Stuck state Resolution
Lore extracting/matching — UI hides retry Not yet. Needs a migration; see below
Wiki draft stuck generating PR #449 — non-destructive reset
Workbench GenerationResult stuck pending Resolved, no new work — see below
Approved-then-edited summary can't re-post PR #449 — withdraw approval
Completed/cancelled are one-way PR #449 — uncomplete / uncancel

Notes on two of them

Workbench pending — this reads worse in the issue than it is. GenerationHistory.jsx filters only by tool_id, never by status, so pending and failed rows both render with a status badge and a working "Re-open" button; the backend's history endpoint has no status filter either. #398's watchdog now gives a stranded row a terminal failed state within the hour. The generation is visible and recoverable throughout, so I'd rather record this as closed than invent work to satisfy a checkbox. Happy to reopen it if you disagree.

Lore in-progress retry — deliberately held back from #449. Doing it properly needs a lore_generation_started_at column (the only timestamp today is Session.updated_at, which any unrelated write bumps, and it isn't even exposed in SessionResponse). A migration doesn't belong smuggled into a PR of endpoint additions.

A defect found while surveying that one, not in this issue

generate_lore_proposals (routers/sessions.py:253) guards on session.lore_generation_status == LoreGenerationStatus.pending only. extracting and matching fall straight through, so calling the endpoint directly during either mid-pipeline state commits pending and queues a second concurrent extraction over the same transcript — duplicate proposals and a doubled token bill. It's masked today purely because LORE_STATUS_IN_PROGRESS hides the button in the UI for all three states, which is the same line the issue asks us to relax. Relaxing it without fixing the guard would turn a latent bug into a reachable one, so the two ship together.

PR #449 verification

Mutation-checked — 13 of 14 new backend tests fail against the unfixed code. The 14th passed vacuously (asserting "the audio is still trashed" holds equally when the endpoint 404s) and was tightened until it failed too. Frontend tests were run in a container, which caught a real bug in the new test helper that reading alone had missed. 1,423 backend + 446 frontend tests pass.

Picking this up. PR #449 covers four of the five states that had **no existing owner**; the issue stays open for the fifth plus the resolution log below. ## Status of all eleven Six were cross-referenced to root-cause issues; three of those have now shipped: | Stuck state | Owner | Status | |---|---|---| | `audio_processing_status='processing'` forever | #398 | **Shipped** — `acks_late` + hourly watchdog | | `lore_generation_status='pending'` from a lost enqueue | #413 | Open | | Bot-side stranded session dir | #399 | **Shipped** | | Orphaned handed-off audio dirs | (reconciliation issue) | Open | | Co-GM campaign journal 500 | #411 | Open | | Vote embeds with lost Redis mappings | (bot vote-failure issue) | Open | The five with no owner: | Stuck state | Resolution | |---|---| | Lore `extracting`/`matching` — UI hides retry | **Not yet.** Needs a migration; see below | | Wiki draft stuck `generating` | **PR #449** — non-destructive reset | | Workbench `GenerationResult` stuck `pending` | **Resolved, no new work** — see below | | Approved-then-edited summary can't re-post | **PR #449** — withdraw approval | | Completed/cancelled are one-way | **PR #449** — uncomplete / uncancel | ## Notes on two of them **Workbench pending** — this reads worse in the issue than it is. `GenerationHistory.jsx` filters only by `tool_id`, never by status, so `pending` and `failed` rows both render with a status badge and a working "Re-open" button; the backend's history endpoint has no status filter either. #398's watchdog now gives a stranded row a terminal `failed` state within the hour. The generation is visible and recoverable throughout, so I'd rather record this as closed than invent work to satisfy a checkbox. Happy to reopen it if you disagree. **Lore in-progress retry** — deliberately held back from #449. Doing it properly needs a `lore_generation_started_at` column (the only timestamp today is `Session.updated_at`, which any unrelated write bumps, and it isn't even exposed in `SessionResponse`). A migration doesn't belong smuggled into a PR of endpoint additions. ## A defect found while surveying that one, not in this issue `generate_lore_proposals` (`routers/sessions.py:253`) guards on `session.lore_generation_status == LoreGenerationStatus.pending` **only**. `extracting` and `matching` fall straight through, so calling the endpoint directly during either mid-pipeline state commits `pending` and queues a **second concurrent extraction** over the same transcript — duplicate proposals and a doubled token bill. It's masked today purely because `LORE_STATUS_IN_PROGRESS` hides the button in the UI for all three states, which is the same line the issue asks us to relax. Relaxing it without fixing the guard would turn a latent bug into a reachable one, so the two ship together. ## PR #449 verification Mutation-checked — 13 of 14 new backend tests fail against the unfixed code. The 14th passed vacuously (asserting "the audio is still trashed" holds equally when the endpoint 404s) and was tightened until it failed too. Frontend tests were run in a container, which caught a real bug in the new test helper that reading alone had missed. 1,423 backend + 446 frontend tests pass.
Author
Contributor

PR #449 merged (CI green on both commits). All five states that had no other owner are closed:

State Resolution
Completed/cancelled are one-way uncomplete / uncancel, audited
Approved-then-edited summary can't re-post unapprove, so approving reposts
Wiki draft stuck generating reset — keeps every edit and iteration
Lore extracting/matching — UI hides retry lore_generation_progress_at + a 30-min staleness threshold
Workbench GenerationResult stuck pending No new work needed — reasoning above, confirmed

The lore item also closed a defect this issue didn't list: generate_lore_proposals guarded on pending alone, so a call during extracting/matching queued a second extraction over the same transcript. It was unreachable only because the UI hid the button — the exact rule this issue asked us to relax — so both halves had to ship together.

Leaving this open for the six states tracked elsewhere. Three have shipped (#398, #399, and the audio half); still open: #413 (lore enqueue wedge), #411 (co-GM journal 500), the orphaned-audio-directory reconciliation, and the bot's silent vote-reaction failure. Once those four land, every acceptance criterion here is met and this can close.

Acceptance criteria status:

  • Every one of the eleven has a dedicated fix-tracking issue or an explicit resolution captured here
  • The lore in-progress UI no longer hides the retry action indefinitely
  • A wiki draft stuck at generating has a non-destructive recovery path
  • An approved-then-edited summary can be re-posted to Discord
  • A mis-clicked "Mark completed"/"Cancel" has a recovery path short of manual database intervention
  • None of the eleven require manual SQL/filesystem/console intervention — blocked on the four issues above
PR #449 merged (CI green on both commits). All five states that had no other owner are closed: | State | Resolution | |---|---| | Completed/cancelled are one-way | `uncomplete` / `uncancel`, audited | | Approved-then-edited summary can't re-post | `unapprove`, so approving reposts | | Wiki draft stuck `generating` | `reset` — keeps every edit and iteration | | Lore `extracting`/`matching` — UI hides retry | `lore_generation_progress_at` + a 30-min staleness threshold | | Workbench `GenerationResult` stuck `pending` | No new work needed — reasoning above, confirmed | The lore item also closed a defect this issue didn't list: `generate_lore_proposals` guarded on `pending` alone, so a call during `extracting`/`matching` queued a **second extraction over the same transcript**. It was unreachable only because the UI hid the button — the exact rule this issue asked us to relax — so both halves had to ship together. **Leaving this open** for the six states tracked elsewhere. Three have shipped (#398, #399, and the audio half); still open: #413 (lore enqueue wedge), #411 (co-GM journal 500), the orphaned-audio-directory reconciliation, and the bot's silent vote-reaction failure. Once those four land, every acceptance criterion here is met and this can close. **Acceptance criteria status:** - [x] Every one of the eleven has a dedicated fix-tracking issue or an explicit resolution captured here - [x] The lore in-progress UI no longer hides the retry action indefinitely - [x] A wiki draft stuck at `generating` has a non-destructive recovery path - [x] An approved-then-edited summary can be re-posted to Discord - [x] A mis-clicked "Mark completed"/"Cancel" has a recovery path short of manual database intervention - [ ] None of the eleven require manual SQL/filesystem/console intervention — blocked on the four issues above
Author
Contributor

All six acceptance criteria are met. Closing.

Verified against the code on main rather than from memory of what was merged:

Criterion Where it landed
Every stuck state has an owner or an explicit resolution This comment
Lore in-progress UI no longer hides retry indefinitely SessionDetail.jsxLORE_STALE_AFTER_MS, mirroring LORE_GENERATION_STALE_AFTER in routers/sessions.py, keyed on the new sessions.lore_generation_progress_at
Wiki draft at generating has a non-destructive recovery path unstick_lost_tasks transitions LoreDraftStatus.generating past the cutoff, leaving the iteration work intact — the discard-only escape is no longer the only exit
Approved-then-edited summary can be re-posted POST /sessions/{id}/unapprove (routers/sessions.py), audited as session.content_unapproved
Mis-clicked "Mark completed"/"Cancel" is recoverable POST /sessions/{id}/uncomplete and /uncancel, audited
None of the eleven need manual SQL/filesystem/console intervention Below

The eleven, and where each was actually resolved:

  1. audio_processing_status='processing' forever — #398 (acks_late + unstick_lost_tasks)
  2. lore_generation_status='pending' from a lost enqueue — #413 (enqueue failures now mark the run failed with a re-runnable message)
  3. lore_generation_status='extracting'/'matching' after SIGKILL — #416's own UI fix, above
  4. Wiki draft stuck at generating#398's watchdog
  5. Workbench GenerationResult stuck pending#398's watchdog
  6. Bot-side stranded session dir with no marker — #399
  7. Orphaned handed-off audio dirs — #406
  8. Approved-then-edited summary — the un-approve endpoint, above
  9. Cancelled/completed one-way — un-complete / un-cancel, above
  10. Co-GM campaign journal 500 — #411
  11. Vote embeds with lost Redis mappings — #409

One correction to this issue's own framing. The body says the Workbench GenerationResult case is "self-serviceable by re-running (at the cost of paying for tokens again)". By the time the watchdog work landed that was no longer the interesting part: the row now reaches a terminal state on its own, so the GM is told the generation failed rather than being left to infer it from a spinner that stopped. The frontend's MAX_POLLS cap is unchanged and still correct — it bounds the polling, it no longer bounds the information.

Worth recording for whoever reads this next: on five of these, the issue's stated evidence had decayed by the time the work started — the root cause had already been fixed by a sibling issue, or the file/line cited had moved. Reading the linked issues before trusting an enumeration like this one is not optional.

All six acceptance criteria are met. Closing. Verified against the code on `main` rather than from memory of what was merged: | Criterion | Where it landed | |---|---| | Every stuck state has an owner or an explicit resolution | This comment | | Lore in-progress UI no longer hides retry indefinitely | `SessionDetail.jsx` — `LORE_STALE_AFTER_MS`, mirroring `LORE_GENERATION_STALE_AFTER` in `routers/sessions.py`, keyed on the new `sessions.lore_generation_progress_at` | | Wiki draft at `generating` has a non-destructive recovery path | `unstick_lost_tasks` transitions `LoreDraftStatus.generating` past the cutoff, leaving the iteration work intact — the discard-only escape is no longer the only exit | | Approved-then-edited summary can be re-posted | `POST /sessions/{id}/unapprove` (`routers/sessions.py`), audited as `session.content_unapproved` | | Mis-clicked "Mark completed"/"Cancel" is recoverable | `POST /sessions/{id}/uncomplete` and `/uncancel`, audited | | None of the eleven need manual SQL/filesystem/console intervention | Below | **The eleven, and where each was actually resolved:** 1. `audio_processing_status='processing'` forever — #398 (`acks_late` + `unstick_lost_tasks`) 2. `lore_generation_status='pending'` from a lost enqueue — #413 (enqueue failures now mark the run `failed` with a re-runnable message) 3. `lore_generation_status='extracting'/'matching'` after SIGKILL — #416's own UI fix, above 4. Wiki draft stuck at `generating` — #398's watchdog 5. Workbench `GenerationResult` stuck `pending` — #398's watchdog 6. Bot-side stranded session dir with no marker — #399 7. Orphaned handed-off audio dirs — #406 8. Approved-then-edited summary — the un-approve endpoint, above 9. Cancelled/completed one-way — un-complete / un-cancel, above 10. Co-GM campaign journal 500 — #411 11. Vote embeds with lost Redis mappings — #409 **One correction to this issue's own framing.** The body says the Workbench `GenerationResult` case is "self-serviceable by re-running (at the cost of paying for tokens again)". By the time the watchdog work landed that was no longer the interesting part: the row now reaches a terminal state on its own, so the GM is told the generation failed rather than being left to infer it from a spinner that stopped. The frontend's `MAX_POLLS` cap is unchanged and still correct — it bounds the polling, it no longer bounds the *information*. Worth recording for whoever reads this next: on five of these, the issue's stated evidence had decayed by the time the work started — the root cause had already been fixed by a sibling issue, or the file/line cited had moved. Reading the linked issues before trusting an enumeration like this one is not optional.
Author
Contributor

Noted while doing #395 (v4.3.0): on WikiDraftReview, the generating banner, and therefore both the new "Check again" control and this issue's reset, live inside the non-convert_stats layout branch, so a convert_stats draft stuck in generating still cannot reach either control. The top status banner now reports the stall in every mode, but the actions do not. Moving that block above the layout branch is about five lines and belongs with this issue's surface.

Noted while doing #395 (v4.3.0): on `WikiDraftReview`, the generating banner, and therefore both the new "Check again" control and this issue's reset, live inside the non-`convert_stats` layout branch, so a `convert_stats` draft stuck in `generating` still cannot reach either control. The top status banner now reports the stall in every mode, but the actions do not. Moving that block above the layout branch is about five lines and belongs with this issue's surface.
Sign in to join this conversation.
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#416
No description provided.