v0.14.1 a reconnection resumes instead of replaying
Fixed. A reconnection replayed the whole conversation instead of resuming it. EventSource reconnects by itself after any drop, and the server answered every reconnection with the entire transcript, while the page - which has no de-duplication and had never been given an id to resume from - appended a second copy of everything. Measured before the fix: three consecutive subscriptions each received all 21 events of the same conversation, and not one carried an id:.
Events that go into the history are numbered now, and a reconnection is answered from where it got to. State events stay unnumbered on purpose: an id moves the resume point, and busy or usage are not places to resume from. The id also carries an epoch, because a position only means something inside one transcript - after a reset, or after the process restarts, the same index points at something else, so a mismatch replays from the beginning and says so first, and the page drops what it was holding instead of growing a chimera of two conversations.
Two more of the same family, found by reading rather than by a failure. The token meter never reached a listener that arrived after a turn had ended, on the one screen whose whole job is to say how big the transcript has become; the current reading is now sent at subscribe time. And a page reconnecting to a restarted process kept believing in the run that died with it, leaving the composer stuck on "queue for next turn" forever.
Accessibility. The queued-message chip was a span with an onclick, so pulling a queued message back into the composer needed a mouse: no tab stop, no Enter, no name. It is a button now. The composer's placeholder was its only hint and drew at 2.2:1 against the 4.5:1 text has to meet, so it moves one step up the ink scale and the field gains a hidden label; the step numbers keep the dimmer ink, which is what it was chosen for. And the page now declares its language, which it never did.