The previous chapter ended with secret-bearing API calls using verified TLS, OpenAI failures becoming actionable, and a song request losing the ability to disguise itself as a yt-dlp option.
This chapter begins immediately after MB420.
It covers fourteen Claude rounds and one final independent pre-commit audit:
MB421 readable Fortnite numbers
MB422 YouTube search durations keep their days
MB423 one shared YouTube view formatter
MB424 TMDB falls back to the original title
MB425 real IRC strikethrough for discarded dice
MB426 accented words stay whole in wordcount
MB427 compat and mood understand the same accented tokens
MB428 quote excerpts stop cutting UTF-8 glyphs
MB429 one shared UTF-8 truncation helper
MB430 achievement channel keys become canonical
MB431 extending an expired poll starts from now
MB432 Hailo ratio lookups leave the hot message path
MB433 leap-day birthdays are observed in short Februaries
MB434 birthday countdown agrees with the announcement
MB435 final audit of Unicode, cache invalidation and mixed-case aggregates
Mediabot remains on the 3.2dev line.
This is not the 3.3 release.
As always:
No database schema was changed.
The Fortnite API may return long floating-point values for K/D and win rate.
A perfectly ordinary result could therefore arrive on IRC as:
K/D: 1.2345678901234 | Wins: 42 (8.5714285%)
MB421 changes only the presentation:
K/D β two decimal places
win rate β one decimal place
trailing zeroes removed
unexpected non-numeric values preserved rather than forged into zero
The same result now reads like a score:
K/D: 1.23 | Wins: 42 (8.6%)
The main YouTube formatter already understood durations such as:
P1DT2H3M4S
The search command still contained an older inline parser that ignored the day component. A 26-hour archive could be displayed as only 2:03:04.
MB422 removes that duplicate interpretation and reuses the shared ISO-8601 parser. Search output remains compact by folding days into hours:
P1DT2H3M4S β 26:03:04
P3D β 72:00:00
Two more inline formatters still disagreed on whether 45,000 views should look like:
45K
45.0k
MB423 routes every YouTube path through the existing shared formatter:
1,234 β 1.2k
45,000 β 45.0k
1,200,000 β 1.2M
Unknown values are omitted from search lines instead of being advertised as a mysterious question mark.
TMDB sometimes returns an empty localized title while still providing a valid original title.
The previous selection looked only at the localized movie or series field and could display:
Unknown title
for a result whose real name was present in original_title or original_name.
MB424 chooses the first non-empty value from:
title
name
original_title
original_name
It also derives βMovieβ or βTV Seriesβ from the already validated media_type rather than guessing from which title field happened to exist.
Advantage and disadvantage rolls marked the discarded die with Markdown syntax:
[15, ~~8~~]
IRC clients displayed the tildes literally.
MB425 replaces them with the real IRC strikethrough control code followed by a reset. Compatible clients show the discarded result crossed out; simpler clients still show the number normally.
No Markdown costume remains in the IRC output path.
CHANNEL_LOG.publictext is returned by DBI as UTF-8 bytes rather than decoded Perl characters.
Splitting those bytes with character-oriented expressions broke French words apart:
cafΓ© β caf
rΓ©ponse β r + ponse
NoΓ«l β No + l
MB426 makes wordcount use a byte-safe token boundary so UTF-8 byte sequences stay together.
MB427 applies the same rule to:
compatibility vocabulary
mood sentiment vocabulary
This matters especially for the French sentiment dictionary. Words such as ratΓ© and pitiΓ© can now reach the dictionary intact instead of becoming fragments that never match.
Quote excerpts were shortened with raw substr calls at byte limits such as 200 or 300.
If the boundary landed between the two bytes of an accented character, the output ended with invalid UTF-8 and a replacement glyph.
MB428 adds a safe quote excerpt routine and applies it to:
quote view
quote search
random quote
quotes by nick
MB429 promotes the same behaviour into one shared helper:
Mediabot::Helpers::truncate_utf8()
The helper is then reused for channel-log excerpts and Partyline displays rather than maintaining several subtly different blades.
Achievement keys folded the nick to lowercase but kept the channel spelling exactly as received.
Because IRC channel names are case-insensitive, the same hall could acquire two trophy cabinets:
teuk + #Teuk
teuk + #teuk
That allowed duplicate unlock announcements and made lookups dependent on casing.
MB430 lowercases the channel component in every achievement key and migrates existing JSON data during load.
When two historical keys collapse into one, achievements are merged and the earliest unlock timestamp wins. Existing trophies are not discarded.
Poll expiration is lazy: a deadline may have passed while the in-memory poll still says it is active.
pollextend used to add time to the old deadline even when that deadline was already in the past:
past deadline + 60 seconds β still in the past
The command could announce a negative remaining time and fail to reopen voting.
MB431 anchors the extension to:
max(current time, existing deadline)
Future polls still extend cumulatively. Lapsed polls receive a genuinely future deadline.
The Hailo chatter decision runs for every public message.
Its channel ratio lookup still performed a SELECT and JOIN on every one of those messages, including channels with no configured ratio.
MB432 adds a lowercase-keyed 60-second cache and stores negative results as well:
30 messages β one lookup instead of 30
An explicit ratio change invalidates the cache so the command does not have to wait for the TTL.
The final audit found one missing branch in that promise; MB435 completes it below.
A person born on 29 February was announced only in leap years because the daily calendar simply never reaches 02-29 in the other three years.
MB433 adopts a clear observance rule:
leap year β celebrate on 29 February
non-leap year β celebrate on 28 February
The Gregorian century rules remain respected: 2000 is a leap year; 2100 is not.
MB434 then aligns birthday next with the same rule. The countdown no longer points several years ahead to the next literal 29 February while the daily announcement plans to celebrate the person on the coming 28th.
The song and the countdown now agree.
The fresh MB434 snapshot was reviewed rather than committed blindly.
Three real gaps remained.
truncate_utf8() correctly handled raw UTF-8 bytes from DBI, but Partyline also uses it for Claude history returned by a JSON decoder.
That content is already a Perl character string.
Trying to decode it as UTF-8 a second time can raise:
Wide character
and break .ai context precisely when the answer contains non-ASCII text.
MB435 now distinguishes the two representations:
raw byte string β trim incomplete UTF-8 tail, return bytes
decoded char string β count encoded wire bytes, cut by codepoint, return chars
No double decoding and no severed glyph.
MB432 invalidated the new ratio cache after inserting the first setting for a channel.
The much more common path β updating an existing row β returned before that invalidation.
A successful setting change could therefore keep using the previous percentage for up to 60 seconds.
MB435 invalidates after both successful database paths.
MB430 canonicalized stored achievement keys, but the dashboard and achievement leaderboard still compared them with the unmodified live channel string.
On a mixed-case target, real trophies could disappear from those two aggregate views.
Both scans now fold the live channel before comparison.
Claude reported the following full-suite baseline before the independent audit:
full suite through MB434: 8869/8869
The independent audit added:
MB435 contract test: 15/15
locally executable MB421βMB435 set: 148/148
fresh-copy installation: OK
second installation: idempotent
forced rollback: OK
embedded payloads: SHA256 verified
source diff whitespace check: OK
The audit container does not contain the complete Perl runtime used by teuk.org, notably JSON::MaybeXS and IO::Async. It therefore does not claim a new full-suite total.
The MB435 installer performs the real syntax checks, the complete MB421βMB435 focused selection and the affected-area regression on the server before the commit can proceed. Any failure restores the MB434 files automatically.
The focused and full-suite totals overlap and must not be added together.
0 new tables
0 altered columns
0 migrations
0 schema changes
0 release to 3.3
Mediabot remains on:
3.2dev-*
Mediabot now:
The castle did not gain a spectacular new tower in these rounds.
Instead, its clocks stopped losing days, its scribes stopped cutting runes in half, its trophy room stopped changing names, and even the people born on a date that sometimes vanishes were finally given a reliable place in the calendar.
That is the sort of quiet magic a stable release is built from.
You must be logged in to reply.