Repository navigation
Description
Summary
Give bidirectional replication a first-class surface: two runtimes declare a replica pair, each stages transmits for the operations the other mirrors, and each ingests the other's envelopes. The transmit family (#47, #48, shipped in #49 with the engine route) provides the mechanism; this proposal names the missing declaration. This is the feature that earns the "replica" name.
Why this is deferred
Wiring register_transmit at both ends of a pair already works today, but only for one direction at a time. Point both directions at each other and the pair loops: an ingested operation applies through an internal message, the actor's operation stages a transmit unconditionally, and the mirrored apply transmits straight back. Echo suppression is the design problem, and it deserves its own review rather than a rider on #49.
Design questions
- Echo suppression. A plausible default:
Actor#transmitstages nothing when the current turn applies a message whose idempotency key carries thetransmit:prefix, so a replayed operation never re-transmits. A multi-hop topology (A → B → C) then needs an explicit forwarding override, because suppression at B would stop the chain. - Declaration API. What form earns the name: a class-level
replicate_toon the actor, or a runtime-level pairing that wiresregister_transmit, the engine route policy, and the resolver in one declaration? - Identity. A replica pair needs a bijective actor type mapping. The current
resolve_actor_type/transmission_actor_type_resolverare one-way; a pair declaration should derive both directions from one mapping. - Conflict posture. Per-actor ordered replay gives last-writer-wins in arrival order. Document single-writer-per-actor as the supported convention first; divergence detection (periodic state digest exchange) is a separate follow-up.
- Wire contract. Loop prevention across more than two runtimes likely needs an
originfield in the envelope. That is a contract revision, so it lands incompatibility/transmit-envelopes.jsonand both runtimes together.
Plan
- Prototype the echo suppression rule behind the existing transmit tests, and prove a two-runtime pair converges instead of looping.
- Settle the declaration API against a real pair (the Rails QA app and a Node runtime already exercise both directions one way at a time).
- Revise the fixture file if
originlands, with consuming tests in both repositories. - Documentation, roadmap, parity ledger, both changelogs, both suites green.
References
#47, #48, the shipped transmit family and engine route in #49, and solid-objects-js#17 / solid-objects-js#18.
Activity
sharmis-tp commentedon Oct 27, 2026
You can trace the loop in the code.
Transmission.receiveenqueues the mirrored operation with an idempotency key ofIDEMPOTENCY_PREFIXplus theeffectId. The handler then runs, and the dispatcher inActor#transmitcallsemit(Transmission::EFFECT_NAME, ...)unconditionally, so the envelope goes straight back.I'd take only step 1 of the plan as a scoped first PR: echo suppression keyed on the
transmit:prefix, with no declaration API and no envelope change. I'd start with a failing test in the style oftransmit_staging_test.rb, where two actor types transmit to each other and the ingesting side must stage nothing. Questions 2–5 stay with you.One thing to settle first: should suppression live in
Actor#transmitor at staging time? Theincrement_mirrorfixture callsemit "solid-objects.transmit"directly, so a check insidetransmitwould never see it.Could you assign me? The PR would be up within 1–2 days.