RFC 0002 — Protocol error model: the tr:: concept namespace¶
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 |
0002 |
Title |
Protocol error model: the |
Status |
accepted (2026-07-03) |
Author(s) |
AvatarSD (maintainer) |
Created |
2026-06-24 |
Comment window |
waived by the maintainer (solo-maintainer project, GOVERNANCE.md window dead ceremony) |
Tracking issue |
#3 (RFC-0001 umbrella) |
Target spec version |
v1 (draft refinement — no released v1 yet, so no v2 needed) |
Supersedes |
RFC-0001 §C.1; subsumes RFC-0001 §C.2, §C.3 |
Summary¶
This RFC replaces libtracer’s flat, numeric error-code registry with a hierarchical error namespace keyed by stable protocol concept — tr::<concept>::<error> (e.g. tr::path::not_found). An error’s wire identity is either a compact registered code (a single integer the frozen registry assigns to a built-in path) or the literal string path (for unbounded third-party extensions); optional structured detail may attach to either. Per-error severity and disposition live in the registry, not on the wire. The namespace is protocol-only — applications never emit protocol errors (ADR-0010). The supporting decisions are ADR-0009 (shape) and ADR-0010 (closed boundary); this RFC is the spec-domain change that applies them and fixes the wire layout of ERROR (0x08).
Motivation¶
RFC-0001 §C.1–§C.3 treated ERROR as a flat list of byte codes (0x00–0x7F core, 0x80–0xFF user) with the code carried as a leading child VALUE. A maintainer review found three problems that a 20-year-frozen v1 cannot ship with:
The module/source set is unbounded. A flat byte registry has no room for the ~50 catalogued modules plus future and third-party ones; reserving a numeric range per module does not scale.
Numeric/module identity is not implementation-independent. Keying an error to the C reference’s modules (
resolver,dispatcher, …) is meaningless to a second implementer in another language and brittle under refactor. The reference suite’s whole premise is implementation independence.There is no such thing as an application protocol-error. Applications signal failure as ordinary data, self-described by their schema — the same way the protocol defines no application data types. A
0x80–0xFF“user error” range is a category error.
Proposed change¶
A. The namespace (ADR-0009)¶
An error is identified by a path in a hierarchy keyed by stable protocol concept:
tr::<concept>::<error>
<concept> is drawn from a small, frozen set that names a protocol concern (the CONTEXT.md vocabulary), not an implementation module:
frame · tlv · path · schema · flow · access · transport · version
The namespace is prefix-filterable with the path wildcard (03-addressing.md §index forms): a subscriber MAY watch tr::flow::* or tr::transport::*.
B. Identity forms (ADR-0009)¶
An error’s identity is carried in exactly one of two forms:
Registered code — a single
u16the registry below assigns to the whole built-in path. The hot, finite, built-in case.String — the literal UTF-8
tr::…path. For modules not in the frozen registry (third-party stack extensions,tr::<vendor>::…) — the unbounded case. No registry entry, no RFC required.
The registered-vs-string choice is the built-in-vs-extensible split. Optional structured detail (a human DESCRIPTION, a concept-specific VALUE, …) MAY follow either identity.
C. Wire layout of ERROR (0x08)¶
ERROR is a structured TLV (opt.PL=1) in all cases — it is never special-cased, and a generic PL=1 walker handles it. Its first child is the identity, distinguished by type:
First child |
Identity form |
Payload |
|---|---|---|
|
registered code |
|
|
string |
UTF-8 |
Subsequent children are optional detail (DESCRIPTION 0x03, VALUE 0x01 binary detail, or concept-specific TLVs). A reader walks children; the first child’s type alone selects the identity form. This keeps the universal “PL=1 ⇒ purely concatenated child TLVs” rule (ADR-0003) — there is no raw prefix byte.
Worked bytes — tr::path::not_found (registered code 0x0020), code-only:
08 40 06 00 01 00 02 00 20 00
└ERROR hdr┘ └── VALUE child ──┘ ERROR: type=08 opt=40(PL=1) len=6
type=01 opt=00 len=2 VALUE payload = 20 00 (u16 LE = 0x0020)
= 10 bytes (14 wrapped in STATUS: 09 40 0A 00 + the 10 above)
ERROR continues to appear as a STATUS (0x09) child; a bare ERROR (no STATUS wrapper) is permitted for protocol-stack reporting where there is no request to answer.
D. Registry (ADR-0009)¶
Normative for protocol v1; the built-in set below is frozen, and additions are RFC-gated. severity ∈ warn | error | critical; disposition ∈ transient (retry) · permanent (don’t retry this request) · fatal (tear down the peer).
Code |
Path |
Severity |
Disposition |
Was (RFC-0001 flat) |
|---|---|---|---|---|
|
|
error |
transient |
|
|
|
error |
permanent |
|
|
|
error |
transient |
|
|
|
error |
permanent |
|
|
|
warn |
permanent |
|
|
|
warn |
permanent |
|
|
|
warn |
permanent |
|
|
|
error |
permanent |
|
|
|
warn |
permanent |
|
|
|
warn |
transient |
|
|
|
warn |
transient |
|
|
|
error |
permanent |
|
|
|
error |
permanent |
|
|
|
error |
transient |
|
|
|
critical |
fatal |
|
OK is removed — an empty STATUS already means OK. tr::version::mismatch is a discovery/bridge-level outcome (RFC-0001 §C.2), not a frame-parse result. tr::frame::invalid covers reserved-bit-set / type=0x00 / oversize length (RFC-0001 §C.3).
E. Closed boundary (ADR-0010)¶
There is no user/application error range. An application failure is ordinary data written into the graph and described by the application’s schema. The protocol interprets neither application data nor “application errors.”
F. Conformance (§4)¶
New vectors under tests/conformance/vectors/v1/errors/: a registered code-only error (tr::path::not_found), a registered error with a DESCRIPTION detail child, a string-form error (tr::acme::widget::jammed), and tr::frame::invalid for a reserved-bit-set input. docs/spec/v1.md §3 (once it incorporates reference/05 per RFC-0001 §A.2) inherits the ERROR layout from §C above.
Compatibility¶
Breaks protocol-v1 implementations? No released v1 exists; this is a draft refinement before the freeze gate. The flat byte registry was never shipped.
Supersession: RFC-0001 §C.1 (leading-child-
VALUEcode) is withdrawn; §C.2 (VERSION_MISMATCHreword) and §C.3 (INVALID) are subsumed here astr::version::mismatch/tr::frame::invalid. RFC-0001 should be edited to point here for theERROR/registry items; its other sections (versioning,LIST,io_dir_t, API, address-shift) are unaffected.New conformance vectors: the
errors/set above.
Alternatives considered¶
Per-module numeric
{module:u8, code:u16}and module-named paths (tr::graph::resolver::…) — rejected in ADR-0009: unbounded and implementation-coupled.ERRORasPL=0opaque with theu16code as the whole payload (6 bytes, cheapest registered form). Tenable, and ~4 bytes cheaper per error, but it makesERRORspecial (a reader must branch onopt.PLto find the identity) and complicates the string/detail forms. Rejected in favour of always-PL=1(one mental model,ERRORnever special). Re-openable in comment if the embedded byte cost proves to matter on a hot error path.A user/application error range — rejected in ADR-0010.
severity/dispositionon the wire — rejected: registry-resident, derived on receipt.
Discussion¶
Accepted 2026-07-03 with the comment window waived (maintainer call — a solo-maintainer window has no commenters). Points that were open for comment, resolved as proposed: the §C PL=1-always vs PL=0-cheap layout; the §D code assignments and the severity/disposition scales; whether the string form’s tr::… path should be one NAME or a /-style segmented structure for finer prefix matching.