Skip to content

fix(profile): a rename recorded nothing, so the old handle died silently - #833

Merged
catomean merged 1 commit into
mainfrom
fix/handle-rename-records-history
Aug 29, 2026
Merged

fix(profile): a rename recorded nothing, so the old handle died silently#833
catomean merged 1 commit into
mainfrom
fix/handle-rename-records-history

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

What a user hit

Asked the Cat to change their handle, and got:

usernames on OrangeCat cannot be changed once set … changing it would break all existing links

Both sentences are wrong, and the Cat was not inventing them. Its brief said
Never update username (it breaks public URLs), and the update_profile
handler excluded the field with the same comment.

That was true until profile_username_history (20260826160000), which exists
precisely so that a rename breaks nothing: /profiles/<old> 301s to the new
handle, and <old>@orangecat.ch keeps resolving through it. Neither the prompt
nor the handler was corrected when the database gained the capability, so the
product kept refusing something it could already do.

The bug underneath the wrong answer

PUT /api/profile has always accepted a new username, and the profile editor
has always shown an editable handle field — and neither wrote the history
row.
The only writer was the one-off admin script.

So renaming yourself through the product got you exactly the breakage the Cat
warned about: the old profile URL 404s, the Lightning address stops resolving,
and nobody sees an error until a payment fails to arrive.

Measured against production before this change — renaming a live profile
recorded zero history rows:

FAIL (a): rename recorded no history row (n=0)

A second hole, named in a comment in 20260826160000 and never enforced:
availability was checked against profiles alone, so a handle someone else
retired read as free
. Both the profile page and LNURL resolve live profiles
first, so taking one would have silently handed the new holder the previous
owner's inbound links and payments.

The fix

The rule lives in the database, not in the route. A username is a payment
identifier, so "a rename is recorded" has to hold for the API route, this
repo's SQL scripts, a psql session, and whatever gets written next — and there
is exactly one place a username can change. Fixing only the route would leave
the same bug one new caller away.

  • profiles_username_rename_guard — records the retired handle on every
    rename, and refuses a handle another account retired.
  • checkUsernameAvailability now consults retired handles, so the person
    typing it learns from the form rather than from a failed save.
  • The Cat can rename you, and says what happens to the old handle.
  • The profile-editor tip "Cannot be changed easily later" was simply false.

Verification

Trigger proved against the production schema in a rolled-back transaction —
plpgsql plans at run time, so a unit test could not have caught a bad column:

PASS (a): the rename recorded the retired handle
PASS (b): reissuing a retired handle to another account was refused
PASS (c): reclaiming your own handle clears its retired row
PASS (d): saving the profile unchanged retires nothing
PASS (e): a case-only change retires nothing

Run without the migration, check (a) fails on real data — the proof is not
vacuous. Both new unit suites were mutation-tested the same way: removing the
guard fails 1 test, removing the rename write fails 3.

npm run verify green: 269 suites, 2566 tests.

Note on the prompt budget

The static Cat prompt sits one character under a budget that only ratchets
down
(it is why platform Groq cannot serve Cat). So this does not add a new
action — the rename folds into update_profile, and the new wording is paid
for by compressing the same line. The ceiling is untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn

Reported by a user: asked to change their handle, the Cat replied that
usernames "cannot be changed once set" because it "would break all existing
links". It was not inventing that — its brief said "Never update username (it
breaks public URLs)" and the handler excluded the field for the same reason.

Both were true until profile_username_history (20260826160000), which exists
precisely so a rename breaks nothing: /profiles/<old> 301s to the new handle
and <old>@orangecat.ch keeps resolving. Neither the prompt nor the handler was
corrected afterwards, so the product went on refusing something it could do.

Underneath the wrong answer was a real bug. PUT /api/profile has always
accepted a new username, and the profile editor has always shown an editable
handle field — but neither wrote the history row. The only writer was the
one-off admin script. So a user who renamed themselves through the UI got
exactly the breakage they were warned about: old profile URL 404s, Lightning
address stops resolving, no error to anyone, and a payment sent to the address
they had published simply does not arrive.

Measured against production before this change: renaming a live profile
recorded zero history rows.

The rule now lives in the database, not in the route. A username is a payment
identifier, so "a rename is recorded" has to hold for the route, for this
repo's SQL scripts, for a psql session, and for whatever gets written next —
and there is exactly one place a username can change. The same trigger refuses
a handle another account retired: the profile page and LNURL both resolve live
profiles FIRST, so reissuing one would hand the new holder the previous
owner's inbound links and payments. 20260826160000 named that risk in a
comment; nothing enforced it, and availability was checked against profiles
alone, so every retired handle read as free.

Verified against the production schema in a rolled-back transaction: a rename
records, a reissue is refused, reclaiming your own handle clears its retired
row, and neither an unchanged save nor a case-only change retires anything.
Without the migration the first check fails on real data.

The Cat can now do it too. Folded into update_profile rather than a new
action: the static prompt sits one character under a budget that only
ratchets down, so the capability is paid for by compressing the same line
instead of raising the ceiling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
@catomean
catomean merged commit a4d6d4a into main Aug 29, 2026
8 checks passed
@catomean
catomean deleted the fix/handle-rename-records-history branch August 29, 2026 06:59
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