An event is permanent and public the moment it is posted, and any app can post. The first time a summary carries a customer's name or a link carries a token in its query string, relay has no answer: there is no delete, no edit and no way for an admin to take it back, at any door.
A hub that cannot be corrected is a hazard the crews adopting it will discover at the worst moment. Redaction, not deletion: the row stays, the words go.
Done looks like
[ ] redactEvent(db, who, { id, reason }) in src/actions.ts, admin only (signin_required for anon, forbidden for a user, not_found for an unknown id): it blanks subject, summary and link in place, sets redacted_at and redacted_by, keeps source, kind, occurred_at and the id, and is idempotent — redacting twice changes nothing and emits once.
[ ] A migration appends the three columns. The words are gone from the row, not hidden by a flag: a redaction that leaves the text in the database is not a redaction, and docs/adopting.md says so in the sentence that tells a crew what to do when they post something they should not have.
[ ] Every door shows the same thing: a redacted row renders as "Redacted" in the timeline (still filterable, still in the right place in time, greyed by .redacted), the JSON carries redacted_at with nulls where the text was, MCP returns the same, and the terminal prints the same word. No door reconstructs what was there.
[ ] It is live: emit("event.redacted", { id }) and static/live.js replaces the row's text in place on an open page, because the whole point is that the words stop being on screens that are already open.
[ ] The reason is for the log, not the page: validated like a summary, stored, never rendered — an explanation of a leak can repeat the leak.
[ ] dedupe survives redaction, so a poster retrying a redacted event gets the redacted row back rather than re-creating the text. A test proves exactly that.
[ ] Tests: the role rules, the columns actually blanked (read the row back and assert the old text is absent), idempotency, the dedupe retry, the live message, and every door's rendering.
Risks: the tempting version — a hidden column the doors filter on — is the wrong one: it leaves the text in the file, in a backup and in anything that reads the table directly, and the request that prompts a redaction is usually the one that needs the bytes gone. What cannot be undone must be behind the same rules as everything else: the check lives in the action, the doors only call it, and a redaction is itself an event worth seeing in the timeline once #33 lands. Deletion of the row is deliberately not offered — an id that vanishes breaks the paging cursor and every link an agent has quoted.
Suggested: Scout to build, Sentinel to review — it is a small action with one irreversible effect.
An event is permanent and public the moment it is posted, and any app can post. The first time a summary carries a customer's name or a link carries a token in its query string, relay has no answer: there is no delete, no edit and no way for an admin to take it back, at any door.
A hub that cannot be corrected is a hazard the crews adopting it will discover at the worst moment. Redaction, not deletion: the row stays, the words go.
Done looks like
redactEvent(db, who, { id, reason })insrc/actions.ts, admin only (signin_requiredfor anon,forbiddenfor a user,not_foundfor an unknown id): it blankssubject,summaryandlinkin place, setsredacted_atandredacted_by, keepssource,kind,occurred_atand the id, and is idempotent — redacting twice changes nothing and emits once.docs/adopting.mdsays so in the sentence that tells a crew what to do when they post something they should not have..redacted), the JSON carriesredacted_atwith nulls where the text was, MCP returns the same, and the terminal prints the same word. No door reconstructs what was there.emit("event.redacted", { id })andstatic/live.jsreplaces the row's text in place on an open page, because the whole point is that the words stop being on screens that are already open.dedupesurvives redaction, so a poster retrying a redacted event gets the redacted row back rather than re-creating the text. A test proves exactly that.Files:
src/db.ts,src/actions.ts,src/actions.test.ts,src/routes.ts,src/page.ts,src/mcp.ts,src/tty.ts,static/live.js,docs/adopting.md,README.mdRisks: the tempting version — a
hiddencolumn the doors filter on — is the wrong one: it leaves the text in the file, in a backup and in anything that reads the table directly, and the request that prompts a redaction is usually the one that needs the bytes gone. What cannot be undone must be behind the same rules as everything else: the check lives in the action, the doors only call it, and a redaction is itself an event worth seeing in the timeline once #33 lands. Deletion of the row is deliberately not offered — an id that vanishes breaks the paging cursor and every link an agent has quoted.Suggested: Scout to build, Sentinel to review — it is a small action with one irreversible effect.