# ADR-005: Session substrate types — duplex + stateless over one state machine
## Status
Accepted (substrate inputs amended by ADR-016: both substrates receive
the service dimension explicitly)
## Context
POC-1 and POC-3 found recurring glue that must not be re-implemented per
adapter or per handler:
- The split → tokio-util compat → packetline bridge (POC-1).
- `StreamingPeekableIter`'s stop-delimiter contract: flush returns `None`,
the iterator is inert until `.reset()`; forgetting reset silently
swallows the next command (POC-1's second hang).
- The three-layer return type `Option<io::Result<Result<PacketLineRef,
decode::Error>>>`.
- The EOF-mid-parse error loop: an error-shaped read that resets-and-retries
spins a tokio worker at 100% (POC-3 recorded bug class) — error branches
must break unconditionally.
- The http-side request-body adapter (axum `BodyDataStream` →
`futures_io::AsyncRead`, ~40 lines, POC-3).
- The smart-http framing facts: first POST carries the capability dump
(skip two flush-delimited sections); capabilities before `0001` delim,
args after; responses end at flush, never `0002` (remote-curl dies on
it); the flush is one-shot per response (idempotent-flush guard is
mandatory); a flush-only POST is a probe answered 200-empty.
If adapters or handlers hand-roll any of this, each gets it subtly wrong.
## Decision
`alkgit` owns the substrate layer; doors and handlers see
friendly types:
1. **`Session` (duplex)** — wraps (stream, limits, service) into the
pkt-line session: owns the split/compat bridge, the request reader (command line
+ pre-delim lines + args + flush, with the `reset()` discipline and the
unconditional-error-break rule), and the writer (sideband sink with
65000-byte chunks, idempotent flush). The service selects the
advertisement and state machine at start (ADR-016). Seed: POC-1's
`serve_v2`.
2. **`StatelessRequest` (http)** — wraps (request-reader, response-writer,
service) per POST: performs the capability-dump skip, the delim-aware arg split,
and the flush-only response rule (no `0002`, idempotent flush,
200-empty for flush-only probes); the door's route supplies the
service (nothing in a POST body distinguishes the services — ADR-016).
Seed: POC-3's `httpservice.rs` + `BodyReader`.
3. **One protocol core** — the V2 state machines (ADR-003) run under both
substrates; the stateless substrate presents each POST as a command
invocation. Version dispatch happens at session start, in one place.
4. **Error taxonomy** — substrate errors are a transport-level
`thiserror` enum; io errors are terminal (session ends), protocol
errors map to pkt-line error bands / http status responses per the
respective substrate's convention.
The sideband sink (band-1 chunks ≤ 65000 bytes, blocking-side `io::Write`
that parks on a bounded channel on the http path) is part of the substrate:
exactly one task drives a given sink (POC-2's `SidebandSink` contract).
## Consequences
- Adapters (http, ssh, downstream) cannot get the framing wrong; the
POC-hang and spin-bug classes are structurally prevented.
- The packetline raw API is confined to transport internals.
- State machines are substrate-agnostic, which is what makes smart-http's
statelessness cheap (per-request state, no session to restore).
## References
- `docs/research/poc-1-findings.md` §"Answered hypotheses" 3 (packetline contracts),
follow-ups 1–2
- `docs/research/poc3-findings.md` §"Course corrections" 1, follow-ups 2, 4
- `docs/research/gitoxide.md` §"Wire format"
- ADR-002 (substrate shapes), ADR-003 (V2-first), ADR-004 (sink = io::Write),
ADR-016 (service selection in the substrate inputs)
- transport.md §substrate