RFC 0031 — Bus-session anchors are child vertices under their door: one walk and one gate reach a session, send-through is directed¶
Note
Status: accepted. This page is an accepted change proposal, kept as the record of why the specification reads as it does. RFCs are proposals and history, not the standard. The normative specification is Protocol v1 and the annexes its §3 incorporates; where an RFC and the specification differ, the specification wins. All RFCs, with their status, are listed in the ADR and RFC index.
Field |
Value |
|---|---|
RFC |
0031 |
Title |
Bus-session anchors are child vertices under their door: one walk and one gate reach a session, send-through is directed |
Status |
accepted (2026-10-07; proposed the same day), maintainer-approved with every §14 recommendation; comment window waived by default and not invoked. The direction of §§5–6 was ruled on 2026-10-07 in #1938 (§”Addendum: stages 5 and 6”, “all rec”). This document turns that ruling into normative text, and §14 lists the choices the ruling left open, each with a recommendation. |
Author(s) |
AvatarSD (maintainer), with AI drafting |
Created |
2026-10-07 |
Comment window |
Waived by default while the project is solo-maintained (GOVERNANCE.md §”Errata, amendments, and the comment window”). Invoke it explicitly if outside input is wanted. At drafting, |
Instrument |
Amendment. It lifts a normative refusal (a PAIR MUST NOT egress through a shared mount, RFC-0029 §10 and v1 §3), narrows a normative MUST (RFC-0020 §3: “MUST NOT resolve the residual against its local graph”), and changes which vertex a frame addressed below a bus door is authorized at. GOVERNANCE.md reserves each of these for an amendment. No backward compatibility is owed: the project takes none (the RFC-0028 ruling, restated as RFC-0029 ruling 10). |
Tracking issue |
|
Target spec version |
v1 itself. |
Scope |
Stage 5 of #1938. The implementation ticket #1948 is blocked on this RFC and on stages 1 and 2 (#1939, #1940). Sequenced after RFC-0030, because both amend RFC-0029. |
Amends |
RFC-0029 §4.2 (the shared-mount example), §6 step 3 (the shared-mount arm), §8.1 (the session-anchor guard) and §10 in full, which this RFC replaces, with §13.2 S8; RFC-0020 §3 (the local-graph MUST, narrowed). |
Confirms, not amends |
ADR-0044 and its 2026-08-13 amendment (an accepted session MAY be an ordinary vertex; announce-census peers create none); ADR-0073 §2 ( |
Discharges |
RFC-0030 §9.1’s forward reference: “the session is the accepted session’s anchor where one exists (RFC-0029 §10, stage 5 of #1938)”. |
Numbering note. Numbering gaps and why they are not reused are recorded in the ADR and RFC index.
1. Summary¶
A WebSocket or TCP listener built with peer_named = true gives each accepted session a session
anchor: a vertex with an owner-issued (index, generation) pair, retired when the session ends,
so that a route naming a dead session fails the generation check (ADR-0044 §Amendment, #1223).
Today that anchor hangs off a private root that no walk can reach. It is keyed by a string that
no path can spell (:<mount>/<peer>), the router parses that string back to find the session, and
a frame that names the session by NAME is authorized at a different vertex from a frame that names
it by PAIR. A PAIR cannot egress through it at all: RFC-0029 §10 refuses a PAIR to a shared mount
until this stage lands, so every chain across a bus stays NAMEs.
This RFC makes the anchor an ordinary child of its bus connection vertex (its door, RFC-0030 §4):
The anchor is a child of the door, named by the peer name the link already uses (
p<slot>, ADR-0073 §2).<door>/<peer>and the anchor’s PAIR reach the same vertex through the same walk: stage 1’s descent to the deepest connection vertex finds it with no new branch.It is the one gate. The anchor is the session’s connection vertex. A frame through the session is authorized once, at the anchor, whose ACL is the door’s ACL as the door evaluates it, plus the anchor’s own
:aclif one was written (§6).It lives for its session. Every new session revives it with a new generation, including a returning peer that gets the same name, so a cached pair to a dead session is refused and the path object falls back to the name (RFC-0030 §5.2).
Send-through is directed. A walk that reaches an anchor with a non-empty remainder sends it to exactly that session, over the link’s directed per-peer endpoint. It never broadcasts.
Announce-census peers stay vertex-less. On CAN (and any other census bus) the walk gates at the bus door and the link’s own peer list resolves the remainder. Census peers get no per-peer ACL, and the documentation says so.
The private anchor root, the :<mount>/<peer> key and its parser are deleted. A FLAT listener
(peer_named = false, the default) stays exactly as free as it is today, and every graph saves one
vertex_t (§7).
2. Motivation¶
2.1 One session, two vertices, two names¶
A peer_named session is visible in two places that do not know about each other:
the link’s peer list (
bus_link_t::peer_link,bus_link_t::enumerate_peers), which the NAME spelling<door>/<peer>/…reaches through the router’s mount descent andchild_registry_t::resolve_peer(ADR-0061);the anchor (
graph_t::register_session_anchor,core/src/graph.cpp), which hangs off the private rootgraph_t::anchor_root(core/include/libtracer/graph.hpp) and is keyed by the unspellable record:<mount>/<peer>(fwd_router_t::session_anchor_id,core/src/fwd_router.cpp).
The graph’s design note for the anchor says so on purpose: an anchor “exists to be REFERENCED and
never to be ADDRESSED”, so that find, every listing and RFC-0020 §3’s premise stay unchanged. That
was the right call for #1223 step 2, which needed a generation before it needed an address. Under the
2026-10-07 model (a connection vertex is a door, and there is one walk) it leaves a session as the
one thing on a node that a walk cannot reach.
2.2 Two spellings, two gates¶
The NAME spelling <door>/<peer>/… is authorized at the door. The PAIR spelling, on the delivery
arm that already accepts an anchor (the bound-delivery path in fwd_router_t, #1223 step 3), is
authorized twice: once at the anchor, which “lives OUTSIDE the path tree … and cannot see the
mount’s” ACL, and once more at the mount’s connection vertex, looked up by name. RFC-0029 §6.4 asks
for a verdict that is a function of the vertex, the caller and the right alone, reached by one
allows site. Two vertices for one session cannot satisfy that by construction.
2.3 No send-through, so the chain stops at every bus¶
RFC-0029 §10 refuses a PAIR that dereferences to a shared mount, because a bus link’s send()
broadcasts (RFC-0020). It names the fix, an egress through the anchor keyed by its slot, and
leaves it to slice S8. Until then the origin’s chain toward anything behind a bus is “PAIRs up to the
bus, NAMEs across it”, which is RFC-0029 §2.2’s second essential divergence. RFC-0030 §9.1 also
leans on the anchor: the pending record’s egress names “the next-hop session … the accepted
session’s anchor where one exists”.
2.4 A second key namespace¶
The private root needs its own key rule (one record whose bytes contain : and /), its own parse
(graph_t::session_anchor_route, which splits the record back into mount and peer with rfind), its
own lookup (find_session_anchor), its own census (session_anchor_slots) and a per-delivery
name lookup of the mount (child_registry_t::entry_by_name). Each exists only because the anchor has
no parent in the tree. A parent deletes all of them.
3. The rulings this document encodes¶
From #1938 (2026-10-07), the stage-5 addendum, ruled “all rec”:
An accepted session’s anchor becomes a real child vertex under its bus connection vertex, named by the peer name the link already uses.
The NAME
<door>/<peer>and the anchor’s PAIR reach the same vertex through the same walk.The anchor inherits the door’s
:acl(nearest bearer) and may carry its own:acloverride.An anchor lives for its session. Every new session bumps the generation, including a returning peer of the same name, so cached pairs to a dead session are refused and fall back to the name.
Send-through an anchor is directed to exactly that session and never broadcasts.
Announce-census peers (CAN) stay vertex-less. The walk gates at the CAN door, and the link’s own peer list resolves the remainder. They get no per-peer ACL, and this is documented.
From #1938, the stage 1–4 rulings this one builds on: the router is stateless and the connection
vertex is the only attachment point; one walk descends to the deepest connection vertex; one
allows call is evaluated there for every spelling; a link cannot exist without its connection
vertex; the maintainer’s model, “a connection vertex is a door to another map” (RFC-0030 ruling 9).
Standing constraints (CLAUDE.md §Design rules): no timers or clock reads in libtracer; no
library-internal buffers; the receiver pays; compile-time policy by default; complexity is lowered
by deleting branches, never by splitting helpers; lookups stay population-independent in vertex
count. §8 checks each one.
4. Vocabulary¶
Proposed for CONTEXT.md at acceptance (§9.4). Door is RFC-0030 §4’s.
Bus door. The connection vertex of a multi-peer link (a listener carrying the
bus_link_tfacet, or a census bus). A bus door is never an egress by itself: its link’ssend()broadcasts (RFC-0020).Session anchor. The child vertex of a bus door that stands for one accepted session, named by the session’s peer name. It is the session’s door: a walk that reaches it with a remainder sends the remainder to that session alone.
Census peer. A peer a bus learns of from announce or heartbeat traffic (ADR-0030, ADR-0044), rather than one this node’s accept policy admitted. A census peer has no vertex.
5. The normative change¶
5.1 The anchor is a child of its door¶
Normative. When a bus link whose sessions are anchored (an accepting listener with
peer_named = true; §5.5 for the others) admits a session, the host MUST register the session’s
anchor as a child of the link’s connection vertex, with the session’s peer name as its one NAME
segment. The name is the one the link already stamps (p<slot> for a creatorless session, ADR-0073
§2), so it passes graph::valid_segment by construction and is the same segment the NAME spelling
already uses.
The anchor is registered by the host’s net plane, never by the application or a peer. It is the one kind of vertex that may exist below a door. RFC-0030 §6.2 (“registering a local vertex below a door is refused”) stands for every other vertex.
The anchor is revived in place. A given
(door, peer name)pair is allocated once and refilled on every later session that receives that name, exactly as today’sregister_session_anchordoes. Anchor count is therefore bounded by the listener’smax_peersand not by churn (ADR-0044 §Amendment’s measurement, unchanged).The private root, the
:<mount>/<peer>key and its parser are deleted (§11.1 slice 5a).
5.2 One walk reaches it, in every spelling¶
Normative; amends RFC-0029 §6 step 3 and RFC-0020 §3.
NAME. A
dstwhose run names<door>/<peer>/…is resolved by stage 1’s descent to the deepest existing connection vertex. When<peer>names a live anchor, the anchor is that vertex. No new branch is needed: an anchor is a connection vertex one level below another.PAIR. A PAIR that dereferences to a live anchor is treated exactly as a PAIR that dereferences to a point-to-point connection vertex (RFC-0029 §6 step 3, first arm). With a non-empty residual it egresses (§5.4). As the last element it addresses the anchor’s own
:facets (RFC-0004 §A’s dual nature).The bus door itself keeps RFC-0029’s shared-mount arm. A PAIR that dereferences to a bus door with a non-empty residual is refused
tr::path::not_found, because a bus door is not an egress (RFC-0020). As the last element it addresses the door’s own facets, as today.Below an anchored bus door, a segment that names no live anchor is rejected
tr::path::invalid, exactly as RFC-0020 §3 rejects a residual naming no current peer. On an anchored door an accepted session is reachable only as its anchor (§14 Q2). A host MUST NOT fall back to the link’s peer list there, and MUST NOT emit on the bus link’s shared endpoint.
RFC-0020 §3’s sentence “MUST NOT resolve the residual against its local graph (a WRITE would
materialize a shadow vertex under the connection mount)” is narrowed: the first segment of the
residual MAY resolve to an anchor, which is the session’s door and not a shadow, and nothing else
below a bus door is ever resolved locally. RFC-0030 §6.2 already forbids the shadow it was guarding
against, and RFC-0030 §7 refuses the creation that would have made one.
5.3 It lives for its session, and every session is a new generation¶
Normative; amends RFC-0029 §8.1.
Departure retires the anchor, before anything else. When a session ends, the host MUST retire its anchor before the link may hand the session’s slot, and therefore its name, to another session. Retirement bumps the saturating generation (RFC-0029 §8.1). This is today’s
fwd_router_t::bus_peer_downorder, made normative.Arrival revives it. A session that receives a name whose anchor was retired revives that anchor at the bumped generation. A returning peer that receives the same name is a new session and gets a new generation, so a pair cached against its previous session fails the generation check, answers
tr::path::not_found, and the path object falls back to the canonical string (RFC-0030 §5.2).An arrival for a live anchor is a session boundary. If the link reports an arrival for a name whose anchor is still live (a missed departure, or a duplicated notification), the host MUST treat it as a departure followed by an arrival: retire, then revive. It MUST NOT keep the old generation for a session that may be new. Today the arrival is refused
PATH_IN_USEand the old generation is kept (§14 Q4).The door going away takes its anchors with it. When the door’s tenancy changes or its link goes down (RFC-0029 §8.2 rules 1 and 2), every live anchor below it MUST be retired before the door is restamped. Accepted sessions report their own boundaries, so RFC-0029 §8.2 rule 4’s per-boot epoch is not needed for them.
Retirement clears the anchor. A retired anchor keeps no
:acl, no value and no edges into the next session (vertex_t::revert_to_placeholder). A per-session ACL override therefore ends with its session (§6.3). This follows ADR-0073 §2: ap<slot>name identifies a session, not a device.
5.4 Send-through is directed¶
Normative; replaces RFC-0029 §10. A walk that reaches a live anchor with a non-empty remainder
hands the remainder to exactly that session: the anchor’s parent is the bus door, the door’s link
resolves the anchor’s name to its directed per-peer endpoint (bus_link_t::peer_link), and the frame
leaves on that endpoint alone. The dst shrinks by the consumed element and src grows by the
door’s NAME run plus the peer segment (RFC-0030 §8.4).
A host MUST NOT emit the frame on the bus link’s shared endpoint, on any arm, including when the session departed between the dereference and the send. In that window the egress finds no directed endpoint and the frame is refused like any other stale address:
tr::path::not_foundto a non-emptysrc, a drop otherwise.No vertex holds a transport pointer (ADR-0038 §3b.1). The anchor-to-session binding is the anchor’s parent and the anchor’s name, resolved by the link’s own peer table. That table is the link’s state, which stage 3 moves onto the transport object. The router adds nothing.
5.5 Census peers stay vertex-less¶
Normative. A bus whose peers are announce-census peers (ADR-0044 §Decision 1; the CAN advertise map) creates no anchor. A walk that reaches such a bus door with a remainder:
is authorized once, at the bus door (§6.1);
hands the remainder to the link, whose own peer list resolves its first segment (
n<node-id>on CAN) to a directed per-peer endpoint, or rejects ittr::path::invalid(RFC-0020 §3, unchanged).
A census peer has no per-peer ACL: every census peer behind one bus door is governed by that
door’s :acl. A census peer has no PAIR, so a chain stays NAMEs across a census bus (RFC-0029 §10’s
last sentence, kept). The census rejection in ADR-0044 (an 8191-wide node-id space would cost
~960 KB of anchors on rv32) is the reason, and it is unchanged. Per-writer authorization on a census
bus is the subject’s job (ADR-0082, ADR-0086), not an anchor’s.
FLAT listeners (peer_named = false) have no bus facet. Their door is an ordinary door with one
routing identity, and none of this section or §5.1–§5.4 applies to them.
5.6 What an anchor is not¶
It holds no value. An anchor is registered value-less (§14 Q6). A data operation that ends at an anchor stores nothing below a door. Its
:facets (:acl,:schema,:children[]) are its surface.Nothing can be created below it. A walk that reaches an anchor with a remainder hands the remainder to the session first (RFC-0030 §6.2), so no creation hook below an anchor is ever consulted.
It is not an identity claim. An anchor names where a session appears in the graph, never who is speaking. That is ADR-0082 §Decision 1, unchanged: the subject is the authorization answer.
6. Request/reply and ACL consequences: the connection vertex is the one gate¶
6.1 One gate, at the session’s door¶
Normative; applies RFC-0029 §6.4 to bus sessions. A frame or a host-API call whose walk reaches a
session anchor is authorized by one allows call, at the anchor, for the caller’s subject and
the operation’s right. That holds for the NAME spelling, the PAIR spelling and a delivery along a
subscriber’s chain alike. The bus door is not gated separately on the way through: one walk, one
gate, at the deepest door. The double gate of today’s bound-delivery arm (anchor, then the mount
looked up by name) becomes that one call.
On a census bus the gate is the bus door, for every peer behind it (§5.5).
6.2 The anchor’s ACL is the door’s¶
Normative. The ACL an anchor is evaluated against is the door’s ACL as the door evaluates it: the door’s own ACEs, then the door’s inherited ancestor ACEs, in the door’s evaluation order. That is the “nearest bearer” of ruling 3, with the anchor counted as part of its door. An anchor with no own ACEs therefore reaches exactly the verdict the door would reach. That is also exactly the verdict the NAME spelling reaches today, so no deployment’s grant set moves when its sessions gain anchors.
The general rule of reference/05 §0x0A is that a bare child evaluates only the ancestor ACEs that
carry INHERIT. This RFC does not apply it to anchors (§14 Q1). Under that rule a door ACE without
INHERIT would stop applying to the door’s sessions, and a door bearing only such ACEs would leave
them with an empty effective list, which is unrestricted on the core subset (reference/05 §0x0A;
under the full model’s strict mode it is denied by default). The reference
implementation realizes the anchor rule as one predicate in the existing bearer walk
(graph_t::acl_allows): an anchor counts as its door’s “self”.
6.3 The anchor’s own :acl override¶
Normative. An anchor MAY carry its own :acl, written through the anchor’s :acl facet by a
caller holding WRITE_ACL there (that is, on the door, by §6.2). Its own ACEs evaluate first,
then the door’s list as in §6.2. The override lives for the session (§5.3 rule 5).
What an override can express depends on the build’s ACL model, and this RFC adds nothing to either:
On the core subset (ALLOW-only, reference/05 §
0x0A), an override can only add grants for one session. Granting a session less than the door grants needs a door that grants less, plus per-session ALLOWs.On the full model (
security_acl, ordered DENY), an override can also narrow one session, because its DENY evaluates before the door’s ALLOW.
A durable per-device policy belongs on the subject (ADR-0082, ADR-0086), which survives the session and the slot. The anchor override is for a decision about one live session.
6.4 Request/reply: the anchor is the egress record’s session¶
Normative; discharges RFC-0030 §9.1. On an anchored bus, the pending record’s “next-hop
session” is the anchor’s PAIR. RFC-0030 §9.2 condition 2 compares it with the session the reply
arrived from. The requester observes that session locally, at the ingress door, when the reply
arrives; it is not a value stamped into src or 0x15 (RFC-0030 §8.4 stamps only the door). Because a returning peer revives the
anchor at a new generation (§5.3), a reply from a successor session in the same slot, with the same
name, fails condition 2 even when it presents a live token. That is a guard RFC-0030 §9.2 could only
offer by name before this RFC.
On a census bus, the session is the peer’s n<node-id> segment, as RFC-0030 §9.1 already says. CAN
names are derived from node identity and are never inherited by another peer (ADR-0044 §Amendment),
so the name comparison is exact there.
Nothing here adds state at a hop or at the terminus. The anchor is the session’s own vertex, bounded by the accept policy. It is not per-request state (RFC-0029 §9.1).
6.5 Stamping across a bus¶
Normative; amends RFC-0029 §4.2 and §6.2 for buses. A node receiving a frame from an anchored session stamps (RFC-0030 §8.4):
into a non-empty
src, the door’s NAME run plus the peer segment, unchanged;into a present
0x15child, the anchor’s PAIR. Todayfwd_router_t::reverse_hop_refalready stamps it; it becomes the rule for every frame that carries the child.
So a reply’s 0x15 chain, and the chain a subscriber learns, is PAIRs across an anchored bus too.
RFC-0029 §2.2’s second essential divergence closes for accepted sessions. It stays open, by design,
across census buses (§5.5).
6.6 Subscriptions¶
A delivery to a subscriber behind an anchored session walks the edge’s chain like any frame. Its
head element is the anchor’s PAIR, and it is gated once at the anchor (§6.1). When the session ends,
the anchor’s retirement makes every such chain stale. The first delivery then answers
tr::path::not_found, and the producer reclaims the edge (#1258), as it does today on the bound
delivery arm. The name-keyed eviction on departure (fwd_router_t::link_down of the peer name) is
unchanged.
6.7 A refusal at an anchor answers the same in every spelling¶
Normative. A refusal at a session anchor has one shape for every spelling. That covers a
generation mismatch, a departed session, and a denial by the anchor’s gate (§6.1). The src decides
whether anything is sent; the spelling does not:
A non-empty
src: the host answers the refusal tosrcwith the identity the NAME spelling answers,tr::path::not_found. This holds whether the frame named the anchor by NAME or by PAIR, and whether the anchor was the last element or a hop.An empty
src: nothing is sent (RFC-0030 §8.5). Every subscriber delivery carries an emptysrc(RFC-0004 Amendment 2), so a refused delivery is a silent drop.
The delivery leg therefore needs no carve-out. Its silence follows from the empty-src rule that
every other frame follows. Today the two spellings differ: on the last-element PAIR arm, which
the bound-delivery path in fwd_router_t serves, a refusal at the mount is dropped silently even
when the frame is a WRITE with a non-empty src. The NAME spelling answers that same WRITE with
tr::path::not_found. Slice 5c makes the PAIR arm answer like the NAME arm (§14 Q8).
7. Cost: throughput, latency and RAM across NARROW, MID and WIDE¶
7.1 Per-peer RAM of an anchor, against today¶
An anchor already exists today for every peer_named session. This RFC moves it; it does not add
one. Per session, against today:
Item, per anchored session |
Today (under the private root) |
This RFC (under its door) |
|---|---|---|
|
64 B rv32 / 88 B host ( |
same |
Key record |
|
|
Vertex-index slot |
4 B rv32 / 8 B host |
same |
Parent’s child-list entry |
one pointer in the private root’s list |
one pointer in the door’s list |
Value |
an empty |
none (value-less, §14 Q6) |
|
never written: no walk reaches it |
allocated only when an override is written (§6.3) |
With the door net/ws and the peer p0, the key record goes from 11 B to 3 B. A plain anchor stays
at the measured cost of a plain vertex: about 111 B on a 64-bit host, as measured by handler_vertex_heap_test in the still-open
#1959 (a PR measurement, not a shipped number), and about 110–120 B on rv32
(ADR-0044 §Amendment, bench_forward_heap), less len(mount) + 2 B of key. §13 clause 4 holds
the claim to “never more than today”.
Per node and per door:
Every graph, every profile: one
vertex_tless. The private anchor root (graph_t::roots_t::anchor) is deleted: −64 B on rv32, −88 B on a host. This includes FLAT and bus-closed builds, which carry it today without using it.Each anchored door: one child list, allocated with its first anchor. That is the same
children_tthe private root carries today, so a node with one anchored door breaks even on it.
Profile |
Typical shape |
Per-peer RAM today |
Per-peer RAM after |
|---|---|---|---|
NARROW (single-upstream MCU) |
FLAT listener or a dialer; or |
0 B |
0 B, and −64 B per graph |
MID (board serving a few browser tabs) |
|
≈ 0.5 KB at 4 sessions |
the same, less |
WIDE (gateway) |
|
≈ 111 B × sessions on a host (the #1959 measurement) |
the same, less |
7.2 peer_named = false keeps FLAT links free¶
ADR-0082’s default is untouched, and this RFC MUST NOT make it cost anything. A FLAT listener has no
bus facet: it registers no anchor, grows no child list, allocates no slot and parses nothing per
frame. The anchor arm of the walk (§5.2) is compiled out where kBusLinks closes the bus tier
(ADR-0082 §Consequences, ADR-0047 §1). The only change a FLAT node sees is the deleted private root
(§7.1).
7.3 Hot path¶
NAME through an anchored session. Today: the mount descent (25 ns per pass,
child_registry.hpp) plusresolve_peer. After stage 1: the tree descent to the door, one more child lookup in the door’s sorted list (a binary search over at mostmax_peersanchors), then the samepeer_link. The added step is bounded by the accept policy, not by the vertex count. Thebench_forward_demuxgate decides (§11.1).PAIR through an anchored session is new. It costs
deref_vertex_slot(11 ns flat), one parent load and the samepeer_link, against a descent it replaces. A bound hop measured 261.7 ns against 304.7 ns for a canonical hop (RFC-0024 §3.4).A delivery along a chain loses one
allowscall under enforcement (§6.1), the per-deliverysession_anchor_routeparse and the per-deliveryentry_by_namelookup of the mount.Payload size. Every change here is per frame and independent of payload size, so the rows above 1 KiB (1 KiB, 4 KiB, 16 KiB and up) move only by the same constant as the 64 B rows.
7.4 Bytes on the wire¶
Across an anchored bus, one 11 B PAIR replaces the NAME run net/<module>/<name>/<peer>. For the
short net/ws/p0 (three records, 10 B) that is +1 B. Every longer mount name saves bytes. Bytes are
not the point: the win is one spelling and no descent. On a census bus, nothing changes.
8. The constraints, checked¶
Constraint |
How this RFC meets it |
|---|---|
No timers, no clock reads |
None added. Session boundaries are the link’s accept and close events (§5.3). |
No library-internal buffers |
None added. The anchor comes from the graph’s injected table source, as today. Its child-list entry replaces one in the private root. |
The receiver pays |
An anchor is drawn from the accepting node’s own graph and bounded by its own |
Compile-time by default |
The anchor arm compiles out with |
Delete, do not split |
Deleted: the private root, the |
Population-independent lookups |
The anchor lookup is bounded by |
9. Normative pages that change¶
9.1 docs/spec/v1.md¶
§3, the 05-protocol-tlvs.md incorporation bullet, its 0x06 routing semantics. “A PAIR MUST NOT
egress through a shared (bus) mount” becomes: a PAIR MUST NOT egress through a bus door. A PAIR
that dereferences to a session anchor egresses to exactly that session (§5.4). A walk below an
anchored bus door reaches only anchors (§5.2). The verdict for a session is evaluated once, at its
anchor, against the door’s ACL plus the anchor’s own (§6.1–§6.3).
9.2 docs/reference/05-protocol-tlvs.md (normative annex)¶
§
0x06§path element PAIR, the per-hop algorithm, step 3. The shared-mount arm splits into two lines. A bus door with a non-empty residual is refused, as today. A session anchor is the point-to-point arm (§5.2). The admonition “until RFC-0029 §10’s session-anchor egress (slice S8) lands” goes.§
0x06, “One element per node’s whole local part”. The example “a shared (bus) mount” becomes “a census bus”.§
0x0AACL. One sentence: a session anchor evaluates its door’s ACL as the door does, then its own (§6.2–§6.3). Census peers have no per-peer ACL (§5.5).
9.3 docs/reference/03-addressing.md¶
The normative annex part (§path syntax) is unchanged: an anchor’s name is an ordinary segment. In the informative routed-scope text, the sentence on nodes that “cannot issue a PAIR — a shared (bus) mount” becomes “a census bus”.
9.4 Informative pages, records and the glossary¶
docs/reference/19-transports-are-vertices.md§What is a vertex: a row for the session anchor (position<door>/<peer>, value-less, retired with its session).docs/reference/16-websocket-session-auth.md: the anchor’s place (under the door, named by the peer) and the override, beside the subject guidance.docs/reference/14-can-transport.md: census peers have no vertex and no per-peer ACL; the CAN door’s:aclgoverns every peer behind it.docs/reference/04-communication-flows.md: a chain crosses an anchored bus as PAIRs.docs/modules/fwd-router.md: the deleted anchor-id machinery and the directed send-through.CONTEXT.md: new entries for Bus door, Session anchor and Census peer (§4). Transport vertex / connection vertex gains “a bus door’s sessions are its child anchors”.Status rows at acceptance: RFC-0029 (§4.2, §6 step 3, §8.1, §10 and §13.2 S8 amended by RFC-0031); RFC-0020 (§3 amended by RFC-0031).
9.5 Records this RFC contradicts, said explicitly¶
Under CLAUDE.md’s precedence rule, each of these is named rather than overridden silently:
The design note on
graph_t::register_session_anchorandgraph_t::anchor_root(core/include/libtracer/graph.hpp): “an anchor is not part of the addressable tree, deliberately … invisible to all of them”. This RFC reverses it. The note’s two reasons are answered here:enumerate_peersstops being the second source of a bus door’s members (§14 Q5), and RFC-0020 §3’s premise is narrowed in §5.2.RFC-0029 §10: “Normative, until §13.2 S8 lands.” This RFC is that landing, and its text replaces §10.
ADR-0044 §Implementation status, “
child_registry_t::by_namefalls back to asking each bus child’sbus_link_t::peer_link(name)”: on an anchored bus the anchor is the route, and the peer-list fallback is kept for census buses only (§5.2, §5.5). ADR-0044’s decisions are unchanged.ADR-0082 §Reason 2, which describes the anchor as a “routing-table entry”: after this RFC it is a child vertex of the door. ADR-0082’s decision and its default are unchanged.
10. Breaking surface¶
Every row is breaking. No compatibility shim is owed (ruling 10 of RFC-0029).
# |
Surface |
Was |
Becomes |
|---|---|---|---|
B1 |
PAIR to a session anchor, non-empty residual |
the anchor is not a door; refused |
directed egress to that session (§5.4) |
B2 |
PAIR to a session anchor, last element |
no addressable surface |
the anchor’s |
B3 |
|
the anchor’s PAIR on the subscribe leg only |
the anchor’s PAIR on every frame that carries |
B4 |
Authorization of a frame through a session |
NAME: at the door; bound delivery: at the anchor and at the door |
once, at the anchor, against the door’s list plus the anchor’s own (§6.1–§6.3) |
B5 |
Below an anchored bus door, a name with no live anchor |
resolved by the link’s peer list |
|
B6 |
Arrival for a live anchor |
refused |
retire and revive, generation bumped (§5.3 rule 3, §14 Q4) |
B7 |
|
synthesized from |
the door’s child anchors (§14 Q5) |
B8 |
Anchor role |
an empty |
value-less (§14 Q6) |
B9a |
Refusal at an anchor, PAIR spelling, non-empty |
silent drop on the last-element arm |
|
B9 |
Host API (reference implementation) |
|
an anchor registered under its door by |
Each implementation slice carries its own CHANGELOG.md entry under Breaking (core/, and
bindings/typescript/ if its client spells a bus route) for the rows it lands. This RFC is not
itself a public API change.
11. Slices and conformance¶
11.1 Slices (the stage-5 ticket of #1938)¶
All four land under #1948, which is blocked by #1939 (one walk, one gate), #1940 (the connection vertex is mandatory) and this RFC.
Slice |
Contents |
Gate |
|---|---|---|
5a anchors under the door |
§5.1–§5.3: register the anchor as a child of the door by |
|
5b directed send-through |
§5.4 and §6.5: the PAIR arm admits an anchor ( |
|
5c one gate |
§6.1–§6.3: one |
the ACL tests of §11.3; |
5d pages |
§9 text, the glossary and the status rows. |
doc gates ( |
5a lands first. 5b and 5c each depend on 5a only.
11.2 Conformance vectors¶
Kept, rescoped: fwd/bus-mount-refuses-pair names a bus door with a residual (§5.2).
fwd-bus-name-reject (RFC-0020 §5) keeps its bytes.
New (names indicative):
fwd/pair-anchor-egress-directed: a PAIR to an anchor with a residual egresses withdstshrunk by the element andsrcgrown by the door run plus the peer segment;fwd/pair-anchor-last-element-is-facet;fwd/pair-anchor-stale-generation-not-found: the pair of a departed session, after a new session took the same name;fwd/name-below-anchored-door-no-anchor-invalid;fwd/reverse-0x15-stamps-anchor-pair: a frame arriving from an anchored session carries the anchor’s PAIR at the head of0x15;fwd/census-peer-stays-names: across a census bus,0x15carries the NAME run;fwd/anchor-refusal-spelling-independent: aWRITEwith a non-emptysrcrefused at an anchor draws the sametr::path::not_foundreply bytes whetherdstnames the anchor by NAME or by PAIR. The same refusal with an emptysrc(a delivery) draws no frame.
11.3 Behaviour tests (the S set, on the wire and at the host API)¶
Each is driven through the node’s external surface (#1938 §Testing), and each fix-shaped test is shown to fail with its change ablated:
NAME and PAIR to one session reach the same vertex and the same verdict, with enforcement on and off;
the door’s ACL governs a session with no override, including a door ACE without
INHERIT(§6.2);an anchor override adds a grant (core subset) and, on the full model, narrows one session;
the override is gone after the session ends and a new session takes the name;
a reconnect under the same name gets a new generation, the old pair is refused, and the name still works;
an arrival for a live anchor bumps the generation (§5.3 rule 3);
send-through reaches exactly one session, and a session departing mid-send leaves nothing on the shared endpoint;
a census peer resolves through the link’s peer list, is gated at the door, and has no vertex;
a FLAT listener allocates no anchor and no child list across 1,000 accept and close cycles;
§6.7: a gate refusal at an anchor answers
tr::path::not_foundto a non-emptysrcon both spellings, and stays silent on a delivery;RFC-0030 §9.2: a reply from a successor session in the same slot fails condition 2.
12. Alternatives considered¶
Keep the private root and do S8 as RFC-0029 §10 wrote it (a directed send keyed on the anchor’s slot, the anchor still unreachable by name). It closes the send-through gap and nothing else. NAME and PAIR keep reaching different vertices with different gates, the
:<mount>/<peer>key and its parser stay, and the anchor remains the one thing on a node that a walk cannot reach. Rejected by ruling 1.Anchors for census peers too. The ADR-0044 arithmetic still holds: ~960 KB at CAN’s full node-id space on rv32, and a census is driven by another node’s liveness, not by this node’s accept policy. Rejected by ruling 6.
An incarnation suffix on the peer name (
p0-7) instead of a generation. Rejected by the ADR-0044 amendment already: it is irreversibly wire-visible, grows everysrc, and contradicts ADR-0073 §2.Bare-child inheritance for anchors (only
INHERITdoor ACEs reach a session). It is the general rule with no special case, but it changes the verdict for every deployment whose door ACEs lackINHERIT, and it can leave sessions with an empty effective list, which is unrestricted on the core subset. Rejected in favour of §6.2; see §14 Q1.Fall back to the link’s peer list when an accepted session has no anchor. One fewer branch, but the same session would then be reachable as two vertices with two gates, which is the state this RFC removes. Rejected; see §14 Q2.
Keep a per-session override across sessions, keyed by name. A
p<slot>name identifies a session, not a device (ADR-0073 §2), so a policy keyed by it would pass to whoever takes the slot next. Durable policy belongs on the subject. Rejected.Give the anchor a transport pointer for a faster egress. ADR-0038 §3b.1 forbids a vertex that holds a transport pointer. The parent door and the link’s own peer table already give the egress. Rejected.
Make every listener
peer_namedso that every session has an anchor. This is ADR-0082’s rejected flip: per-peer state everywhere for an addressing capability most nodes never use. Rejected.
13. What would falsify this RFC¶
The NAME arm through an anchored session regresses on
bench_forward_demuxbeyond the A/A band at any session count up tomax_peers, at any payload size including above 1 KiB, against today’sresolve_peerpath. Then the child lookup of §7.3 is the wrong structure, not the model.NAME and PAIR to one session can reach different verdicts, or different replies to one refusal. §6.1 claims one gate and §6.7 claims one refusal shape. A reachable counterexample means a second gate or a second refusal arm survived somewhere.
A reachable sequence lets a stale anchor pair validate for a successor session other than through a saturated generation. §5.3’s rules are the whole guard.
An anchored session costs more RAM than today on rv32, or a FLAT listener pays anything per peer. §7.1 and §7.2 claim neither happens.
A transport cannot pair its arrival and departure reports, so that §5.3 rule 3 bumps live sessions often enough to cost measurable re-learn traffic. That would be a measurement on a real listener (the ESP httpd link is the first to check), and the answer would be a fix to the transport’s reporting, not a refusal in the graph.
14. Questions for the maintainer, each with a recommendation¶
Ruled 2026-10-07: the maintainer accepted every recommendation below as written.
Does an anchor evaluate the door’s ACL as the door does (§6.2), rather than as a bare child (only
INHERITACEs)? Recommendation: yes. It keeps every existing door verdict for sessions exactly as the NAME spelling reaches it today, it can never leave a session with an empty effective list while its door has ACEs, and it costs one predicate in the bearer walk.On an anchored bus door, is a session reachable only as its anchor (§5.2)? Recommendation: yes. A name with no live anchor answers
tr::path::invalid, as RFC-0020 §3 already answers a name with no peer. A fallback to the link’s peer list would let one session be two vertices with two gates.If an anchor cannot be registered at admission (the table source refuses its block), is the session refused? Recommendation: yes. The link closes it and counts the refusal (
core/STYLE.md§Introspection). Under Q2 a session without an anchor is unreachable and ungateable, so admitting it buys nothing. The receiver pays: admission fails on the receiver’s own budget.Is an arrival for a live anchor a session boundary (§5.3 rule 3)? Recommendation: yes, retire and then revive. A spurious bump costs one re-learn. A missed bump lets a cached pair reach a session that may be new, which is the class #1223 closed. Today’s
PATH_IN_USEkeeps the old generation.Does
:children[]of an anchored bus door list its real child anchors, with the link’s synthesis kept only for census buses? Recommendation: yes. There is one source per bus kind, and the synthesis (handlers_t::on_childrenwired tobus_link_t::enumerate_peers) stops being a second answer for the same members.Is an anchor value-less (§5.6)? Recommendation: yes. An anchor is a door, not a store. Today it is an empty
STORED_VALUE, so a data write that ended at one could leave a value below a door, which RFC-0030 §6.2 forbids for every other vertex. The conformance test pins the answer a data operation at an anchor receives.Is the session’s liveness published on the anchor (the way a connection vertex publishes its link state, reference/19)? Recommendation: no, not in this RFC. An anchor exists only while its session is up, so its existence is the liveness, and a departure is already observable as a
:children[]change on the door. A value can be added by a later amendment if a consumer needs one.Does a refusal at an anchor answer
tr::path::not_foundto a non-emptysrcon both spellings (§6.7), with the delivery leg’s silence following from its emptysrc? Recommendation: yes. The alternative keeps today’s split: answer on the NAME spelling, drop on the PAIR spelling, with the delivery leg written down as a carve-out. That makes the reply depend on the spelling, which RFC-0029 §6.4 rules out, and it needs a branch that the empty-srcrule already covers. The identity is the one the NAME spelling answers today, so no existing NAME reply changes.
15. Discussion¶
Per GOVERNANCE.md, the comment window is waived by default while the project is solo-maintained, and it is not invoked here. Sustained objections and their resolution are recorded in this section as they arrive.