fix(cat): @cat has resolved to nobody for two days - #802
Merged
Conversation
The email-derived-handle retirement renamed the Cat. Its predicate — `username = split_part(email, '@', 1)` — is exactly right for a person, whose public crawlable handle then republishes their email local part. It is exactly wrong for the Cat, because deriving the handle from the address is HOW the Cat is created: cat-account.ts registers `cat@orangecat.invalid` precisely so handle_new_user mints `cat`. So the script matched, and at 2026-08-26 21:58:59Z the handle became `user_0234d5e38e66`. `@cat` is the platform's advertised interface and it resolves by username, so every `@cat` in a message or under a post has since resolved to nobody. No reply anywhere. Found by typing `@` in the composer in production and seeing no Cat in the menu. Nothing went red. The account still existed, /profiles/cat still 301'd through profile_username_history, CI was green, health was 200. The only symptom was silence from the feature whose job is to answer. Three defects, three fixes: POLICY — the retirement could not tell a leaked address from a system identity. Now excluded by property, not by name: RFC 2606 reserves `.invalid` for undeliverable addresses, so there is no mailbox, no owner, and nothing personal in the local part. Hardcoding 'cat' in SQL would have put a second definition of the handle where it could drift from config/cat-identity.ts. Applied to the rename script and to count_email_derived_usernames(), which would otherwise have gone from 0 to 1 and turned the nightly gate red for a profile that is fine. CODE — ensureCatAccount looked the Cat up BY USERNAME, so its self-healing walked straight past a rename: lookup missed, createUser said "already registered", second lookup missed, returned null on every tick. It now keys on the login address, which no policy has reason to rewrite, and treats the handle as a field to assert rather than a key to search by. A rename now repairs itself on the next tick. GATE — nothing checked the product's own claim. checkCatHandle asserts that the name we tell people to type reaches the Cat; a lookup by id would have stayed green throughout. The literal it carries is pinned to CAT_USERNAME by a test, so the duplicate cannot drift. Proven by mutation: restoring the username lookup, removing the re-assert, and drifting the gate's literal each turn the suite red. The migration was rehearsed against production inside a transaction and rolled back — UPDATE 1, DELETE 1, 28 real retired handles untouched, gate still reads 0. Also restores the github.com/catomean links lost when this worktree was reset; the owner rename left them pointing at a redirect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
catomean is itself a redirect now — the repos live in the bitbaum org. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
Production has 84 profiles and 74 of them have no display name — the correct consequence of no longer minting names out of email local parts, where NULL is honest. The menu did not account for that, so what it actually showed on a bare `@` was two rows of `user_d58c7dccec41`, printed twice each: once as the name, because the name falls back to the handle, and once as the subtitle. Three changes, all following from the data rather than from taste. A nameless row now shows its handle ONCE, in the position the eye reads first, instead of the same hex string on two lines. A nameless profile is not offered at all on a bare `@`. You cannot be searching for a name that does not exist, so those rows were pure noise in the one position that matters most — the first thing anyone sees when they discover the feature. Type any part of the handle and they come straight back through the existing prefix rules. At equal match quality, a named profile outranks a nameless one. `@u` matches `ursula` and `user_d58c7dccec41` equally well by anything the score can see, and only one of them is a row a human can act on. The tiebreak sits after match quality, never before it, so an exact handle match still wins — a demotion that overrode "this is plainly the row being asked for" would be a worse bug than the one it fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
Both branches edit the same line; one owner avoids a conflict that would stop CI running on whichever loses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by typing
@in the composer in production: the Cat was not in the menu. It is not in the database either — no profile answers tocat.The email-derived-handle retirement renamed it. That predicate (
username = split_part(email,'@',1)) is exactly right for a person, whose crawlable handle then republishes their email local part. It is exactly wrong for the Cat, because deriving the handle from the address is how the Cat is created:cat-account.tsregisterscat@orangecat.invalidprecisely sohandle_new_usermintscat. At 2026-08-26 21:58:59Z the handle becameuser_0234d5e38e66, and every@catin a message or under a post has resolved to nobody since.Nothing went red. Account still there,
/profiles/catstill 301s through the history table, CI green, health 200. The only symptom was silence from the feature whose job is to answer.Three defects, three fixes.
Policy — the retirement could not tell a leaked address from a system identity. Excluded by property, not by name: RFC 2606 reserves
.invalidfor undeliverable addresses, so no mailbox, no owner, nothing personal in the local part. Hardcoding'cat'in SQL would have put a second definition of the handle where it could drift fromconfig/cat-identity.ts.Code —
ensureCatAccountlooked the Cat up by username, so its self-healing walked straight past a rename and returned null on every tick. It now keys on the login address, which no policy has reason to rewrite, and treats the handle as a field to assert rather than a key to search by.Gate — nothing checked the product's own claim.
checkCatHandleasserts that the name we tell people to type reaches the Cat; a lookup by id would have stayed green throughout.Proven by mutation (each turns the suite red): restoring the username lookup, removing the re-assert, drifting the gate's literal. The migration was rehearsed against production in a transaction and rolled back —
UPDATE 1,DELETE 1, 28 real retired handles untouched so Lightning addresses keep resolving, gate still reads 0.🤖 Generated with Claude Code
https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5