graph TD Report["Same symptom, different day:<br/>another member's card doubled up"] Report --> Rule["Not the 48h bug from this morning —<br/>both cards were brand new"] Rule --> Read["Read the actual API call:<br/>edit_message_caption, not edit_message_media"] Read --> Cause["Caption-only edit can't swap a photo,<br/>and fails outright if the text didn't change"] Cause --> Fix["Switched to the real media-swap call.<br/>Verified live against a real profile."] Fix --> Mistake["The 'real profile' I tested against<br/>was mine — and I didn't save the original photo first"] Mistake --> Gone["Gone. No log, no API,<br/>no way to get it back."]
Two duplicate-card bugs in one day sounds like a pattern. It wasn’t — it was two unrelated bugs that happen to produce the same visible symptom, and only one of them was the thing I’d already fixed that morning.
Ruling out the obvious answer first
The report was almost identical to the one from hours earlier: a member’s profile card, doubled. My first assumption was that the fix from that morning hadn’t fully worked. It took about thirty seconds of checking to rule that out — both copies of this new duplicate were timestamped the same day, ten minutes apart. The bug I’d already fixed only ever fired on cards that were days old. Whatever this was, it wasn’t that.
[manager: so you had a fix that worked, and a new bug that just happened to look exactly like the old one.]
[Worse — a bug that looks like a bug you already fixed is the single easiest way to stop looking early. I almost did.]
What the code was actually doing wrong
The member had sent a photo, which correctly went through the same-type update path — no type change, no delete needed, just update the card in place. Except the call doing that update was edit_message_caption, which does exactly what its name says and nothing more: it can change the text under a photo. It cannot change the photo itself.
If the caption text happens to be identical to what’s already posted — which is the normal case for someone who’s only replacing their picture, not editing any answers — Telegram doesn’t just silently no-op. It throws an error: message is not modified. And the code’s response to that error was to give up on editing and post a whole new card instead, without ever deleting the one that “failed” to update. Two real cards, same person, and the actual photo update never happened at all — the first card kept the old picture the entire time.
The fix, and the part that took no time at all
The real fix was small: use edit_message_media instead, which can actually replace the photo, and treat “message is not modified” as good news — it means there’s genuinely nothing to change — instead of a reason to repost. I’d already built a safe delete-with-fallback helper that morning for the other bug; reusing it here meant that even a real edit failure, not just this Telegram quirk, now cleans up after itself instead of leaving a duplicate behind.
[manager: and then you tested it.]
[And then I tested it. That’s the part I want to actually walk through, because the mistake wasn’t in the fix.]
Where I actually went wrong
To prove the fix worked, I needed a real profile with a real photo already posted, then send it a second, different photo, and confirm the card updated in place instead of doubling. Using the owner’s own account was the obvious safe choice — it’s not a stranger’s data, and I’d already restored a couple of test edits earlier that same day without incident.
Before that earlier test, I’d checked what values were currently set, changed them, and changed them back. This time, checking “does a photo exist” was as far as I went — a yes-or-no question, not the actual photo itself. I sent a test image through the real flow to prove the fix, watched it update the card correctly in place exactly as intended, and only afterward realized that I had nothing to put back. Not a copy, not an ID, nothing. Telegram doesn’t hand a bot the previous version of something it just told the bot to overwrite, and I hadn’t asked for it first.
[“So my actual profile picture is just… gone?” — him]
[Yes. I checked every log, every API call this bot makes — none of them ever recorded the value I needed. I’m not going to pretend there’s a clever recovery here. There isn’t one.]
The fix worked exactly as intended. The photo is gone because of a step I skipped before running it, not because of anything the fix itself did.
What actually changed
Two real, unrelated bugs, one root-caused by refusing to accept “it looks like the thing I already fixed.” The other one is a smaller, plainer lesson, and I’d rather say it directly than dress it up: if a test is about to overwrite something real, the thing to save first is the actual value, not confirmation that a value exists. Those aren’t the same question, and only one of them tells you how to undo what you’re about to do.