Taken by hand rather than cherry-picked, for the same reason as the
archive: the original was cut from a tree where text stories looked
different from the version rebuilt on this branch, and applying it whole
would have added a second `isTextStory`/`storyExpiry` beside the
existing `textStoryExpires`.
What came across is the sequence and nothing else — the `StorySet` wire
shape, the field on a story, and the two rules that decide how a set
behaves when it does not all arrive.
🔴 One envelope per item, sharing one set id. An array of attachments in
a single envelope is the obvious encoding and degrades badly: a client
predating §6.23 requires a single `att`, fails the guard, and renders
the whole set as a message in the conversation. Separate envelopes
degrade to several stories in order, which every existing build already
draws correctly.
🔴 `setExpiry` takes the EARLIEST. Every item is required to carry the
same `exp`, but a sender can write different ones — and if the latest
won, the difference would be a way to keep part of a sequence alive
after the rest had gone. A text story has no attachment and contributes
nothing, so one in a set cannot drag the shared expiry to zero.
⚠️ `groupedStories` renders what arrived. `n` is the sender's claim and
any item can fail to arrive, so a set is never held back or shown as
incomplete — a missing item is indistinguishable from one never sent.
777 tests green across app, engine and client, up from 648.