-
Notifications
You must be signed in to change notification settings - Fork 1
Releases: thatSFguy/reticulum-specifications
Release list
v2026.08.31
Verified against RNS 1.5.2 / LXMF 1.1.1 — all 21 tools/verify_*.py pass, the citation gate is clean at 0 errors, and the new drift gate reports every citation still pointing at the text it cites.
Read this if you followed a source citation in v2026.08.29 or v2026.08.30. Both of those tags carried 52 citations pointing at code they did not mean. No normative claim was affected — every one of them is a wrong pointer, not a wrong statement — but a reader who followed one to check a constant or a branch would have landed somewhere unrelated and reasonably concluded the spec was wrong. That is fixed here, along with the gap that let it ship green.
The 52
Two separate defects, both introduced by the tooling used for the RNS 1.5.2 bump.
36 citations were never remapped at all. The one-off script used for the bump filtered its work list on path.startswith("RNS/"), so every citation written as `Transport.py:133` rather than `RNS/Transport.py:133` was skipped and left holding a 1.5.0 line number:
| citation | meant (1.5.0) | landed on (1.5.2) | corrected to |
|---|---|---|---|
Transport.py:133 |
PATH_REQUEST_GRACE = 0.4 |
blank line | :136 |
Transport.py:190 |
max_pr_tags = 16000 |
blackholed_identities = {} |
:195 |
Resource.py:481 |
if self.waiting_for_hmu: |
self.data = None |
:490 |
Resource.py:868 |
the windowed hashmap search | fast_rate_rounds / window_max |
:877 |
Each is remapped by content — the 1.5.0 line's text located in 1.5.2 — with 60 line references verified content-identical across the two installs and none mismatched. Resource.py:1101-1107 fell inside a changed hunk and was resolved by hand: the RESOURCE_RCL cancel block now cites Resource.py:1113, the line that actually carries the claim.
15 anchored ranges had stale start lines. check_anchored accepts an anchor found anywhere inside the cited range, so a range whose start has drifted but which still contains its anchor passes silently and --fix never renumbers it. RNS/Resource.py:564-674 sat nine lines stale with a green check.
Five anchors remain deliberately mid-range. All are in files byte-identical across the pin (Destination.py, LXMF/LXMessage.py), so they cannot be drift.
Why nothing caught it, and what now does
Both classes are invisible to check_citations.py by construction:
- a line-only citation is bounds-checked, so a stale number that still exists in the new version resolves happily — pointing at whatever now occupies it;
- an anchored range is satisfied by its anchor appearing anywhere inside it.
tools/check_citation_drift.py asks the question that catches both: does each citation still point at the same upstream text it pointed at before the bump? It resolves every citation twice — against the base branch at the old pin, and the working tree at the new one — and requires the source line to be identical. A citation correct before and after must satisfy that; one silently left behind cannot.
It is wired into resync-citations.yml ahead of the commit step, so a drifted citation blocks the auto-commit rather than being pushed, and no-ops when the pin is unchanged.
Replaying the real 1.5.0 → 1.5.2 bump with --fix run exactly as CI runs it:
--fix rewrote 46 drifted line number(s) ← reports success, exit 0
888 citation lines compared, 419 drifted ← the new gate, exit 1
Control, same documents and same pin on both sides: 849 compared, 0 drifted, 0 unpaired.
Citation coverage
Unchanged at 502 anchored (91%), 16 line-only, 6 continuation — this release moved pointers rather than adding anchors.
A note on what "verified" has meant here
v2026.08.29 said 215 references were "verified content-identical." That was true and it was not sufficient: the verification only covered the citations the buggy filter let through, and nobody checked the filter. The drift gate exists so that claim is made by a check that sees every citation, on every bump, rather than by the same script that did the work.
Assets 2
v2026.08.30
Verified against RNS 1.5.2 / LXMF 1.1.1 — all 21 tools/verify_*.py pass, and the citation gate is clean at 0 errors.
No errata. Nothing the spec claims changed in this release; what changed is where it points. Following the precedent set by the source-citation audit in v2026.08.28, citation corrections ship as an ordinary section rather than errata — no code built against v2026.08.29 needs a change.
Twelve citations were pointing at the wrong code
The pin bump in v2026.08.29 left every citation resolving and 78% of them anchored to the text they cite. Finishing the remaining 22% surfaced something the anchoring itself had been hiding: a dozen citations aimed at unrelated code. Every one had the same tell — the cited first line was blank, or a bare except / try: / return. Nothing points at whitespace on purpose.
| citation | pointed at | actually |
|---|---|---|
Transport.py:133 |
blank line | :136 PATH_REQUEST_GRACE = 0.4 |
Transport.py:1877-1882 |
discovery_path_requests code |
:1967-1968 where from_local_client is computed |
Transport.py:2670-2679 |
link-request proof error handler | :2735 the reverse_table.pop |
Transport.py:2764 |
def synthesize_tunnel |
:2826 [tunnel_id, interface, paths, expires] |
Transport.py:3417-3422 |
path-request error handler | :3495-3497 the local-client grace bypass |
Transport.py:4083-4086 |
except Exception as e: |
:4158-4161 the IDX_TT_* constants |
Transport.py::path_request |
the if not packet.unpack(): guard |
the discovery_path_requests assignment |
Resource.py:401/408 |
blank | :410/:417 the get_random_hash() prefix sites |
Resource.py:729/793 |
blank | :738/:802 the conclusion checks |
Resource.py:737 |
blank | :746 os.unlink(self.meta_storagepath) |
Resource.py:868-870 |
sets window_max from fast_rate_rounds |
:877-879 the receiver's windowed hashmap search |
Resource.py:1030-1071 |
the send loop's exception handler | :1039-1051 the if wants_more_hashmap: branch |
Two of these were made worse before they were made better: anchoring cements whatever a line currently points at, so Transport.py:2764 and ::path_request were briefly given anchors that were mechanically valid and semantically wrong. They surfaced because the anchor made the mismatch legible — a bare line number never reads as absurd, which is the argument for anchoring in one line.
Anchoring coverage: 78% → 91%
v2026.08.29 |
v2026.08.30 |
|
|---|---|---|
| anchored | 429 (78%) | 502 (91%) |
| line-only | 76 | 16 |
| continuation | 19 | 6 |
Three additions to tools/anchor_citations.py got there:
- Symbol-scoped anchors.
path::symbol→snippetresolves by searching only the symbol's body, so it carries no line number at all — an upstream move, reformat or length change all leave it valid. The tool had been rejecting these because it tested uniqueness across the whole file:if self.type == Destination.GROUP:appears in bothencryptanddecrypt, but exactly once in each. - Substring anchors.
find_snippetmatches by containment, so an anchor need not be a prefix — which the tool had been assuming. The distinguishing part of a long line often arrives late; a 186-charMDU = math.floor(...)needs only the fragment that differs from its siblings, not the whole statement. This required a guard the prefix search never did: unfiltered, substring search picks English out of log strings —'transport link','request. The'— unique today and meaningless after any reword. - Range-start normalisation. A range beginning on a blank line or bare
else:aimed its anchor at whitespace. The start advances to the first meaningful line still inside the range. Single-line citations are deliberately left alone: moving one changes what is cited, which is a judgement call rather than a normalisation.
What deliberately stays line-only
16 citations, and they are correct as they stand. 14 are lines whose text is byte-identical elsewhere in the same function — new_raw += struct.pack("!B", packet.hops ...) appears verbatim at Transport.py:2159 and :2659, both inside _inbound. No snippet can distinguish them, so the line number is the only thing that can. The other two are a bare return at Transport.py:2166, where §13's erratum is specifically about that return, and one line still over the 70-char anchor cap.
Also ruled out along the way: multi-line anchors do not work here. find_snippet's 6-line window fallback returns the earliest window containing the text, up to 5 lines before the true start, so --fix would renumber backwards. Worth knowing before anyone reaches for it.
Assets 2
v2026.08.29
Verified against RNS 1.5.2 / LXMF 1.1.1 — all 21 tools/verify_*.py pass, and the citation gate is clean at 0 errors.
No errata in this release. The last one carried six, three of them silent interop breaks. Nothing here invalidates code built against v2026.08.28: this is two new receiver rules in upstream, a large citation-integrity repair, and the automation that stops that repair from ever being needed by hand again.
Upstream pin: RNS 1.5.0 → 1.5.2 (#45)
Two receive-side bounds are new in 1.5.2. Both are drops, so a non-conformant sender sees silence rather than an error — which is the whole reason they are worth knowing about.
-
§2.2 —
Packet.unpackrejects a zero-length data field. The minimums are now strict: the shortest conformant frame is 20 bytes (HEADER_1) or 36 (HEADER_2). This is only partly redundant with interface framing.TCPInterfacehas long refused frames offrame_len <= HEADER_MINSIZE, butHEADER_MINSIZEis 19 — so that bound never caught a zero-data HEADER_2 frame, which is 35 bytes and clears it comfortably. The unpack-time rule closes that case on every interface, including the ones that frame without a length floor at all (KISS, RNode, serial). -
§4.5 — announce-specific ingress bound.
Transport.preprocess_inbounddrops anANNOUNCEwhose raw frame exceedsReticulum.MTU(500) and counts a protocol violation, beforevalidate_announceruns. This is not the general per-interface frame bound a few lines earlier, which measures againstinterface.HW_MTU— bitrate-derived, and reaching524288on a 1 Gbps link. Through RNS 1.5.0, an announce of up to half a megabyte arriving on a fast interface was carried all the way into signature validation before anything discarded it.
The matching emit bound turns out to be long-standing rather than new: Packet.pack raises above the packet's MTU, which for any non-LINK destination is Reticulum.MTU. A conformant sender could never produce an oversized announce; the new receive rule exists for the senders that aren't. §4.5 now states both halves, and tools/verify_ingress_bounds.py covers all three claims.
§2.2 also gained an upstream structural change. RNS 1.5.2 extracts the IFAC transform out of Transport.transmit into handle_outgoing_ifac (a bignum-XOR path) and handle_outgoing_ifac_legacy (the prior byte loop, verbatim); the receive side splits the same way into handle_ifac / handle_ifac_legacy. Wire bytes are unchanged — this is not an interop concern — but the section's prose and quoted block pointed into a function that no longer holds that code.
Link.py, Channel.py, Buffer.py and Destination.py are byte-identical between 1.5.0 and 1.5.2: no handshake or link-framing change.
Citation integrity: 215 references re-anchored (#45, #46)
The pin bump moved Transport.py by 75 lines and Resource.py by 12. check_citations.py --fix only re-anchors citations carrying a snippet anchor, and the 343 line-only ones were bounds-checked only — so a citation pointing at real but wrong code passed CI. The sharpest instance: MAX_RANDOM_BLOBS cited at RNS/Transport.py:159, which in 1.5.2 is MAX_RECEIPTS = 1024. A reader following it would have concluded the cap was 1024 rather than 64.
215 line references are corrected, each verified content-identical between the 1.5.0 and 1.5.2 sources. That includes a class the checker could not see at all: bare prose references (line 511-613, at line 754) carrying no citation syntax.
Three corrections go beyond renumbering:
| where | was | now |
|---|---|---|
flows/receive-announce.md |
quoted and RNS.Identity.validate_announce(packet) at Transport.py:1653 |
upstream reads and announce_valid, at :2176 — wrong before this bump, not drift from it |
| §12.3 relay rewrite | path_table[dest] |
path_entry |
| §2.2 IFAC | Transport.transmit |
handle_outgoing_ifac / _legacy |
Citations now repair themselves (#46)
tools/anchor_citations.py migrates a line-only citation into the anchored form, which carries the upstream text it points at:
before `RNS/Transport.py:162`
after `RNS/Transport.py:162` → `MAX_RANDOM_BLOBS = 64`
Anchored coverage 126 → 429 citations (23% → 78%); line-only 343 → 76.
The point is not tidiness. An anchor splits two cases that a bare line number collapses into one: upstream moved the code (repaired mechanically) versus upstream changed it (hard error, --fix refuses, a person reads it). The second is the spec asserting something upstream no longer says, and it is the only kind worth a human's attention.
Upstream bumps are now mostly automatic (#47)
.github/workflows/resync-citations.yml runs on a Dependabot or upstream-sync PR, repairs what is mechanical, and pushes it back to the bump branch. check_citations.py --fix also repairs stale Pinned against **RNS x.y.z** headers now — a version string that drifted is generated output for the same reason a line number is.
Measured on the same pin change, across this release:
| auto-repaired | left for a person | |
|---|---|---|
v2026.08.28 |
37 | 22 |
| after #46 | 177 | 22 |
v2026.08.29 |
191 | 8 |
Ordering is deliberate: a header reading Pinned against **RNS 1.5.2** claims the document was verified against that version, so the job runs the full verifier suite before committing anything. A bump that breaks a wire-format claim is never auto-stamped as verified.
Documentation (#48)
README's "Source citations" section documents the three citation forms, the rule they follow — a line number is generated output, never input — and which failure case reaches a person.
Known limitations
- Anchoring cements, it does not audit. Anchors were generated from the line numbers as they stood, so a citation already pointing at the wrong place now has a matching anchor for the wrong place. Three such were found in this release, and only because their anchors were absurd on their face — one cited an
exceptclause. 8 anchors are currently a bareRNS.log(...)or a comment, which is the signal to look for. Tracked intodo.md. - 87 citations remain line-only. 85 of them sit inside a named symbol and want
path::symbol, which carries no line number at all. Until that lands,--strictcannot be the CI gate.
Assets 2
v2026.08.28
Verified against RNS 1.5.0 / LXMF 1.1.1 — all 20 tools/verify_*.py pass, plus the new citation gate.
Six errata in this release, and three of them are silent interop breaks. If you built against v2026.08.19, read the errata section first — nothing below it will matter if your stamps and propagation retrieval are already failing.
⚠️ Errata — code built against v2026.08.19 needs changes
| Section | The spec said | Upstream actually does | What breaks |
|---|---|---|---|
| §5.7.3 | ticket stamp is SHA256(ticket || message_id)[:32] |
truncated_hash(...) — 16 bytes (LXMF/LXMessage.py:300) |
Every ticket-stamped message rejected, in both directions. No length error is logged; the message just reads as unstamped. |
| §5.8.3 | /get returns msgpack.packb([time.time(), [bodies]]) |
a flat list of bodies (LXMRouter.py:1523, :1555-1556) |
Body #1 decoded as a timestamp, body #2 as the message list. Fails on the first non-empty mailbox. Served bodies are also stamp-stripped (:1549) — don't slice 32 bytes off again. |
| §5.6.1 | canonical msgpack encoding is a SHOULD | a MUST whenever the message carries a §5.7 stamp | The signature covers the 4-element payload (:367) while the wire carries 5 (:381), so the receiver re-encodes (:758). One wide integer envelope voids every stamped message: signature_validated = False, and message_id diverges with it. |
| §12.6 | tunnel entry [now, expires, paths, ...] |
[tunnel_id, interface, paths, expires] — only index 2 was right |
Transport nodes read the interface handle as an expiry. Tunnels never expire, or expire instantly. Leaf clients unaffected. |
| §12.3.2 / §16.1 / §7.2.2 | MAX_RANDOM_BLOBS = 32; max_pr_tags = 32000 |
64; 16000 as of RNS 1.5.0 |
Announce blobs evicted twice as fast as upstream — mild rebroadcast amplification on busy destinations. |
| §9.10 / §4.1 | the microReticulum random_hash deviation is live |
fixed upstream (Destination.cpp:272-280) |
Receiver-side workarounds carried for a bug that no longer exists. §9.10 is now a historical entry keeping only the clock-hygiene advice. |
The first three were found by a clean-room Go implementation and are the ones that cost real days: each fails silently, none reproduces Python↔Python, and §5.6.1 additionally fails only after a Resource transfer completes and is proved — so it reads as a transport fault.
Upstream pin: RNS 1.4.2 → 1.5.0 (#34)
No wire-format change, but RNS 1.5.0 refactors Transport heavily (+379 lines) and splits Transport.inbound into inbound / preprocess_inbound / inbound_job / _inbound around a prioritized ingress queue. 142 of 360 line references were rewritten. Behaviour newly documented:
- §13.1.1 (new) — the prioritized ingress queue:
InboundQueues, the fourTC_*traffic classes and per-class depths, and which stage runs on which thread. - §12.5.2 — relays now drop link packets arriving before the link is validated (
IDX_LT_VALIDATEDgate); the branch'sreturnis no longer commented out, so link-addressed packets stop falling through to the announce / LRPROOF / reverse-table branches. - §2.2 —
Packet.unpacklength-checksdestination_hashandtransport_idinstead of accepting a short slice.
Full source-citation audit — 39 corrections (#35)
Every one of SPEC.md's 366 citations rendered next to the code it points at and read, rather than re-anchored mechanically. Surfaced the §12.6 and §9.10 errata above. Verified against rns==1.5.0 / lxmf==1.1.1 / nomadnet==1.2.8, plus RNode_Firmware@d39339f, microReticulum@40fa628 and reticulum-lora-repeater@4adb57b for the non-pip sources.
Resource MTU sizing, receive-side bounds, stamp-cost range (#36) — closes #31, #32, #33
- §10.2 step 6 / §10.4 — which quantities follow the negotiated MTU. Only the instance-level ones (
link.mtu,link.mdu,resource.sdu) move with §6.6 negotiation;HASHMAP_MAX_LENandCOLLISION_GUARD_SIZEare import-time constants offReticulum.MTUand MUST NOT be recomputed from a negotiated MTU. Each side computes its part count asceil(size/sdu)from its ownsdu, so a disagreement breaks the count, not just the per-part bound. - §10.4 / §16.9 — bound cumulative received bytes against
tand abort on the part that overruns; if also bounding per-part size, derive it from the link'ssdu, never the fixedResource.SDU. - §5.7.4 —
stamp_costisNoneor1..254;< 1means "off", not malformed;>= 255is refused by the setter, and the parse side does not re-validate.
NomadNet pin split out of the verifier environment (#37)
requirements-docs.txt now carries nomadnet==1.2.8 separately; CI does not install it and no verifier imports it. The §11.6.3 link-target table's bare-number Browser.py refs were re-anchored (they matched none of the audit's citation patterns and had gone unchecked).
Three clean-room findings closed (#41) — closes #38, #39, #40
Beyond the §5.7.3 and §5.8.3 errata above, §10.11 gained a receiver-side subsection (§10.11.1) — previously the sender was fully specified and the receiver was not:
- The reassembly store is file-backed and keyed on the advertised
oalone — per-node, not per-link. Two transfers presenting the sameoon different links share one file. - Segments are concatenated in arrival order, never placed by
i;idecides only which segment is final. Out-of-order segments each pass their own integrity check, so misordering is silent. - Retention is a 24 h idle deadline refreshed by every append, not a transfer budget. A deadline measured from the first segment is unmeetable — upstream imposes no wall-clock bound on a single segment.
lis unbounded on the wire, and so is the count of concurrent partial assemblies, all held for an unauthenticated peer.
New: CI citation gate (#41)
tools/check_citations.py resolves every source citation in SPEC.md and flows/ against the pinned install, and runs as the first CI step.
The rule: a line number is generated output, never input. A citation carries a verbatim upstream snippet as its anchor:
(`LXMF/LXMessage.py:277` → `self.stamp == RNS.Identity.truncated_hash(ticket+self.message_id)`)
--fix rewrites drifted line numbers, so a version bump becomes bump the pin, run --fix, review the diff. It refuses to touch a snippet that exists nowhere upstream — that is a behaviour change, not drift — and exits non-zero so a successful fix run cannot mask one. --selftest proves the checker still catches each failure mode, including the exact §5.7.3 text that motivated it.
Pin declarations are gated too: a document whose Pinned against **RNS x** header disagrees with tools/requirements.txt fails CI.
All 12 flow documents re-anchored (#41)
The gate's first run found 12 of 13 flow docs still declaring the pre-1.5.0 pin. All are now re-read against it; flows/ goes from 0 to 111 anchored citations and tools/citations-exempt.txt is empty. The drift was not cosmetic — most citations pointed at real code unrelated to the claim beside them. forward-announce.md §2 also presented the rebroadcast gate as one three-term boolean when upstream nests the rate check inside it.
§5.6.1 canonical msgpack (#42) — closes #43
Covered in the errata table. §5.6 additionally now records that upstream does not try both verification variants — it selects one, on stamp presence (LXMF/LXMessage.py:755). Trying both stays right for a receiver, but a sender has no raw-bytes fallback to rely on.
Verifiers: 16 → 20
| New | Covers |
|---|---|
verify_resource_sizing.py |
§10.2 step 6, §10.4, §5.7.4 — MTU-derived vs fixed constants, absent receive-time part checks, stamp_cost range |
verify_propagation_get.py |
§5.8.3 — flat-list response, stamp stripping, transient_id round-trip, purge round |
verify_resource_segment_store.py |
§10.11.1 — o-keyed store, arrival-order concatenation, retention constants |
verify_canonical_msgpack.py |
§5.6, §5.6.1 — MUST when stamped / SHOULD when not, including the unstamped control case |
verify_stamps.py gained the ticket-stamp length assertions. Every new verifier is mutation-tested: reverting the upstream behaviour it checks turns it red.
Verification status
0 🔮 SPECULATION callouts remain. Three ⚠️ UNVERIFIED callouts stand, all marked and none touching this release's changes: app-layer FIELD_* byte shapes (§5.9), PN_META_* value types (§5.9), and the unpopulated test-vector categories (§11).
Assets 2
v2026.08.19
Verified against RNS 1.4.2 / LXMF 1.1.1 — all 16 tools/verify_*.py pass.
Pin bump and spec-wide citation refresh (#28)
tools/requirements.txtmoves torns==1.4.2/lxmf==1.1.1. Neither upstream release touches a wire format; §7.2.3 and §12.6.1 note the one documented behavior change — RNS 1.4.2 skips offline interfaces in the recursive path-request fan-out.- Every
File.py:linecitation inSPEC.md,playbook.md,agent.md,todo.md, andflows/was re-anchored on the symbol its surrounding prose cites and repointed at the new pins. ManyTransport.pypointers were RNS 1.2.4-era and off by 30–210 lines; two pointed past end-of-file.
Errata: §5.8.2 propagation-node error values (#28)
The error table carried three wrong wire values from the section's introduction:
ERROR_THROTTLEDis0xf6, not0xf2— upstream leaves0xf2unallocated.ERROR_NOT_FOUNDis0xfd, not0xf5.ERROR_INVALID_STAMP=0xf5was missing entirely.
The table now lists all eight constants and warns that the values are not contiguous. Recorded in the README errata under 2026年08月19日: a client built against the old table retries immediately instead of backing off on a genuine throttle, and mis-reads 0xf5.
§5.8.1 two-destination split and path-string corrections (#29)
- The request handlers span two destinations, not one:
/offerand/getare onlxmf.propagation(ALLOW_ALL), while the control paths live on a separatelxmf.propagation.controldestination — name_hash4576672b3f55049c2e46— whose allow-list holds only the node operator's own identity hash. - The control-path strings are
/pn/get/statsand/pn/peer/sync, not the bare/statsand/syncthe table gave, and the previously undocumented/pn/peer/unpeerhandler is now tabled with the others. - The control-path argument shape is documented: a bare 16-byte peer destination hash (not a msgpack wrapper), with
ERROR_INVALID_DATA/ERROR_NOT_FOUNDsemantics per handler.
No errata entry for these: nothing built against the old text could ever have worked — the documented destination and paths were unreachable — so there is no deployed implementer code to warn.
New verifier: tools/verify_lxmf_peer_constants.py (#29)
Asserts the eight LXMPeer.ERROR_* response bytes, the documented 0xf2 gap, and the five request-path strings across both destinations, plus an audit that fails CI if upstream allocates an un-enumerated ERROR_* or *_PATH constant — the check that would have caught the §5.8.2 errata above.
De-flaked verify_stamps.py (#29)
The §5.7.3 wrong-ticket check failed roughly 1 CI run in 18: the randomly derived stamp cleared the 4-bit PoW target by chance (~6%), letting the fall-through PoW path rescue a stamp the ticket shortcut had correctly rejected. The stamp is now pinned to miss the target, making the rejection deterministic, and the check additionally asserts the shortcut itself didn't fire (stamp_value != COST_TICKET) — the actual §5.7.3 claim.
Assets 2
v2026.07.28
Verified against RNS 1.4.1 / LXMF 1.1.0 — all 15 tools/verify_*.py scripts pass, including the new §7.1 retry-constants check.
§7.1 retry-loop timing constants pinned (#26)
- §7.1 — the LXMF outbound retry-loop timing constants are now stated in the spec with an upstream citation (
LXMF/LXMRouter.py:30-34):PATH_REQUEST_WAIT = 7 s,DELIVERY_RETRY_WAIT = 10 s(constant across attempts — no exponential backoff),MAX_DELIVERY_ATTEMPTS = 5,MAX_PATHLESS_TRIES = 1. Cross-referenced to the full per-message retry state machine inflows/lxmf-outbound-retry.md, which already documented the correct values. - §7.1 — fixed two line citations that predated the 1.1.0 sync: the
handle_outboundpath-request preamble is at~line 1777(was~1672) and the retry branch atLXMRouter.py:2736+(was2568+). tools/verify_path_request.pygainsverify_retry_constants(), asserting all four values against the liveLXMRouterclass so upstream retry-timing drift surfaces in CI.
Also in this release
- New
proposals/directory with a draft upstream LXMF group-messaging proposal (sender keys over the existing pairwise E2E channel + a multi-recipient propagation-node extension). Non-normative — a discussion draft addressed to upstream, not spec content.
Assets 2
v2026.07.26
Verified against RNS 1.4.1 / LXMF 1.1.0 — all 16 checks in the 15 tools/verify_*.py scripts pass.
Upstream security note: RNS 1.3.9 (2026年07月19日) fixes a critical, undisclosed vulnerability in
rnsh. It does not affect the wire formats this spec covers, but if you operaternsh, update it immediately.
Spec sync (RNS 1.3.5 → 1.4.1, LXMF 1.0.1 → 1.1.0)
- §2.4 — hop count is bounded: since RNS 1.3.8 receivers drop packets with
hops ≥ 128(PATHFINDER_M) as malformed. Newverify_packet_header.pycheck locks in the0..127bound. - §2.4 — documented the opt-in
local_hops_deltaoriginator-privacy option (RNS ≥ 1.3.7): originated packets carry a random hops value in[2, 7]instead of0, and originated HEADER_1 announces are rewritten to HEADER_2 with the node's owntransport_id. Receivers MUST NOT infer originator-adjacency or exact path length from hop counts. - §10.3 — since RNS 1.3.9,
ResourceAdvertisement.unpackhard-rejects ADVs witht > 3 ×ばつ MAX_EFFICIENT_SIZE(~3 MiB) at parse time. - §10.7 — HMU hardening (RNS 1.3.9): out-of-turn hashmap updates are ignored; empty ones cancel the transfer on both sides.
- §10.9 —
RESOURCE_RCLis now also sent on receiver-side abort (RNS 1.3.9), not just on advertisement reject. - LXMF 1.0.1 → 1.1.0 carries no wire-format changes (propagation-node sync behavior only); citations refreshed to 1.1.0 line numbers.
- Source-cited line numbers re-checked in all touched sections; §10 source map refreshed to RNS 1.4.1; pins bumped in
tools/requirements.txt.
Verified against PyPI wheels — signed 1.4.1 artifacts are distributed via rngit only, which this verification environment cannot fetch.
Assets 2
v2026.06.19
Verified against RNS 1.3.5 / LXMF 1.0.1 — all 15 tools/verify_*.py pass.
Spec sync (#16)
- §4.3 — announce
app_datais now a 3-element array[display_name, stamp_cost, [SF_COMPRESSION]]; receivers accept both 2- and 3-element forms. - §5.9 — new upstream fields:
FIELD_REPLY_TO(0x30),FIELD_REPLY_QUOTE(0x31),FIELD_REACTION(0x40),FIELD_COMMENT(0x41),FIELD_CONTINUATION(0x42). - Reactions/replies follow upstream: emit
0x40and0x30/0x31. - CI — Dependabot +
verifyworkflow added to catch upstream drift automatically.
Spec authority (#17)
agent.md§0 prime directive — the spec is authoritative; every claim must cite an admissible upstream source (markqvist/Reticulum,markqvist/LXMF, ormarkqvist/Sidebandfor app-layer shapes LXMF doesn't pin). No other client app — third-party or our own — is admissible. Apps conform to the spec, not the reverse.- Removed third-party app references (former §5.9.12 and related notes); the spec documents only upstream forms.