Bot.listen awaited every frame to completion, so one Commons fetch or one
attachment upload held up every text reply behind it (DOCS-latency.md).
Each frame now runs in its own task, never more than LIMIT at once; the
reader waits for a slot before spawning, so a burst applies back-pressure
at the websocket instead of fanning out upstream. One person's commands
stay serialised (user_lock.py): two quick !kot would otherwise both pass
the cooldown gate before either recorded a use.
Every served command reports its timings split into the bot's own time
and everybody else's (served.py); /_selftest carries that block and
tool/latency.py holds own_ms to 100 ms at p95, with the command's
cooldown switched off for the run and put back afterwards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds !papież / !papiez / !pope: a random pope as a photo, captioned with his
Latin name, reign and pl.wikipedia link. popes.json already carried a Commons
portrait filename for all 267 and nothing had ever consumed it.
The Latin name comes from the Latin column of Poczet papieży, not from the
la.wikipedia sitelink. The sitelink resolved for all 267 and was still wrong:
la.wikipedia numbers several popes differently, so #154 came out as
"Stefan IX — Stephanus X" in a single caption, which reads as an off-by-one.
The table's own column says "Stephanus Nonus" and cannot disagree with the
name beside it. LATIN_OVERRIDES ships empty; it is a tripwire, not a backlog.
Portraits are cached on disk under CacheDirectory=signal-bot, named from the
ordinal so no upstream string ever reaches a path, and bounded at 267 entries
so nothing needs evicting. Verified that CacheDirectory wins against
ProtectHome=read-only before relying on it.
Also adds POST /_selftest and scripts/selftest.sh. Signal never delivers an
account its own sends, so no command has ever been exercised end-to-end
without asking a human to type it. This injects a real frame into the live
dispatcher; it is loopback-only and refuses any group not in SELFTEST_GROUPS,
which is empty by default.
Verified against the real endpoints, not just the fake: 267/267 portraits
resolve on Commons, regeneration is byte-identical on every previously
shipped field, and !papież was driven 7 times through the live dispatcher in
testbot, returning distinct popes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom