Port the alknet-tty architecture docs into alktty and add the BAST document for the alk/tty wire format. Docs-only; no Rust source changes. Spec docs (docs/architecture/, flat layout — single-crate repo): - overview.md — crate purpose, two-carriage model, deps, ALPN, backend location map, feature gates - tty-wire.md — 5-byte chunk codec, control channel split (STREAM_CTRL_IN=3 / STREAM_CTRL_OUT=4), sentinels - tty-backend.md — TtyBackend trait, TtyHandle, TtyControl, REQ-TTY-01 (backends need not be natively async) - tty-adapter.md — TtyAdapter, three-pump driver, exit-chunk ordering (ADR-004), cancel cleanup (ADR-005), access control - tty-local.md — LocalTtyBackend (local feature module), PTY + pipe modes, REQ-TTY-02 (signal forwarding to process group) - README.md — architecture index ADRs (docs/architecture/decisions/, renumbered 001..008 from alknet 052,053,054,055,056,057,077,093 in order): - 001 wire format + two-carriage model (incl. Phase 7 control- channel split amendment) - 002 TtyBackend trait + TtyHandle - 003 local backend placement (records both the alknet sibling- crate decision and the alktty single-crate consolidation behind a local feature) - 004 exit code on a control chunk - 005 backend cleanup on session cancel - 006 self-contained negotiation framing - 007 tty inside channels (reversed by 008; kept for historical context with reversal notice) - 008 channels pure channel multiplexing (reverses 007; TTY always uses its 5-byte format) BAST document (docs/architecture/tty-bast.md): - Normative JSON spec for the alk/tty wire format, conforming to the BAST meta-schema at https://alk.dev/bast/v1/schema - 5-byte chunk header (struct, big-endian: stream_type uint8, length uint32) + StreamType enum (Stdin=0..CtrlOut=4) - ControlMessage union (field-name discriminator on type: resize/signal/eof/exit) with documented deviation that on-wire control payloads are UTF-8 JSON, not BAST's binary union encoding - NegotiationFrame (4-byte BE length + UTF-8 JSON body) + NegotiateRequest / TerminalParams JSON shapes - StreamType enum deviation noted: on-wire uint8, not BAST's standard u32 enum index (chunk header is 5 bytes, not 8) - alktty does not depend on alktype; the hand-rolled wire.rs is the runtime codec, the BAST is the human-readable contract AGENTS.md: fixed the ADR mapping table to match the plan's 8-to-8 mapping (the previous table substituted ADR-050 for 054, relabeled 056 as control-message split, dropped 077, and added a new control-split ADR at 006 — inconsistent with both the plan and the prose). ADR-050 (dynamic resource ownership) is an alkcall/alknet- core ADR, not tty-specific, and is not ported; the Phase 7 control split stays as an amendment inside ADR-001, mirroring alknet. Verification (all pass, no Rust source changed): - cargo test (80 passed) - cargo test --all-features (103 passed) - cargo clippy --all-targets -- -D warnings (clean) - cargo fmt --check (clean) - cargo check --target wasm32-unknown-unknown (clean) - cargo clippy --target wasm32-unknown-unknown -- -D warnings (clean) - cargo doc --no-deps: 9 pre-existing intra-doc-link warnings in src/session.rs and src/channels.rs (untouched by this commit; not introduced here) - BAST JSON parses; StreamType indices match wire.rs constants (0=Stdin..4=CtrlOut) - all markdown cross-reference links resolve
28 KiB
ADR-008: Channels Pure Channel Multiplexing (8-Byte Header, No stream_type)
Status
Accepted (amends alknet ADR-071 — wire format is 8 bytes, not 9, and
the channels layer has no stream_type concept; amends alknet ADR-074
— into_sub_streams() removed, accept_bi is the only accessor and
yields one BiStream per channel; reverses ADR-007
— TTY always uses its 5-byte format, the channels layer carries it
transparently in the payload. Ported from alknet ADR-093 2026-08-17;
cross-references renumbered to alktty's ADR range — ADR-052→001,
ADR-053→002, ADR-054→003, ADR-055→004, ADR-056→005, ADR-057→006,
ADR-077→007, ADR-093→008. The alknet ADRs referenced by alknet number
(052, 070, 071, 072, 073, 074, 075, 076, 078, 079, 080, 081, 092) are
not ported into alktty's ADR range — they are alknet-core /
alknet-channels ADRs. The alknet originals at
/workspace/@alkdev/alknet/docs/architecture/decisions/ remain
authoritative.)
Context
alknet ADR-071 committed the channels wire format as a 9-byte chunk
header ([channel_id:u32][stream_type:u8][length:u32]) — a 4-byte
extension of TTY's 5-byte format, with stream_type carried in the
channels header and decomposed into unidirectional halves (0/1/2 =
data write/read/err, 3/4/5 = control write/read/err, % 3 formula).
alknet ADR-074 added a second accessor (into_sub_streams())
alongside accept_bi for handlers that need typed sub-streams (TTY's
stdin/stdout/stderr/ctrl-in/ctrl-out). ADR-007
split TTY's wire format into two modes — direct (5-byte) and
inside-channels (the channels layer de-chunks and the adapter
destructures via into_sub_streams()).
The stream-unification research surfaced that these three decisions
share one root: the channels layer carries a concept (stream_type)
it doesn't own. The 9-byte header bakes TTY's sub-stream multiplexing
into the channels wire format. The into_sub_streams() accessor exists
because the channels layer reassembles per-stream_type and needs to
expose the result. The two-mode TTY design exists because the channels
layer's stream_type overlaps with TTY's own stream_type. The mod
2/mod 3/mod 4 numbering question (settled as mod 3 in alknet ADR-071
revised) was a symptom of this overlap — a numbering convention for a
concept the channels layer shouldn't carry.
The structural question
The channels layer has two objectives in tension:
- "Pass a stream to/from any ALPN" — every channel is a
BiStream; any handler getsaccept_bi()and treats the channel as a duplex stream. Uniform, transport-agnostic, recursive-composition-friendly. - "Channels carry N sub-streams" — a TTY channel carries
stdin/stdout/stderr/control; the handler destructures via
into_sub_streams(). Carries what the source produces.
The tension is real when a sub-stream is unidirectional (stderr). You
can't represent stderr as a BiStream without wasting the write half;
you can't make it a "third half" (mod 3) without breaking pair
symmetry; you can't make the channel a single BiStream without
losing the stdout/stderr distinction.
alknet ADR-074's two-accessor design resolves this by making the "pass a stream to/from any ALPN" objective qualified — it applies to single-stream channels (tunnel, SSH, call), not multi-stream channels (TTY). The mod 2/mod 3/mod 4 numbering was a symptom of that qualified design.
The resolution: channels layer is pure channel multiplexing
The channels layer's job is "one connection carries N channels, routed
by channel_id." It does not know about TTY's sub-streams, SSH's
channel protocol, or how call frames its JSON. Handlers own their
sub-multiplexing on the BiStream the channels layer gives them.
- Every channel is a
BiStream.accept_bi()yields oneBiStreamper channel (per alknet ADR-092, already landed). Nointo_sub_streams(), no second-class accessor. - Handlers sub-multiplex their
BiStreamhowever they want. TTY sub-demuxesstream_typefrom itsBiStream(its 5-byte format). Tunnel uses theBiStreamas raw bytes. Call length-prefixes JSON. SSH runs its own channel protocol. The channels layer carries the bytes transparently. - The mod 2/mod 3/mod 4 question dissolves at the channels layer.
The channels layer has no
stream_typeconcept — not in its header, not in its code, not in its mental model.stream_typeis the inner layer's framing byte, carried transparently. - The control channel is handler-internal. TTY sub-demuxes control
from its io
BiStreamusing its 5-byte format (STREAM_CTRL_IN = 3,STREAM_CTRL_OUT = 4— ADR-001 amended by Phase 7). The channels layer doesn't carry control. The "control isn't actually bidirectional" flaw is fixed at the TTY layer, not the channels layer. - Recursive composition is literal. A channel with ALPN
alk/channelsruns another channels demux on itsBiStream. The outer layer strips its 8-byte header; the inner layer parses its own 8-byte header from the payload. Each level is the same shape —BiStream → accept_bi → N BiStreams.
The wire format decision: 8 bytes
The channels wire format is 8 bytes:
[channel_id:u32 BE][length:u32 BE] followed by an opaque payload. The
channels layer owns channel_id and length; the payload is the
handler's framing, carried transparently.
The 9-byte alternative ([channel_id:u32][stream_type:u8][length:u32])
was considered and rejected. The 9-byte format puts stream_type in the
channels header, which means the channels layer carries a concept it
doesn't own. For TTY this composes cleanly (the 9-byte header is TTY's
5-byte header with channel_id prepended), but for non-TTY handlers
(tunnel, call, SSH) the stream_type byte is dead weight — the
channels layer carries a byte it doesn't understand, and the handler
ignores a byte in a header it doesn't control.
The 8-byte format is uniform across all handlers: the channels layer
carries channel_id + length + opaque payload. Every handler parses
its own framing from the payload. The cost is that TTY's wire.rs is
called from a payload buffer rather than directly from the wire, and
the total header for a TTY chunk is 13 bytes (8 channels + 5 TTY)
instead of 9. The two length fields are close but not identical
(ch_len = tty_len + 5); for typical TTY chunks (4 KiB+), the 5-byte
overhead is ~0.1%, and the trade is clean separation of concerns. See
"Consequences" for the full cost/benefit.
The add/strip composition
Each layer has its own add/strip pair. The channels layer:
add_channel_id(channel_id, payload_bytes) -> chunk on write (prepends
the 8-byte header); strip_channel_id(chunk) -> (channel_id, payload_bytes) on read (strips the 8-byte header, returns the
payload). The handler layer (e.g. TTY) parses its own framing from the
payload bytes per its existing wire.rs. The handler doesn't know or
care that a channel_id was stripped before it saw the bytes.
The composition is uniform — the same shape at every level. This is
SSH's model (layered headers, each layer strips its own at its
boundary), applied to channels. A alk/channels-inside-alk/channels
recursive composition is the outer layer stripping its 8-byte header,
the inner layer parsing its own 8-byte header from the payload — same
code, same shape, each level.
Why this can land now
Three things changed since alknet ADR-071/074/077 were accepted:
- alknet ADR-092 landed
BiStreamas the handler leaf.accept_bi()returns aBiStream(a concreteAsyncRead + AsyncWritenewtype), not a split(SendStream, RecvStream)pair. The join moves into core's quinn/iroh/stream impls (once per source, invisible to handlers). This ADR's "every channel is aBiStream" is the channels-layer consequence of alknet ADR-092's handler-leaf decision — the research-then-sync pattern applied: alknet ADR-092 settled the transport leaf, this ADR settles the multiplexing layer above it. - Phase 7 fixed the TTY control channel at the TTY layer. The
STREAM_CONTROL = 3"bidirectional" flaw is fixed by splitting it intoSTREAM_CTRL_IN = 3/STREAM_CTRL_OUT = 4— inside TTY's 5-byte format, not at the channels layer. This removed the load-bearing reason for the channels layer to carrystream_type: the control bidirectionality fix is a TTY-internal concern, not a channels-layer concern. ADR-007's two-mode TTY design was motivated by the channels layer carrying control; with control moved inside TTY, the motivation dissolves. - No production constraint. The develop branch is a rewrite of
main (pre-alpha). The channels crate doesn't exist yet (per
alknet ADR-081, it's planned as
alknet-channels-core+alknet-channels-call). The decision is purely "what's cleanest," not "what's least disruptive." The 9-byte POC validated the per-channel_id/stream_typerouting mechanism; the 8-byte spec update changes the header before implementation begins.
What this ADR does NOT decide
- The add/strip API shape (built into read/write vs. a separate
utility): the stream-unification research proposed
add_channel_id/strip_channel_idas standalone functions. Ideally the header is built into the read/write path so the utility isn't needed at the handler boundary — but there may be a generalized reason to expose it (recursive composition, test helpers, the hub relay'schannel_idrewrite). The exact API shape is an implementation detail for the channels crate. The contract — the channels layer strips its 8-byte header on read and the handler parses its own framing from the payload — is decided here; the function surface is not. - TTY's
wire.rsadaptation: TTY'sChunkReadercurrently reads from anAsyncRead. Adapting it to read from a payload buffer (&[u8]orCursor<Bytes>) is a small, well-scoped change (the framing logic — stream_type constants, length validation, control message parsing — is unchanged). This is an implementation concern for the channels + TTY integration, not an architecture decision. In alktty,drive_sessionalready runs against aBiStreamwhoseAsyncReadyields the post-channels-strip payload bytes; the samewire.rscode runs in both modes. - Full channel-level flow-control windowing (alknet OQ-56): unchanged. The bounded-buffer backpressure (alknet ADR-076) is the v1 mechanism; full windowing is an additive extension that doesn't change the wire format.
Decision
1. The channels wire format is 8 bytes
[channel_id: u32 BE][length: u32 BE][payload bytes]
8 bytes of header, followed by length bytes of opaque payload. The
channels layer owns channel_id and length; the payload is the
handler's framing, carried transparently.
| field | offset | width | meaning |
|---|---|---|---|
channel_id |
0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as alk/call (alknet ADR-072). Channels 1..N are opened dynamically via channel/open (alknet ADR-073). |
length |
4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max MAX_CHUNK_LEN (16 MiB, matching TTY's cap — ADR-001 §5). |
The stream_type byte is removed from the channels header. The
channels layer has no stream_type concept — not in its header, not in
its code, not in its mental model. What was the channels header's
stream_type byte is now the first byte of the payload, owned by the
handler's framing (TTY's 5-byte format, call's length-prefixed JSON,
tunnel's raw bytes, SSH's channel protocol).
This amends alknet ADR-071: the wire format is 8 bytes, not 9; the
stream_type decomposition (mod 3, unidirectional halves, 85 groups)
is removed from the channels layer. The stream_type concept survives in
TTY's 5-byte format (ADR-001, amended by Phase 7), which the channels
layer carries transparently.
2. into_sub_streams() is removed; accept_bi is the only accessor
alknet ADR-074's into_sub_streams() / ChannelSubStreams /
SubStreamHandle are removed. The channels layer exposes one accessor:
accept_bi(), which yields one BiStream per channel (per alknet
ADR-092). Every handler — TTY, tunnel, SSH, call — receives a
Connection, calls accept_bi() once, gets a BiStream, and
sub-multiplexes it however it wants.
This amends alknet ADR-074: the two-accessor design (accept_bi for
generic handlers, into_sub_streams for typed handlers) collapses to
one accessor. The "typed handler path" (alknet ADR-074's motivating
case for TTY) is replaced by TTY sub-demuxing its BiStream via its
own 5-byte format — the same code TTY runs in direct mode. alknet
ADR-074's yield-once accept_bi contract is preserved; the
into_sub_streams() accessor is the amended part.
3. TTY always uses its 5-byte format; the channels layer carries it transparently
ADR-007's two-mode TTY design (direct vs inside-channels) is reversed.
TTY's 5-byte format ([stream_type:u8][length:u32][payload], ADR-001)
is TTY's internal format, used in both direct mode and
inside-channels mode. The two modes differ only in where the
BiStream comes from (a top-level alk/tty connection vs a
channel/open with ALPN alk/tty), not in how TTY parses it. The
same wire.rs code runs in both modes.
When TTY is inside channels, the channels layer strips its 8-byte
header and hands TTY the payload bytes. TTY parses its 5-byte header
from the payload. The channels layer carries TTY's 5-byte chunks
transparently in its payload — no shared fields, no leaked
abstraction, no double-chunking concern (the 13-byte total header is
8 channels + 5 TTY, not 8 + 9; the channels length is always
tty_len + 5).
This reverses ADR-007: the 5-byte format is NOT scoped to direct —
it's TTY's internal format, carried transparently in the channels
payload. The channels feature on alktty (if added) becomes "run
TTY's sub-demux on a channels-backed BiStream" — the same code as
direct mode, different BiStream source. The control channel split
(STREAM_CTRL_IN / STREAM_CTRL_OUT, Phase 7) is TTY-internal; the
channels layer doesn't know about it.
4. The add/strip composition
The channels layer's read path strips the 8-byte header and hands the
payload to the handler. The write path prepends the 8-byte header
(add_channel_id) onto the handler's output. The handler never sees
the channel_id; it sees only its own framing (the payload bytes).
channels: [channel_id:u32 BE][length:u32 BE][payload]
= 8-byte header + opaque payload
8 bytes
TTY inside channels:
[channel_id:u32][ch_len:u32][stream_type:u8][tty_len:u32][payload]
4 bytes 4 bytes 1 byte 4 bytes N bytes
\_________ __________/ \_________ _____________/
| |
channels header TTY chunk (5+N bytes)
(8 bytes) carried as channels payload
The composition is uniform — the same shape at every level. A
alk/channels-inside-alk/channels recursive composition is the
outer layer stripping its 8-byte header, the inner layer parsing its
own 8-byte header from the payload — same code, same shape, each
level.
5. What does NOT change
- alknet ADR-092's
BiStreamleaf — unchanged. This ADR is the channels-layer consequence of alknet ADR-092:accept_biyields aBiStream, handlers sub-multiplex it. The two ADRs compose (alknet ADR-092 settles the transport leaf; this ADR settles the multiplexing layer above it). ProtocolHandlertrait shape (alknet ADR-002) — unchanged. Handlers receive aConnectionand callaccept_bi().- Channel 0 pre-negotiated as
alk/call(alknet ADR-072) — unchanged. Channel 0's chunks havechannel_id = 0in the 8-byte header. The call protocol'sEventEnvelopeframing is the payload; the channels layer carries it transparently. - Channel lifecycle operations (alknet ADR-073) — unchanged. The
four operations (
channel/open/close/control/resources/subscribe) and theirdirectionsemantics are call-protocol operations on channel 0, not channels-wire-format concerns. ChannelsAdapter/ChannelManagersplit (alknet ADR-075) — structurally unchanged. The demux loop reads 8-byte headers (not 9-byte); theChannelManageris ALPN-blind, auth-blind, transport-blind. Thestream_typesfield onchannel/openandChannelStateis removed (the channels layer doesn't track per-stream-type reassembly buffers; it tracks one reassembly buffer perchannel_id, yielding aBiStream).- Backpressure, channel limits, ID reuse (alknet ADR-076) —
unchanged. The bounded-buffer backpressure is per-
channel_id(was per-(channel_id, stream_type); now per-channel_idsince there's one reassembly buffer per channel). The 256-channel cap, 1 MiB default, and monotonic-ID-with-wrap strategy are unchanged. - Two-pump shutdown-on-completion (alknet ADR-078) — unchanged.
Tunnel/SSH handlers call
tokio::io::split(bidi)for their two pump halves; the shutdown-on-completion contract applies to theReadHalf/WriteHalfunchanged. - Hub relay (alknet ADR-079) — unchanged in contract. The hub
translates
channel/openon channel 0 and byte-forwards data channels withchannel_idrewrite. The relay reads 8-byte headers (not 9-byte) and rewrites thechannel_idfield (a 4-byte rewrite within the 8-byte header, not a 9-byte header). The relay does not parse the payload. ChannelClient(alknet ADR-080) — unchanged in API.from_connectionprimary,open_channelreturns aChannel. Thestream_typesfield onopen_channelandChannelis removed (the channels layer doesn't negotiate per-stream-type sets; the handler owns its sub-stream multiplexing). Thechannel:stream_type_unavailableerror code is removed (the channels layer can't refuse astream_typeit doesn't know about).- Sub-crate decomposition (alknet ADR-081) — unchanged.
channels-core(pure multiplexer, depends on alknet-core only) /channels-call(channel 0 pre-negotiation + lifecycle op registrations, depends onchannels-core+ alknet-call). The 8-byte wire format, demux/mux, andChannelBidiStreamSourceare inchannels-core; the call-protocol coupling is inchannels-call. BidiStreamSourcetrait (alknet ADR-070) — unchanged in shape.ChannelBidiStreamSourceimplements it;accept_biyields aBiStream(per alknet ADR-092, already landed).
Consequences
Positive:
- Clean separation of concerns. The channels layer has no
stream_typeconcept — not in its header, not in its code, not in its mental model. The handler owns its framing entirely. This dissolves the mod 2/mod 3/mod 4 question at the channels layer (there's nothing to decompose) and fixes the "control isn't actually bidirectional" TTY flaw at the TTY layer (where it lives, not the channels layer). - Uniform across all handlers. Tunnel, call, SSH, and TTY all
receive the same shape: a
BiStream. No handler gets astream_typebyte it doesn't use; no handler needs a second accessor (into_sub_streams) to reach its sub-streams. The channels layer's API surface isaccept_bi -> BiStream, period. - Recursive composition is literal. A
alk/channelschannel runs another channels demux on itsBiStream. The outer layer strips its 8-byte header; the inner layer parses its own 8-byte header from the payload. Same code, same shape, each level. This is a property, not a feature — the primary use case is one level of multiplexing, but the add/strip composition makes the recursion cleaner than alknet ADR-071's group framing did. - The
into_sub_streams()accessor and its consuming handler code are removed. This is a net simplification: one accessor, one handler path, no downcast / extension trait / "two paths" ergonomics question (which alknet ADR-074 left as an implementation detail). The handler crate destructures itsBiStreamvia its own framing (TTY's 5-byte format), not via a channels-crate-provided typed accessor. - TTY's
wire.rsruns unchanged in both modes. Direct mode and inside-channels mode use the same code; only theBiStreamsource differs. ADR-007'sdrive_session_direct/drive_session_channelssplit collapses to onedrive_sessionfunction. Thechannelsfeature on alktty (if added) becomes a thin wrapper that gets theBiStreamfrom a channels-backedConnectioninstead of a top-level one. (In alktty as built,drive_sessionalready takes a genericAsyncRead + AsyncWritepair, so both the directTtyAdapter::handlepath and the channelsTtyOpenHandlerpath call the same function — seesrc/adapter.rsandsrc/channels.rs.) - The channels layer is WASM-compatible by construction. The
8-byte header's core is pure byte manipulation (the sync core
compiles under
wasm32-unknown-unknown, validated by the POC). The 8-byte format is simpler than the 9-byte (one fewer field to parse), strengthening the WASM-clean property.
Negative:
- 5 extra bytes per TTY chunk. The total header for a TTY chunk
inside channels is 13 bytes (8 channels + 5 TTY), not 9. The two
length fields are close but not identical (
ch_len = tty_len + 5). For typical TTY chunks (4 KiB+), this is ~0.1% overhead. For extreme multiplexing scenarios, the clean separation is worth the trade-off; for high-throughput bulk transfer, the escape hatch is multi-connection (one channels connection per leg), not stripping the header. This is the documented cost of the clean separation; the alternative (9-byte header withstream_typein the channels layer) carries a concept the channels layer doesn't own, which is the root cause this ADR addresses. - TTY's
wire.rsneeds a small adaptation.ChunkReadercurrently reads from anAsyncRead(the transport stream). Inside channels, it reads from a payload buffer (&[u8]orCursor<Bytes>) — the bytes the channels layer handed it after stripping its 8-byte header. The framing logic (stream_type constants, length validation, control message parsing) is unchanged. This is a bounded, well-scoped implementation change, not an architecture change. The same adaptation applies to any handler that parses its own framing from a payload buffer (call'sEventEnvelopeframing already reads from a buffer; tunnel and SSH don't parse the payload, so no adaptation). In alktty as built,drive_sessionalready reads from aBiStreamwhoseAsyncReadyields the post-channels-strip payload bytes — the samewire.rscode runs unchanged in both modes. channel/openloses thestream_typesfield. alknet ADR-073'schannel/openinput includedstream_types: [u8](the active sub-stream set) and the response echoed the negotiated set. Under this ADR, the channels layer doesn't negotiate sub-stream sets — the handler owns its sub-stream multiplexing. Thestream_typesfield is removed fromchannel/open(and from thechannel:stream_type_unavailableerror code). Thealpnandparamsfields remain; the handler's sub-stream set is implicit in its ALPN's wire format. This is a small wire-format change tochannel/open(one field removed); since the channels crate isn't implemented yet, there's no migration cost.ChannelState.streams: HashMap<u8, ReassemblyBuffer>becomesChannelState.reassembly: ReassemblyBuffer(one per channel, not per(channel_id, stream_type)). This is an internal simplification (fewer reassembly buffers, simpler drain logic) but is an implementation change, not an architecture one. The bounded-buffer backpressure (alknet ADR-076) is per-channel_idnow, not per-(channel_id, stream_type)— the 1 MiB default and the 256-channel cap are unchanged; the per-channel memory ceiling is 1 MiB (was up to 5 MiB for a TTY channel with 5 active stream_types). This is a net improvement (lower memory ceiling per channel), not a regression.
Door type
One-way (wire format, accessor removal, two-mode reversal). The
8-byte chunk header layout (channel_id:u32 + length:u32), the
removal of stream_type from the channels header, and the removal of
into_sub_streams() are wire-format and API commitments. Changing
them after the channels crate is implemented and handlers are written
against them requires a version migration. Since the channels crate
doesn't exist yet, the one-way door is being cast now, before
implementation — the right time to cast a one-way door.
The reversal of ADR-007 (TTY always uses its 5-byte format) is one-way
in the same sense: once TTY's wire.rs runs in both modes (direct and
inside-channels), re-introducing a separate inside-channels mode would
be a rewrite of TTY's session driver. The trade is one unified session
driver now vs. two-mode maintenance forever.
The add/strip API shape (alknet OQ-68) is a two-way door — whether the header add/strip is built into the read/write path or exposed as a standalone utility is an implementation detail that can change without breaking the wire format or the handler contract.
References
- alknet ADR-071: channels wire format (amended — wire format is 8
bytes, not 9;
stream_typeremoved from the channels header; the stream_type decomposition is removed from the channels layer) - alknet ADR-074: ChannelBidiStreamSource (amended —
into_sub_streams()removed;accept_biis the only accessor, yields oneBiStreamper channel) - ADR-007: TTY inside channels (reversed
— TTY always uses its 5-byte format; the channels layer carries it
transparently in the payload; the two-mode design is preserved but
differs only in
BiStreamsource, not in parsing) - alknet ADR-092:
BiStreamas the handler leaf (the transport-leaf layer this ADR builds on —accept_bireturnsBiStream;from_bidiis the only public stream constructor) - alknet ADR-070:
BidiStreamSourcetrait (the extension pointChannelBidiStreamSourceimplements;accept_biyieldsBiStream) - alknet ADR-072: channel 0 pre-negotiated
alk/call(unchanged — channel 0's chunks havechannel_id = 0in the 8-byte header; the call protocol's framing is the payload) - alknet ADR-073: channel lifecycle operations (amended —
stream_typesfield removed fromchannel/open;channel:stream_type_unavailableerror code removed) - alknet ADR-075:
ChannelsAdapterandChannelManager(structurally unchanged — demux reads 8-byte headers; one reassembly buffer per channel) - alknet ADR-076: backpressure, channel limits, ID reuse (unchanged —
bounded-buffer is per-
channel_id; 256-channel cap, 1 MiB default, monotonic IDs) - alknet ADR-078: two-pump shutdown-on-completion (unchanged — the
contract applies to
tokio::io::split(bidi)halves) - alknet ADR-079: hub relay (unchanged in contract — 8-byte header,
4-byte
channel_idrewrite, payload byte-forwarded) - alknet ADR-080:
ChannelClient(amended —stream_typesfield removed fromopen_channelandChannel) - alknet ADR-081: sub-crate decomposition (unchanged — 8-byte wire
format in
channels-core; call-protocol coupling inchannels-call) - ADR-001: alktty wire format (the 5-byte format carried transparently in the channels payload; the control channel split from Phase 7 is TTY-internal)
src/channels.rs— alktty's channels integration (theregister_openablehelper +TtyOpenHandlerthat runsdrive_sessionon a channels-backedBiStream, the same code as the direct path)- Port origin: alknet ADR-093 at
/workspace/@alkdev/alknet/docs/architecture/decisions/093-channels-pure-channel-multiplexing.md