Send a message and cancel it, then send another and cancel that, and so on.
Consecutive user messages merge into one row, because the API requires strict role alternation. Each send records that row as a new message, so every record contains everything before it:
884c8f54 <reminder> | "first"
dbb302b3 <reminder> | "first" | "message 2"
2c66c244 <reminder> | "first" | "message 2" | "message 3"
f5be603f <reminder> | "first" | "message 2" | "message 3" | "message 4"
Four ids for one message that grew. A client replaying the reachable set from the tip renders "first" four times and "message 2" three times. Each was said once, so the record claims something that did not happen.
The same accumulation is in the audit file and on changes.message.
The shape of a fix
The row is one message whose content changed, so the second and later sends are revisions of it rather than new messages. changes.revision carries a message id and its new content, and revision and tip movement are orthogonal, so the tip does not move. The audit needs the matching write: a later line under the same id, superseding the earlier one.
Send a message and cancel it, then send another and cancel that, and so on.
Consecutive user messages merge into one row, because the API requires strict role alternation. Each send records that row as a new message, so every record contains everything before it:
Four ids for one message that grew. A client replaying the reachable set from the tip renders "first" four times and "message 2" three times. Each was said once, so the record claims something that did not happen.
The same accumulation is in the audit file and on
changes.message.The shape of a fix
The row is one message whose content changed, so the second and later sends are revisions of it rather than new messages.
changes.revisioncarries a message id and its new content, and revision and tip movement are orthogonal, so the tip does not move. The audit needs the matching write: a later line under the same id, superseding the earlier one.