fix(webapp): stop double-writing wiki suggestions, and hold them for GM approval (#609, #610) #611

Merged
claude-bot merged 1 commit from fix/609-610-wiki-suggestions into main 2026-09-11 23:16:38 +00:00
Collaborator

Two changes to how a session's wiki suggestions are produced.

#609 — every suggestion was written twice

lore_consolidate_proposals waits for its match tasks by calling self.retry(). That publishes a retry message and raises celery.exceptions.Retry, which is an Exception subclass — so the task's outer handler caught it and _retry_unless_permanent called task.retry() again, with the same 30 · 2ⁿ countdown. Two identical deliveries then ran together, both saw every match row present, and both wrote. create_bot_lore_proposals deletes before inserting, but under READ COMMITTED neither delete sees the other's uncommitted rows.

Measured before the fix: dev's replay session and one prod session each held 12 suggestions stored twice.

  • _retry_unless_permanent now re-raises Retry untouched. Fixed in the shared helper rather than in the one task: all four lore tasks route through it, and any of them gaining a self.retry() later would reproduce the bug.
  • The write is now idempotent on its own, because Celery delivers at least once whatever the retry code does. The task takes a row lock on the session before reading anything, then treats "already ready with no cache rows left" as "a twin finished this" and returns.

#610 — suggestions waited for no one

They were generated once, automatically, from the first transcription, and never again. They read the stored summary plus the raw transcript, never the verified events — so nothing a GM corrected could reach them. On the session used for pipeline testing, that left "Ironroot Hold" proposed as a real settlement after the GM's slip had been corrected in the events, alongside table chatter as lore.

The owner's decision, recorded on #610: suggestions should not exist until the GM has signed off on the content.

  • Generation moves to approval. It leaves the summary fan-out entirely; approve_session_content queues it, only when lore_generation_status is None, so approve → withdraw → re-approve doesn't pay for a second extraction. A broker failure there is recorded and logged, never raised — approval is what the GM actually asked for.
  • Corrections clear them. on_session_summary_available(..., first_time=False) is the one funnel every summary change passes through — regenerate, restore and hand-edit — so a new clear_wiki_suggestions task fires there. It deletes the still-pending bot suggestions and resets the lore state to NULL, which returns the button to "Suggest wiki updates".
  • Approved entries are never touched. Approving a suggestion flips proposed_by_bot to False, so it cannot match the delete.
  • The manual button is unchanged, for a GM who wants suggestions before approving.

Found while building, beyond the brief

process_audio set lore_generation_status = pending eagerly, which worked only because the fan-out queued the generation in the same breath. With generation gone from the fan-out, that would have left every newly transcribed session claiming queued work that nothing would ever pick up — the #413 failure shape, self-inflicted. Removed, along with the enqueue-failure handling that existed to catch it.

Known gap, narrow and deliberate

A GM who regenerates while an extraction is still running gets the status reset, but the in-flight run still finishes and writes suggestions from the pre-correction text. That is no worse than today's behaviour, and closing it properly wants a generation epoch rather than a bigger delete. Noted on #610.

Verification

  • Full backend suite: 3404 passed, 13 skipped.
  • ruff format --check and ruff check, pinned to CI's 0.4.4: clean.
  • Frontend unchanged — the button already keys off lore_generation_status, and the approve handler already refetches the session.
  • No migration; no schema change.

🤖 Generated with Claude Code

Two changes to how a session's wiki suggestions are produced. ## #609 — every suggestion was written twice `lore_consolidate_proposals` waits for its match tasks by calling `self.retry()`. That publishes a retry message and raises `celery.exceptions.Retry`, which is an `Exception` subclass — so the task's outer handler caught it and `_retry_unless_permanent` called `task.retry()` again, with the same `30 · 2ⁿ` countdown. Two identical deliveries then ran together, both saw every match row present, and both wrote. `create_bot_lore_proposals` deletes before inserting, but under READ COMMITTED neither delete sees the other's uncommitted rows. Measured before the fix: dev's replay session and one prod session each held 12 suggestions stored twice. - **`_retry_unless_permanent` now re-raises `Retry` untouched.** Fixed in the shared helper rather than in the one task: all four lore tasks route through it, and any of them gaining a `self.retry()` later would reproduce the bug. - **The write is now idempotent on its own**, because Celery delivers at least once whatever the retry code does. The task takes a row lock on the session before reading anything, then treats "already `ready` with no cache rows left" as "a twin finished this" and returns. ## #610 — suggestions waited for no one They were generated once, automatically, from the first transcription, and never again. They read the stored summary plus the raw transcript, never the verified events — so nothing a GM corrected could reach them. On the session used for pipeline testing, that left "Ironroot Hold" proposed as a real settlement after the GM's slip had been corrected in the events, alongside table chatter as lore. The owner's decision, recorded on #610: **suggestions should not exist until the GM has signed off on the content.** - **Generation moves to approval.** It leaves the summary fan-out entirely; `approve_session_content` queues it, only when `lore_generation_status is None`, so approve → withdraw → re-approve doesn't pay for a second extraction. A broker failure there is recorded and logged, never raised — approval is what the GM actually asked for. - **Corrections clear them.** `on_session_summary_available(..., first_time=False)` is the one funnel every summary change passes through — regenerate, restore and hand-edit — so a new `clear_wiki_suggestions` task fires there. It deletes the still-pending bot suggestions and resets the lore state to NULL, which returns the button to "Suggest wiki updates". - **Approved entries are never touched.** Approving a suggestion flips `proposed_by_bot` to False, so it cannot match the delete. - **The manual button is unchanged**, for a GM who wants suggestions before approving. ### Found while building, beyond the brief `process_audio` set `lore_generation_status = pending` eagerly, which worked only because the fan-out queued the generation in the same breath. With generation gone from the fan-out, that would have left every newly transcribed session claiming queued work that nothing would ever pick up — the #413 failure shape, self-inflicted. Removed, along with the enqueue-failure handling that existed to catch it. ### Known gap, narrow and deliberate A GM who regenerates **while an extraction is still running** gets the status reset, but the in-flight run still finishes and writes suggestions from the pre-correction text. That is no worse than today's behaviour, and closing it properly wants a generation epoch rather than a bigger delete. Noted on #610. ## Verification - Full backend suite: **3404 passed, 13 skipped**. - `ruff format --check` and `ruff check`, pinned to CI's 0.4.4: clean. - Frontend unchanged — the button already keys off `lore_generation_status`, and the approve handler already refetches the session. - No migration; no schema change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(webapp): stop double-writing wiki suggestions, hold them for GM approval (#609, #610)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Backend lint (ruff) (pull_request) Successful in 51s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 58s
CI / Docker image build (pull_request) Successful in 39s
CI / Bot tests and audit (pull_request) Successful in 2m29s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m14s
CI / Backend migration, tests, and audit (pull_request) Successful in 11m7s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 15m30s
4f05f3c4a7
#609 — lore_consolidate_proposals waits for its sibling match tasks by
raising self.retry(), which raises celery.exceptions.Retry — itself an
Exception subclass. The task's outer `except Exception` caught it and
called _retry_unless_permanent, which called task.retry() a *second*
time with the same backoff, so two identical retry messages ran
concurrently and both wrote the proposals via create_bot_lore_proposals'
delete-then-insert, with no protection against a concurrent twin under
READ COMMITTED. Fixed both halves:

  - _retry_unless_permanent now re-raises a Retry unchanged instead of
    retrying it a second time. Fixed in the shared helper (all four
    lore tasks route exceptions through it) rather than adding a
    one-off `except Retry: raise` to lore_consolidate_proposals alone,
    since any of the other three gaining a manual self.retry() later
    would reproduce the same bug.
  - lore_consolidate_proposals now takes a row lock on the session
    before reading anything, and re-checks "already ready, nothing
    left in the cache" after acquiring it — the backstop for the at
    least once delivery Celery guarantees regardless of the Retry fix.

#610 — wiki suggestions no longer generate automatically at
transcription time; they wait for a GM to approve the session's
content (or press "Suggest wiki updates" manually), and any suggestions
still pending review are discarded when the summary is edited out from
under them:

  - summary_events.py: generate_lore_proposals is no longer part of the
    fan-out at all. A new edit-only task, clear_wiki_suggestions, fires
    instead when first_time=False.
  - reminder_tasks.py: process_audio no longer eagerly sets
    lore_generation_status=pending on the initial transcription — since
    nothing queues the extraction task there any more, that would have
    left a permanent phantom "pending" with no task ever running (the
    #413 shape, self-inflicted). Also adds clear_wiki_suggestions,
    which deletes a session's still-pending bot proposals
    (proposed_by_bot=True) via a new lore_service helper that mirrors
    create_bot_lore_proposals' own delete scoping, leaves approved
    entries untouched, and resets lore_generation_status/error/
    progress_at to NULL.
  - sessions.py: approve_session_content now queues wiki-suggestion
    generation after the approval commits, mirroring the manual
    lore-proposals/generate endpoint, but only when
    lore_generation_status is None and a summary/transcript exists —
    so an approve/withdraw/re-approve cycle doesn't pay for a second
    extraction. A broker failure here is logged, not raised: approval
    must still succeed.

Frontend needs no change — SessionDetail.jsx's approve handler already
refetches the session via onAction(), and the "Suggest wiki updates"
button already keys off lore_generation_status, so it correctly hides
itself while the new auto-queued run is pending.

Tests extended in test_lore_generation_failure.py, test_lore_pipeline_tasks.py,
test_summary_events.py, test_enqueue_failure.py, test_post_session_tasks.py,
and test_sessions.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-11 23:00:38 +00:00
claude-bot deleted branch fix/609-610-wiki-suggestions 2026-09-11 23:16:39 +00:00
Sign in to join this conversation.
No description provided.