fc99bed55a
`marmot message react "$gid" "$id" "🍕"` was producing a kind:7 whose inner content was four `U+FFFD` replacement characters instead of the emoji. Whitenoise's `message_aggregator::emoji_utils` rejected it with `Invalid reaction content: ����`, and the openmls receiver retried the event ten times before giving up — by which point the secret tree had already consumed that generation, so the retry surfaced as a confusing `Ciphertext generation out of bounds 1 / SecretReuseError`. That last error was a downstream symptom; the root cause is argv decoding. Why it happens: JEP 400 (Java 18+) pins `file.encoding` to UTF-8 but deliberately leaves `sun.jnu.encoding` — the encoding the JVM uses to decode argv, filenames, and native interop strings — tied to the OS locale, and `sun.jnu.encoding` is read **before** the JVM parses any `-D` flag, so it can't be overridden via `applicationDefaultJvmArgs`. Under POSIX/C locales (no `LANG`/`LC_ALL` set, common on CI runners and stripped-down containers) `sun.jnu.encoding` ends up at `ANSI_X3.4-1968` — i.e. ASCII — and every byte > 0x7F in argv lands as `U+FFFD` before our code ever sees it. The four UTF-8 bytes of 🍕 (F0 9F 8D 95) become four replacement chars, get signed into the kind:7 content, and the test fails on receipt. The only place we can set the encoding from is the launching shell. Patch the gradle-generated start scripts (POSIX `amy`, Windows `amy.bat`) right after `:cli:startScripts`: * POSIX: if neither `LANG` nor `LC_ALL` is set, `export LANG=C.UTF-8` before invoking Java. Existing locale settings are respected. * Windows: `chcp 65001` to switch the console to UTF-8 before Java runs. Tested manually: with `LANG=` (sandbox default) `amy` previously sent `argv[0]=????` for an emoji argument; with the patch it sends `🍕` and `sun.jnu.encoding=UTF-8`, and whitenoise accepts the kind:7 reaction. The interop test 09 polling also needed a fix unrelated to the encoding bug: wn aggregates kind:7 reactions onto the anchor message under `.reactions.by_emoji.<emoji>` instead of surfacing them as standalone messages whose `.content` is the emoji, so the previous `wait_for_message B "$mls_gid" "🍕"` would have failed even with a correctly-decoded reaction. The new poll inspects each message's `.reactions.by_emoji` keys for the emoji literal. Marmot interop score: 15/16 → **16/16** 🎉 https://claude.ai/code/session_013VYkpz8P1mPh9Ejxy9anhJ