Expand description
Hosted discussion sync bridge.
Local discussions live in the append-only CollaborationStore op-log
(.heddle/collaboration/ops). The hosted weft CollaborationService speaks
a different, per-state DiscussionsBlob model (id-keyed discussions with a
linear turn list). This module bridges the two:
- Push (write path): after a successful
heddle push, replay local symbol-anchored discussion turns we authored to the server via the caller-authenticatedOpenDiscussion/AppendTurnRPCs (enforce-mode signed). #549 rejects attachments in the pack, so they cannot ride it. - Pull/clone (read path): after a successful clone/pull,
ListByStatethe head state’s discussions and materialize any turns we do not already hold into the local op-log sodiscuss list/discuss showsee them.
§Turn identity
Local turn order is op-log materialization order; server turn order is
push/append order. They diverge the moment both sides append, so a single
“N turns synced” prefix count is a lie. Instead the per-repo mirror map
(.heddle/collaboration/hosted-mirror.json) records, per discussion, an
explicit set of turn links: a local turn id ↔ a server turn ordinal.
A local turn id is (CollabOpId, index-within-op) — NOT the CollabOpId
alone, because a LegacyImported op (a migrated blob→op-log discussion)
materializes all its turns under one shared CollabOpId. Keying on the op
alone would give turns 2..N the same idempotency key with different bodies
(a weft with_idempotency conflict) and collapse them into one link, so the
rest would be silently dropped on exactly the migrated repos.
Push sends only turns that are self-authored AND unlinked; pull materializes only server ordinals not yet linked. Client operation ids are derived from the stable turn id, so a retry replays instead of conflicting.
§Reconciliation is author-aware, never body-alone
When the mirror map is lost/rebuilt, an unlinked server turn is reconciled
against an unlinked local turn only under an explicit author rule — never
body equality alone, which would cross-link two different authors’ identical
bodies ("lgtm", "+1") and silently drop one:
- (i) a turn WE pushed — the local turn is self-authored AND the server
turn’s author is our own hosted username (weft stamps
Principal::new(username, "")); or - (ii) a turn we previously PULLED — the local op’s author (written as
Principal::new(author_name, author_email)) andoccurred_at_msexactly equal the server turn’s author andposted_at.
Anything matching neither rule materializes as a new, distinct turn. Distinguishing “a turn I pushed” from “a turn another clone of the SAME user pushed” is impossible client-side without server-minted turn ids (weft#640); rule (i) is precise across distinct hosted principals, which is the real multi-party case.
The mirror is saved after each discussion and on the error path, with
collect-and-continue per discussion — one wedged discussion (e.g. weft#638’s
no-HEAD AppendTurn) cannot abort the rest, and a mid-run failure never
leaves durable writes without their mapping.
Scope: discussions only; context/review share the same seam (not built).
resolve/reopen are not yet mirrored (turns only).
Functions§
- pull_
discussions - Fetch hosted discussions for the repository head and materialize any turns we do not already hold. Saves the mirror after each discussion and continues past a per-discussion failure.
- push_
discussions - Publish local symbol-anchored discussion turns we authored to the hosted
CollaborationService. Saves the mirror after each discussion and continues past a per-discussion failure (warn-and-skip).