Specification §6
Formation and splitting are the same machinery. The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.
FrogNet Specification — Draft 0.9, revision 2026-08-29. Cite a conformance claim against this revision, not against a section number alone.
A pond is not declared and is not configured. It is the set of nodes that can currently reach each other, sharing one membership, one topology view, and one authoritative store — recomputed from observation every time observation changes. An implementation MUST NOT hold a pond definition that survives contradiction by the current walk.
The same machinery handles formation and splitting, and MUST NOT distinguish them. A node does not decide whether it is joining or losing; it recomputes what it can see. A split is a walk that returned a smaller world.
Nodes that come to share a reachable segment form a pond. Each node probes, proves the link in both directions, and writes the proven peer into its own hosts file; the peer does the mirror image. Within a small number of walks each node's /etc/hosts carries the other and the pond exists. Nothing was configured and no node was told.
A node MUST NOT enter a peer into its membership on an unproven observation. Reachability MUST be proven in both directions before a peer is admitted; a one-way probe response is not a link.
WORKED EXAMPLE — forming
HomeBase runs alone.
ShopBox is plugged into the switch.
HomeBase, next walk probe wire -> answer from 10.84.84.1
prove alive -> write to /etc/hosts
ShopBox, next walk mirror image
two-node pond. nobody configured anything.
Verified where the source states them. FROGNET_ALIVE_9009_TRIES defaults to 3 and is read at call time [ALIVE_9009_RETRY_V1]: only an ambiguous negative — None or alive=False — is re-asked. Decisive verdicts (a float RTT, LOOP, REFUSED, alive=True) MUST NEVER be retried. The ping-pong is acknowledged to false-negative on high-RTT and tunnel paths, and one miss at the walk dropped measured candidates to vouch-only and flapped the single-winner roles pass to pass. A truly dead path still fails after all tries: the gate is unchanged and the final negative is still believed. This is not a fallback — nothing is hidden.
A timeout MUST NOT mark a peer as not-FrogNet: a slow real node is not an absent one. Default-route resolution uses a 15 s timeout; the control-plane name read uses 3.0 s.
discovery/discovery.py. They are properties of the trigger cadence (§4a) rather than of a timer, and are to be established from an instrumented run rather than inferred.A split is a defined state, not an error. When a path is lost, each side recomputes from what it can still see and each becomes a smaller pond that keeps running. There MUST be no error, no hang, and no degraded mode: an implementation MUST NOT treat loss of membership as a failure condition of the node.
Each fragment MUST hold an election where an elected role was lost (§8). When the database host moves, the memory moves with it: FrogNet Memory is not migrated to the new host, not replicated to it, and not reconciled against the old one. It lives on whichever machine currently holds the role, and what is in memory is whatever is on that machine.
WORKED EXAMPLE — the database host follows the split
BEFORE databasehost.frognet = 10.80.80.1 (HomeBase, most RAM)
link drops — two islands:
island A { HomeBase 10.80.80.1 } -> databasehost.frognet = HomeBase
island B { ShopBox 10.84.84.1 } -> databasehost.frognet = ShopBox
link returns — one island:
most capable overall -> databasehost.frognet = HomeBase
Applications on both sides keep asking for the same name and keep getting an answer. Neither side pauses.
On rejoin, fragments MUST re-run discovery and election. The surviving store is authoritative and there is no merge of divergent tuples (§2c). No log is replayed, no histories are reconciled, and no negotiation determines whose value wins. Values written on the far side age out of readers' freshness windows and cease to be observable; new writes land on the new host because that is where the name now points.
An implementation MUST NOT perform automatic reconciliation. Reconciliation would require the platform to invent an answer about data whose meaning it does not hold, and an invented answer that looks correct is the failure this system is built to avoid.
Detection of a moved store is local. Nothing is pushed, because the machine that would have announced the change may be the machine that left. An implementation MUST NOT depend on being told.
| Mechanism | How it detects | Suits |
|---|---|---|
| Write-then-read | A process keeps one entry of its own — its own name, its own timestamp — writes it, and reads it back next cycle. Same value returned means the same memory. Missing, or an older timestamp than it last wrote, means the memory changed underneath it. | Any process with a cycle |
| Generation counter | A counter maintained by the store itself moves on every change from any writer. | Processes needing any-change detection |
| Name resolution | Resolve databasehost_control.frognet each tick and compare to the previous answer. Different means the role moved; same means do nothing. | The daemon, proxy, and bundle shells |
A process that has just started has nothing to compare against and correctly detects nothing, because it holds no stale state. First observation MUST be recorded without firing a reconcile.
hostResetA newly elected host's store starts empty. It is repopulated not by copying — the old store may be on the unreachable side of the break — but by every participant re-asserting its own current truth into the new space.
WORKED EXAMPLE — island A repopulates its own store
split — ShopBox is island A's database host. its store is empty.
the merge fires hostReset() everywhere:
HomeBase.databasehost.advertise() -> capability tuple -> new store
ShopBox.databasehost.advertise() -> capability tuple -> new store
HomeBase.mediahost.advertise() -> capability tuple -> new store
within a beat, island A knows who can host what
— rebuilt from each node re-asserting itself, NOT copied.
hostReset MUST be trigger-agnostic: it takes no argument indicating why it was called and MUST behave identically on boot, merge, split, float, or manual invocation. It has one behaviour — publish what this node currently is, to wherever the coordination plane currently lives — which is what makes it safe to call from anywhere, at any time, as often as desired. A reconcile that varied by trigger would require every caller to know its own situation, which is a larger promise than any caller can keep.
hostReset is a protocol, not a method on a class hierarchy: any object in any process MAY answer to it, and the watcher fans it across whatever modules the process holds without knowing what they are. A module participates by implementing one no-argument method. Nothing registers it and no interface is declared, and a module that holds nothing legitimately implements nothing and is skipped.
The reconcile is two phases and an implementation MUST NOT interleave them:
for each module: reset world -> ensure MY consistency -> write MY memory
then ONCE: read the SHARED memory by tuple vector
(re-establish relationships)
Every module MUST re-assert its own memory before any shared read happens, and the shared read MUST happen once, not once per module. Relationships are then established over a store that every module has already repopulated, rather than over a half-reasserted one. The dispatcher owns the shared read; the per-module work is each module's own.
One module MUST NOT sink the reconcile. A failure in one module is recorded against that module and the pass continues. A reconcile that aborts on the first error leaves every module after it un-reasserted, which is a worse state than the one that triggered it.
The trigger is the existing change detector and MUST NOT be a new message: on-mesh, the merge already knows the database host line moved; off-mesh, a name-resolution delta. Either calls the same entry point (§6.5).
Two consequences an implementation MUST design around. First, writes made by hostReset land on a store that has never seen them, so the first exchange of each shape crosses FULL — the new host holds no template to diff against — and only then collapses to DIFF and SAME (§11). The media handler nulls its control reference deliberately so that the next frame re-FULLs the control plane against the new host. Second, hostReset re-establishes current state, not a log; it is not a historical recorder and MUST NOT be used as one.
Convergence is complete when every member's derived topology is stable against current observations. The term is reserved for this meaning. An implementation MUST NOT describe memory behaviour as convergence: the store does not converge (§2c).
An implementation claiming conformance to this section MUST NOT provide, and MUST NOT be described as providing: a merge procedure, a replay log, an anti-entropy pass, a background sync, data ferrying between fragments, a quorum, or a notification that the topology changed. Detection is local, re-establishment is by re-assertion, and what to do about it is the application's, because only the application knows what its own state means. FrogNet reports that the ground moved and stops.
Specification §4 (Discovery), §8 (Election), §9 (Memory), §13 (Failure semantics). Build manual: Croakus §7, §18, §29, §30.