Lane 8: taking a handout back deletes its Discord posts and DMs; handout posts use faces (#896, D157, D238) #909

Merged
claude-bot merged 4 commits from build/lane8-handout-takeback into main 2026-09-28 16:54:23 +00:00
Collaborator

Closes #896.

One take-back path

shelf_service.take_back now serves both a shelf card's revoke (O10) and a handout's Undo (§2.10). The callers differ only in how they find the reveal. In order, it:

  1. does nothing if the reveal is already revoked;
  2. stamps revoked_at;
  3. tells the campaign's other GMs (D238);
  4. commits;
  5. asks the bot to delete every post or DM the reveal made (D157).

Where a handout landed

  • A post to the table or a party reuses lane 10's POST /api/bot/reveals/{id}/message.
  • A handout sent by DM to a chosen audience records one row per DM through the new, additive, idempotent POST /api/bot/reveals/{id}/dm-message into shelf_reveal_messages.
  • Migration 8c9d0e1f2a3b on 8b8c9d0e1f2a. Up, down and up ran clean on Postgres 16. The downgrade drops the table.
  • One reveal_taken_back event carries extra.messages. When there was a channel post, the old top-level channel_id and extra.message_id are kept, so an older bot still deletes the post.
  • The bot deletes each message separately. DM channels are resolved on their own. A missing message or a Forbidden is skipped and logged, and the rest still go.

Fix outside the spec

create_handout_reveal now commits before dispatching to the bot. Before this, the bot's message-id report could race the commit, get a 404, and leave that message impossible to take back.

Faces

The handout embeds now use faces.handout. They have a golden row in test_faces.py, and the dev preview lists them.

Contract

The contract stays additive (v2), and check_version_sync passes. A newer backend with an older bot can't delete DMs, which only matters during an upgrade.

Checks

  • Backend (targeted): 733 passed (wiki, shelf reveal, bot, notifications, migrations).
  • Bot (full suite): 690 passed, 1 skipped.
  • ruff: clean.
  • Changelog: a changelog.d/ fragment.

Note: D227 (10e5f6a7b8c9) also chains on 8b8c. Whichever merges second gets re-chained.

🤖 Generated with Claude Code

Closes #896. ## One take-back path `shelf_service.take_back` now serves both a shelf card's revoke (O10) and a handout's Undo (§2.10). The callers differ only in how they find the reveal. In order, it: 1. does nothing if the reveal is already revoked; 2. stamps `revoked_at`; 3. tells the campaign's **other** GMs (D238); 4. commits; 5. asks the bot to delete every post or DM the reveal made (D157). ## Where a handout landed - **A post to the table or a party** reuses lane 10's `POST /api/bot/reveals/{id}/message`. - **A handout sent by DM to a chosen audience** records one row per DM through the new, additive, idempotent `POST /api/bot/reveals/{id}/dm-message` into `shelf_reveal_messages`. - **Migration `8c9d0e1f2a3b` on `8b8c9d0e1f2a`.** Up, down and up ran clean on Postgres 16. The downgrade drops the table. - **One `reveal_taken_back` event** carries `extra.messages`. When there was a channel post, the old top-level `channel_id` and `extra.message_id` are kept, so an older bot still deletes the post. - **The bot deletes each message separately.** DM channels are resolved on their own. A missing message or a Forbidden is skipped and logged, and the rest still go. ## Fix outside the spec `create_handout_reveal` now commits **before** dispatching to the bot. Before this, the bot's message-id report could race the commit, get a 404, and leave that message impossible to take back. ## Faces The handout embeds now use `faces.handout`. They have a golden row in `test_faces.py`, and the dev preview lists them. ## Contract The contract stays additive (v2), and `check_version_sync` passes. A newer backend with an older bot can't delete DMs, which only matters during an upgrade. ## Checks - **Backend (targeted):** 733 passed (wiki, shelf reveal, bot, notifications, migrations). - **Bot (full suite):** 690 passed, 1 skipped. - **ruff:** clean. - **Changelog:** a `changelog.d/` fragment. **Note:** D227 (`10e5f6a7b8c9`) also chains on `8b8c`. Whichever merges second gets re-chained. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
WIP checkpoint before tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(webapp,bot): one take-back path for shelf cards and handouts (#896)
All checks were successful
CI / Feedback site tests (pull_request) Successful in 44s
CI / Backend lint (ruff) (pull_request) Successful in 1m30s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Docker image build (pull_request) Successful in 37s
CI / Does this change touch the recording path? (pull_request) Successful in 23s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m57s
CI / Bot tests and audit (pull_request) Successful in 5m5s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m34s
CI / Frontend tests, audit, and build (pull_request) Successful in 18m19s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 30m3s
865cc08233
A handout's Undo and a shelf card's revoke now share
shelf_service.take_back: stamp, tell the other GMs (D238), commit, then
publish one reveal_taken_back event carrying every recorded post or DM
as extra.messages (D157). Lane 10's single-post fields stay in the
event, so an older bot still deletes a channel post; the bot deletes
every listed message, best-effort, before the channel check.

The handout reveal is committed before the bot is asked to post it, so
the bot's message-id report cannot 404 against an uncommitted row, and
the dm-message route is idempotent under redelivery. faces.handout gets
golden tests for its note and fallback title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.