Skip to content

fix(cat): @cat has resolved to nobody for two days - #802

Merged
github-actions[bot] merged 4 commits into
mainfrom
fix/cat-handle-retired
Aug 28, 2026
Merged

fix(cat): @cat has resolved to nobody for two days#802
github-actions[bot] merged 4 commits into
mainfrom
fix/cat-handle-retired

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

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 to cat.

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.ts registers cat@orangecat.invalid precisely so handle_new_user mints cat. At 2026-08-26 21:58:59Z the handle became user_0234d5e38e66, and every @cat in a message or under a post has resolved to nobody since.

Nothing went red. Account still there, /profiles/cat still 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 .invalid for 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 from config/cat-identity.ts.

CodeensureCatAccount looked 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. checkCatHandle asserts 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

catomean and others added 4 commits August 28, 2026 08:18
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>
@github-actions
github-actions Bot merged commit 30abac2 into main Aug 28, 2026
8 checks passed
@github-actions
github-actions Bot deleted the fix/cat-handle-retired branch August 28, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant