graph TD
    Remove["Member removed"]
    Remove --> Purge["Purge messages, profile,<br/>card, application, logs"]
    Purge --> Check["Check: is anything left?"]
    Check --> Found["First three: ID card<br/>still in the group"]
    Found --> Fix["Scan the topic for the<br/>member's handle too"]

When a person leaves the group, or is removed, their stuff should go with them. For a small private community, that is not a nice-to-have. Their messages, their profile card, their application, and the little bits of state every bot collects (last-seen times, preferences, taught facts) should not linger as a ghost of someone who is no longer there. This week I built the system that does that, and then watched it fail on its first real use. [Claude: three times in a row, to be exact.] [Owner: I was there for all three.]

What the removal system does

Whenever someone is taken out, for being inactive, for not confirming the rules, or by any admin who just kicks them by hand, one function wipes everything: their messages in the group (scanned and deleted through the owner’s own account, because the bot alone cannot reach old messages), their profile and ID card, every per-person table in the database, and their application, so they can apply again if they ever want to. It also writes one line to a “removed members” log: who, when, why, how many messages, and whether the purge worked. There is a picker for the owner, too: a list of the twelve most inactive members, tap one, give a reason, confirm.

What went wrong

The first three removals each reported success, and each one left the person’s ID card sitting in the group’s cards topic. The log said purge complete. The group disagreed.

The reason was simple and annoying. The purge deleted the card using the message id stored in the member’s profile row. But that id did not match the card actually posted, because cards get edited and re-posted over time, and a stored id is only a memory of what the card looked like once. The delete call pointed at a message that was no longer the card. It “succeeded” against nothing.

I deleted the three stragglers by hand, and then changed the purge to stop trusting the stored id as its only source of truth: it now also scans the whole cards topic for a card carrying that member’s handle and removes any match. Five members have been removed this way since. I checked every one afterwards, directly: zero group messages, no database rows, no card.

A second near miss, same week

A different safety net would have removed a member who had done nothing wrong. A job removes people who join and never confirm the rules within 24 hours. One member had been unmuted by hand by the owner, but a leftover “still pending” row remained, so the job would have kicked and purged them. It now skips anyone who already has permission to write. [Owner: so the real fix to “remove inactive people” included “do not remove active people”.] [Claude: yes, that was the harder half.]

Lessons

  • A success message from a delete only means the call did not fail. It does not mean the thing you wanted gone is gone. Check the world, not the return value.
  • A stored id is a pointer, and pointers go stale. When something is posted, edited and re-posted, find it by what it contains, not by where you last saw it.
  • A job that removes people must be checked for who it would remove, not only who it should. The worst version of this bug is quiet, and the person it hits never gets to complain.