Stress-testing the beta with request shapes we had not tried turned up two
more ways to hit the same class of bug. Roles were compared exactly, so a
client sending "Assistant" instead of "assistant" fell straight into the
dingus error. And content was assumed to be a string, so OpenAI's multipart
array form did the same. Roles are now compared case-insensitively and
content is flattened to text first; messages with no usable text are
skipped rather than mistaken for a slot. The slots are plain strings now,
so no caller can reach past them to a raw field again.
Separately: the Changelog button carries a dot when the current release
has not been opened yet, on the desktop button, the mobile menu item, and
the hamburger that hides it. The marker is the version in localStorage, so
every release brings it back on its own and reading it once clears it. The
version is a single constant now rather than being written in two places.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
The first pass read the name from the last user turn and the token from
the last assistant turn. Both are wrong once a conversation continues.
Only sucker's *first* reply carries a conversation token — later ones say
"Character updated" or "unchanged" with no token in them — so reading the
last assistant turn lost the link three turns in and would have created a
duplicate card on every message. tokenCandidates() now searches the whole
conversation, still preferring whatever the user just typed, since pasting
a token is deliberate.
The name is the first user turn after the greeting, not the last one. That
is what the old code read at messages[3], and unlike the last turn it stays
put as messages are appended.
Tests cover a three-turn conversation for both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
Some users' JanitorAI sends a "." user turn between the card and the
greeting; others' does not. Sucker read fixed indices, so on the shorter
shape every slot was off by one: first_mes was set to the name line, and
the name slot did not exist at all. That TypeError was caught by the
handler and reported as the "you dingus, read the directions" message,
which blamed the user for a shape they never chose.
src/lib/messages.ts now resolves the slots by role instead — the card is
the system turn, the greeting is the first assistant turn, the user's
latest input is the last user turn, and sucker's own previous reply is the
last assistant turn. All four hold in both shapes and in a continued
conversation, so the conversation token is found in either as well.
Extraction now throws a description of what is missing rather than a
TypeError, and tests/messages.test.ts pins both shapes. The tests need no
framework: `npm test` runs node's built-in runner.
Existing cards are unaffected: the update path already discards the
extracted name and keeps the original.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa