diff --git a/docs/architecture/consumer.md b/docs/architecture/consumer.md index 05406d4..902950b 100644 --- a/docs/architecture/consumer.md +++ b/docs/architecture/consumer.md @@ -84,7 +84,11 @@ one API, `-L` and `-R` alike. `Some(bytes)` per datagram — possibly empty (`len=0` is a legal empty datagram) — and `None` only on stream EOF (the channels-level sentinel; the codec never collides with it, F-2). A >65535-byte - send is rejected at frame time (`Oversize`). + send is rejected at frame time (`Oversize`). `take_halves()` on a + datagram session yields the RAW halves — the wire bytes carry the + codec framing (ADR-005's N-3 amendment: one `take_halves` semantic + for every substrate; keep the session for codec-applied I/O, or + compose the framed adapter at your own boundary). ## Lifecycle and Teardown (the point of ADR-005) diff --git a/docs/architecture/decisions/005-consumer-session-owns-teardown.md b/docs/architecture/decisions/005-consumer-session-owns-teardown.md index 79ac78d..9240ad9 100644 --- a/docs/architecture/decisions/005-consumer-session-owns-teardown.md +++ b/docs/architecture/decisions/005-consumer-session-owns-teardown.md @@ -2,7 +2,13 @@ ## Status -Accepted (2026-09-07; resolves the reverse POC W3 finding) +Accepted (2026-09-07; resolves the reverse POC W3 finding). +Amended (2026-09-08, review 001 N-3 decision): the datagram +`take_halves` posture is pinned — raw framed halves stay the only +owned-halves shape (`take_halves` keeps its semantics for every +substrate), the datagram accessors remain the codec path, and a +framed-halves convenience is deferred to the first consumer that +reaches for it. See §Amendment (2026-09-08, N-3). ## Context @@ -86,6 +92,58 @@ its teardown: ## Consequences +### Amendment (2026-09-08 — the N-3 datagram `take_halves` posture, decided before the first external consumer) + +Review 001 N-3: a datagram session exposes codec-applied accessors +(`send_datagram`/`recv_datagram`) until `take_halves` — then the +caller owns RAW halves whose wire bytes carry the `[len: u16 BE]` +framing. Documented, but a footgun asymmetry the API window +(this ADR's surface becomes ABI-stable at the first external +consumer) forced a decision on. Decided: **option 1 — keep as-is, +carried by docs**, with the rationale: + +- **No external consumer exists** (verified 2026-09-08 — no crate in + the workspace depends on alktunnels); the window is open, so a + breaking reshape was *available* — but nothing yet demonstrates a + need for a different shape. +- **The framing asymmetry is ADR-003's spec, not drift.** The + datagram data plane is mandatory-framing by substrate + discrimination; "raw halves of a datagram session carry the + frames" is the same statement as "the codec lives at the substrate + boundary." `take_halves` yielding raw halves keeps ONE semantic + across substrates (the tunnel's wire bytes, uninterpreted); a + per-substrate `take_halves` (option 3) diverges semantics by + substrate — the worse confusion at the type level even as it + removes the byte-level footgun. +- **`pump_against` needs the framed adapter, not framed halves.** The + adjacent seam (N-11, resolved in-review with the doc note): a UDP + session pumped against an accepted local half requires the FRAMED + adapter on the accepted side — the `local` feature's `UdpHalf` (or + an assembly equivalent), not a raw socket. What composes there is + the adapter AT the boundary, not a session-side wrapper pair; a + `take_framed_halves` convenience would not feed this use. +- **The convenience is additive and can land later.** Option 2's + `take_framed_halves` breaks nothing when added after the first + consumer (unlike a `take_halves` reshape), so its timing is + decoupled from this window — deferring it costs nothing now, and + the first real consumer's reach defines its shape (a + `DatagramReader`-wrapped pair has an open design question: an + `AsyncRead` face re-collides with the F-2 empty-datagram/EOF + ambiguity the codec exists to prevent — the codec-side framing + wants accessor-shaped surface, which the session already has). +- **The docs carry the contract.** `take_halves`'s doc states the + framing (landed with the implementation); consumer.md's datagram + variant section now states the posture explicitly. + +**Consequence:** datagram callers wanting codec-applied I/O after +taking ownership should keep the session (drive `send_datagram`/ +`recv_datagram`; the session IS the owner) or compose `UdpHalf` at +their own boundary — `take_halves` is the raw-wire escape hatch and +stays documented as such. Revisit only on a demonstrated consumer +need (file an OQ at that point). + +### Consequences (original) + - **Assembly layers cannot leak adopted channels** by forgetting to reap — the type is the discipline (the W3 gap closes structurally). - **The reverse-flow initiator gets the same session shape** as the diff --git a/docs/reviews/001-implementation-review.md b/docs/reviews/001-implementation-review.md index f407972..39fdb81 100644 --- a/docs/reviews/001-implementation-review.md +++ b/docs/reviews/001-implementation-review.md @@ -447,6 +447,17 @@ convenience `take_framed_halves` that keeps the codec (a the review notes it for the API-stability window (ADR-005's surface becomes ABI-stable at first external consumer — decide before then). +**RESOLUTION (2026-09-08, decided before the first external +consumer):** option 1 — keep as-is, docs carry it — recorded as +ADR-005's §Amendment (2026-09-08, N-3). No external consumer exists +(window open, so the breaking reshape was available but +undemonstrated); one `take_halves` semantic across substrates beats +per-substrate divergence; the `pump_against` seam composes the +framed ADAPTER (UdpHalf), not framed halves; and the convenience +(option 2) is additive — it can land after the first consumer +defines its shape without breaking anything. consumer.md's datagram +section states the posture. + ## N-4 [minor] — `open_reverse_channel` builds the params JSON by hand instead of serializing `TunnelParams` — FIXED in-review `src/consumer.rs:395-409`: the substrate is matched to its string and diff --git a/tasks/tunnels/decide-take-framed-halves.md b/tasks/tunnels/decide-take-framed-halves.md index 4a49341..9266172 100644 --- a/tasks/tunnels/decide-take-framed-halves.md +++ b/tasks/tunnels/decide-take-framed-halves.md @@ -1,13 +1,14 @@ --- id: tunnels/decide-take-framed-halves name: "N-3 decision — take_halves on datagram sessions hands RAW FRAMED bytes; decide the API posture before the first external consumer" -status: pending +status: completed depends_on: [tunnels/review-impl] scope: single risk: low impact: component level: research tags: [api, udp, codec, adr-005, api-stability, review-remediation] +completed: 2026-09-08 --- ## Description @@ -66,12 +67,38 @@ exposes both codec halves of the job). ## Acceptance Criteria -- [ ] A written decision (ADR amendment or task-note resolution) with +- [x] A written decision (ADR amendment or task-note resolution) with the chosen option and rationale, before the first external consumer -- [ ] If the decision adds surface: tests + battery green; review 001 +- [x] If the decision adds surface: tests + battery green; review 001 §N-3 gets a resolution note +## Resolution (2026-09-08) + +**Option 1 — keep as-is, docs carry it.** Recorded as ADR-005's +§Amendment (2026-09-08, N-3). Decision inputs: + +- **No external consumer exists** (workspace grep verified 2026-09-08) + — the window is open, so the breaking reshape (option 3) was + available but nothing demonstrates the need. +- One `take_halves` semantic across substrates (the tunnel's wire + bytes, uninterpreted) beats per-substrate divergence; the framing + asymmetry is ADR-003's spec, not drift. +- The `pump_against` seam composes the framed ADAPTER (`UdpHalf`, + ADR-003's boundary codec — landed with the U-1 remediation the same + day), not framed halves; option 2 would not feed that use. +- Option 2 (`take_framed_halves`) is additive and can land after the + first consumer defines its shape without breaking anything — + deferring costs nothing. Design note recorded in the ADR: an + `AsyncRead`-faced wrapper re-collides with F-2's + empty-datagram/EOF ambiguity; the codec-side framing wants + accessor-shaped surface, which the session already has. + +Docs sync: ADR-005 §Amendment; consumer.md's datagram section states +the posture; review 001 §N-3 resolution note landed. No code change +(the decision adds no surface), so the standing battery is the +verification: 63 default / 76 local green, doc build clean. + ## References - `docs/reviews/001-implementation-review.md` §N-3 (the finding) and