Specification §4

Discovery and membership.

Membership is derived by observation, never accepted on assertion. 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.

1. Model

Croakus: §7, §13, §14

Each node derives membership by observation. No node accepts a topology asserted by another, and no controller distributes one (§2d). What a walk produces is evidence for §5 to plan from and names for the node's own hosts file; what fires a walk is §4a.

The walk is bounded by depth. MAX_DEPTH = 2 (discovery/discovery.py:41): a node descends at most two levels from its own segment, so the cost of a walk is bounded by topology rather than by the size of the mesh.

2. What is not walked

A candidate address MUST be skipped where any of the following holds. These are not optimisations; each removes a class of false result.

SkipBecause
Not a 10.x addressOutside the mesh's address space
The chorus on-link rangeA synthetic infrastructure plane, not node identities
The reserved transit rangePoint-to-point carrier blocks (§7); no node lives there
Any local addressThat is this node
The .2 of the local subnetA probe alias, not an identity
An already-attempted pathSee §4.3
An address proven non-FrogNet this sessionSee §4.4

3. Deduplication is per path, not per destination [HOP_VIA_V1]

A walk MUST deduplicate on the tuple of device, destination and candidate next hop — not on the destination alone. The same node may be reachable more than one way: a direct one-hop seed and a relayed path learned from a peer's host list.

If deduplication keyed on the destination, whichever path was attempted first would suppress the others, so a failing or looping relay path could poison a working direct path to the same node. Each distinct next-hop attempt MUST be allowed to stand on its own evidence.

4. The negative cache

A host that definitively proves not to be a FrogNet node is remembered for the rest of the session and MUST NOT be re-probed within it. Re-probing an ordinary LAN box every cycle burns a full probe budget and feeds a bootstrap storm.

4.1 What may mark, and what MUST NOT

SignalMarks?Because
Connection refusedYesSomething answered and there is no service
No route / network unreachableYesDefinitive absence
On-segment probe with no echoYesDefinitive absence
TimeoutNEVERA real FrogNet node that is merely slow or briefly unreachable would be silenced until the next merge
Connection resetNEVERA reset means something was there and dropped it, which a real peer can do under load. Not proof of absence.

The cost of getting this wrong is not theoretical. On 2026-08-08 a single legitimate connection-refused, during a two-second restart of the elected database host, marked that host as non-FrogNet. It was never dialled again: every sensor write failed, the capability tuple never landed, and the role floated.

4.2 The cache MUST be flushed, and the flush MUST have a caller [FLUSH_HAS_A_CALLER_V1]

A session is the interval between merges. The cache MUST be cleared at the top of every merge, so a host that joins FrogNet later gets exactly one fresh probe on the next merge.

The requirement is that the flush actually runs. The documented flush stage named a command whose entire body cleared an unrelated database table; nothing in the tree ever called the cache's own flush, so a mark was permanent for the life of the process. The comment describing the behaviour was false from the day it was written, and the false comment is what hid the fault for weeks.

Two consequences for a conforming implementation. A flush stage MUST be named for what it flushes. And an implementation MUST NOT rely on a documented flush without a caller in the tree — unreachable clearing state and never-invoked clearing state produce the same permanent mark by opposite routes.

4.3 The cache is read where it is written [NOT_FROGNET_SKIP_V1]

The walk MUST read the same negative cache it writes. Without the read the cache is write-only at that site, and every convergence pass within one merge re-pays the full probe budget on the same stray LAN clients. The cache MUST protect node identity addresses, so a real FrogNet node that is merely down is never skipped.

4.4 Test isolation [SENTINEL_DIR_HONOURED_V1]

The cache is shared between processes through a sentinel file and each process reloads only when the file's modification time changes, so the membership test is cheap on the hot path. The sentinel directory MUST be resolved per call and MUST honour the environment override, not pinned at import.

Pinning it made oracles read and write the live node's real cache: tests failed on the machine's history rather than on the code, and a test run could write marks back into production state.

5. Proving a link

Reachability MUST be proven by the same probe for every route kind, and that probe is the daemon's own liveness exchange — not the HTTP echo [PINGPONG_GATE_V1].

The reason is an asymmetry in what each can answer. The daemon binds all addresses, so it answers on whichever LAN address a probe arrives at. The proxy echo can answer only on the node's identity address. Gating an on-segment leg on the echo therefore false-negatives a live on-segment node, and skips the loop check as well.

The liveness exchange gates reachability and detects a path that loops back through this node. The echo is demoted to a post-gate identity fetch: it establishes who the peer is, never whether it is there.

An ambiguous negative MUST be retried before it is believed (§6.5); a decisive verdict MUST NOT be. A timeout MUST NOT mark the peer (§4.4.1).

6. Output

A walk produces two things and no more: observations, which are evidence for the planner (§5.2), and names, written into this node's own hosts file. A discovered host needs a name there and not merely a route [DESCEND_NAME_V1]; a route without a name is reachable and unaddressable.

A walk MUST NOT install a route directly. Planning and commitment are §5, and separating them is what makes the plan a pure function of the evidence.

7. References

Specification §2d, §3, §4a (Triggering), §5, §6, §8. Source: discovery/discovery.py, discovery/descend.py, core/not_frognet.py. Build manual: Croakus §7, §13, §14, Appendix B (The Echo Probe), Appendix H (The Liveness Gate).