docs(research): add stream-unification findings — the leaf, the instance, the convergence
Captures the deep dive that started as the alknet-crate-extraction Phase 6 tangle and surfaced a layered issue: 1. Transport leaf split (ADR-092, drafted+pushed separately) — accept_bi returns BiStream, from_stream removed, from_bidi is the only public stream constructor. Load-bearing, separable. 2. Control channel 'isn't actually bidirectional' in TTY code (wire.rs STREAM_CONTROL=3 one stream both sides write). ADR-071 already fixes at wire-format level (3=ctrl_in, 4=ctrl_out); TTY code lags. 3. ADR-071's mod-3 stream_type decomposition is structural but asymmetric (stderr baked into group shape). Cleaner framing is mod 2/mod 4 by instance: an instance is a bidirectional unit addressed as a contiguous block of stream_types. No control: 128 instances/channel (mod 2). With control: 64 instances/channel (mod 4). Combined address space ~255*128 or ~255*64. 4. TTY and channels should converge on one format. Channels was written after TTY as a natural extension (5-byte + channel_id:u32 = 9-byte). Whether TTY-direct retires the 5-byte format is a bigger call — backward compat — flagged as POC candidate. 5. Recursive multiplexing follows: each instance can be a channels connection. Unbounded, uniform per level via the instance framing. Following the research-then-sync pattern: iterate here, fix inter-document drift, sync to specs only after it settles. ADR-092 is pushed because it's load-bearing and separable; ADR-093 (multiplexing redesign) and ADR-094 (retire 5-byte format) draft after POCs validate. POC candidates (ordered by leverage): - POC 1: stderr split/recombine (load-bearing for mod 2 vs mod 3) - POC 2: TTY-direct-as-channels (load-bearing for format convergence) - POC 3: recursive composition (low leverage, deferred)
This commit is contained in:
1 parent
528cfa0367
commit
909935ded3
1 file changed
+500
@@ -0,0 +1,500 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-18
|
||||
---
|
||||
|
||||
# stream-unification — Findings: the leaf, the instance, the convergence
|
||||
|
||||
**Status:** Draft findings, iterating. This doc is the working scratch
|
||||
for the stream/multiplexing redesign that surfaced during the
|
||||
`alknet-crate-extraction` Phase 6 deep dive. It is **not** synced to the
|
||||
architecture specs — per the research-then-sync pattern, we iterate
|
||||
here, fix inter-document drift, and only then sync to
|
||||
`docs/architecture/` and the ADRs. ADR-092 (`BiStream` as the handler
|
||||
leaf) has been drafted and pushed because it's load-bearing and
|
||||
separable; everything else in this doc is pre-ADR and will produce
|
||||
ADR-093 (the multiplexing/stream_type redesign) and possibly ADR-094
|
||||
(retire the 5-byte TTY-direct format) once it settles.
|
||||
|
||||
**Date:** 2026-07-18
|
||||
**Scope:** (a) the transport-leaf unification (ADR-092, drafted); (b)
|
||||
the stream_type / multiplexing redesign that ADR-092 enables; (c) the
|
||||
TTY/channels convergence; (d) the POC candidates that would take
|
||||
guesswork out before committing.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
The deep dive surfaced a layered tangle:
|
||||
|
||||
1. **Transport leaf is split.** `accept_bi` returns `(SendStream,
|
||||
RecvStream)`; every handler that wants a duplex stream re-joins
|
||||
(HTTP's `QuicStream`, 44 lines) or bypasses (WS's `WsStream`).
|
||||
Five abstractions exist for "a bidirectional byte stream."
|
||||
**Resolved by ADR-092** (drafted, pushed): `accept_bi` returns one
|
||||
`BiStream`; the join moves into core's quinn/iroh impls once; the
|
||||
split never crosses a crate boundary as part of a constructor
|
||||
(`Connection::from_stream` removed, `from_bidi` is the only public
|
||||
stream constructor).
|
||||
|
||||
2. **The control channel "isn't actually bidirectional"** in the
|
||||
current TTY code (`wire.rs:13,32-33`: `STREAM_CONTROL = 3` is one
|
||||
stream_type both sides write to). ADR-071's mod-3 stream_type
|
||||
decomposition already fixes this at the wire-format level
|
||||
(stream_types 3 = ctrl_in, 4 = ctrl_out), but the TTY code hasn't
|
||||
been updated. ADR-077 amends ADR-052 to add stream_type 4, but the
|
||||
implementation lags.
|
||||
|
||||
3. **ADR-071's mod-3 stream_type decomposition is structural but
|
||||
asymmetric.** "Data group 0/1/2 = in/out/err; control group 3/4/5 =
|
||||
ctrl_in/ctrl_out/ctrl_err" bakes stderr into the group structure.
|
||||
The cleaner framing is **mod 2 by instance**: an *instance* is a
|
||||
self-contained bidirectional unit, addressed as a contiguous block
|
||||
of stream_types. No control: instance K uses `2K`/`2K+1` (128
|
||||
instances/channel). With control: instance K uses
|
||||
`4K`/`4K+1`/`4K+2`/`4K+3` (64 instances/channel). The stream_type
|
||||
space is the *instance address space*, not a *role* space. This is
|
||||
richer than "control is just another pair" — the instance is the
|
||||
addressing unit.
|
||||
|
||||
4. **TTY and channels should converge on one format.** Channels was
|
||||
written after TTY and was a natural extension — TTY's 5-byte header
|
||||
+ a `channel_id:u32` prefix = the 9-byte channels header. TTY
|
||||
should be rewritten to use the channels format. The one hardcoded
|
||||
`channel_id` is channel 0 = `alknet/call` (ADR-072); everything
|
||||
else is dynamic. Whether TTY-direct retires the 5-byte format
|
||||
entirely (using 9-byte with `channel_id = 0` or `channel_id = 1`)
|
||||
is a bigger call — backward compat — flagged as a POC candidate
|
||||
and a possible ADR-094.
|
||||
|
||||
5. **The recursive multiplexing property follows.** A channels
|
||||
connection with N channels, each with up to 128 (or 64) instances,
|
||||
has N×128 (or N×64) logical sub-streams. Combined address space
|
||||
(channel_id × instance) is ~255×128 or ~255×64. An instance can
|
||||
itself be a channels connection (recursive composition, ADR-074).
|
||||
The "channel within a channel within a channel" shape is unbounded,
|
||||
and the mod-2/mod-4 instance framing makes each level uniform.
|
||||
|
||||
**POC candidates** (ordered by leverage): stderr split/recombine
|
||||
(load-bearing for the mod-2 vs mod-3 decision); TTY-direct-as-channels
|
||||
(load-bearing for the format-convergence decision); recursive
|
||||
composition (low leverage — already implied by the abstraction, just
|
||||
needs validation).
|
||||
|
||||
---
|
||||
|
||||
## The tangle, restated
|
||||
|
||||
Five abstractions for "a bidirectional byte stream" before ADR-092:
|
||||
|
||||
| # | Abstraction | Where | Status |
|
||||
|---|-------------|-------|--------|
|
||||
| 1 | `BiStream` trait (`AsyncRead + AsyncWrite + Send + Unpin`) | `crates/alknet-core/src/types.rs:226` | Vestigial in code — zero consumers. Resurrected by ADR-092 as a concrete newtype. |
|
||||
| 2 | `Connection` (yields `(SendStream, RecvStream)` via `accept_bi`) | `crates/alknet-core/src/types.rs:507` | Handler-facing. Leaf split. ADR-092 changes `accept_bi` to return `BiStream`. |
|
||||
| 3 | `SendStream` (AsyncWrite-only) + `RecvStream` (AsyncRead-only) | `crates/alknet-core/src/types.rs:228-294` | Quinn-welded enums. ADR-092 collapses to thin newtypes, retained for `into_sub_streams()`. |
|
||||
| 4 | `WsStream` trait (recv/send `axum::ws::Message`) | `crates/alknet-http/src/websocket/upgrade.rs:44` | Bypasses `Connection`. ADR-092 replaces with `WsBidiStream` through `Connection::from_bidi`. |
|
||||
| 5 | `MpscSendStream` / `MpscRecvStream` (channels POC) | `/workspace/alknet-channels-poc/src/mpsc_stream.rs` | Split mpc-backed halves. ADR-092: channels crate joins via `tokio::io::join` before `from_bidi`. |
|
||||
|
||||
The two `findings.md` Phase 6 issues (QuicStream wrapper, WS bespoke
|
||||
dispatch) are symptoms of the split leaf. ADR-092 addresses the
|
||||
transport seam; this doc addresses the multiplexing seam that ADR-092
|
||||
exposes.
|
||||
|
||||
---
|
||||
|
||||
## ADR-092 recap (what's settled)
|
||||
|
||||
- `accept_bi` / `open_bi` return `BiStream`, not `(SendStream,
|
||||
RecvStream)`.
|
||||
- `BiStream` is a concrete newtype (internal `Box<dyn AsyncReadWrite +
|
||||
Send + Unpin>`), not the bare ADR-007 trait (which is removed; the
|
||||
bounds survive as implied bounds).
|
||||
- The join moves into core's quinn/iroh `BidiStreamSource` impls once
|
||||
(via `tokio::io::join` or equivalent).
|
||||
- `Connection::from_bidi` is the only public stream constructor.
|
||||
`Connection::from_stream(send, recv, ...)` is removed — the split
|
||||
never crosses a crate boundary as part of a constructor.
|
||||
- `SendStream` / `RecvStream` collapse to thin newtypes over
|
||||
`Box<dyn Async* + Send + Unpin>`, retained only for `into_sub_streams`
|
||||
(ADR-074) and the channels reassembly path's `SubStreamHandle` leaves.
|
||||
- `HttpAdapter::handle` drops `QuicStream` (44 lines) and
|
||||
`QuicStreamDuplex` (38 lines).
|
||||
- WS runs through `Connection::from_bidi(WsBidiStream)` + the
|
||||
call-protocol handler; `WsStream` trait, `drive_ws_session` loop,
|
||||
and ~150 lines of dispatch glue removed.
|
||||
- "VPN-like without being a VPN" over WS in v1 becomes real
|
||||
(`webtransport.md`'s path, over WS).
|
||||
- WebTransport h3 extraction is recorded as a future channels-variant
|
||||
move enabled by the unification (out of scope per ADR-044).
|
||||
|
||||
ADR-092 is pushed (`f8d4650`, amended `528cfa0`). It is load-bearing
|
||||
and separable from the multiplexing redesign below.
|
||||
|
||||
---
|
||||
|
||||
## What ADR-092 exposes: the multiplexing seam
|
||||
|
||||
With the transport leaf unified, the remaining asymmetry is in the
|
||||
*stream_type* space — the multiplexing layer above the transport.
|
||||
|
||||
### ADR-071's mod-3 decomposition (current)
|
||||
|
||||
ADR-071 groups stream_types in threes:
|
||||
|
||||
| Group | stream_type | direction | purpose |
|
||||
|-------|-------------|-----------|---------|
|
||||
| Data | 0 | write | in (stdin) |
|
||||
| | 1 | read | out (stdout) |
|
||||
| | 2 | read | err (stderr) |
|
||||
| Control | 3 | write | ctrl_in |
|
||||
| | 4 | read | ctrl_out |
|
||||
| | 5 | read | ctrl_err (optional) |
|
||||
| Future | 6/7/8, 9/10/11, ... | write/read/read | next groups |
|
||||
|
||||
Formula: `stream_type % 3 == 0` → write, `% 3 == 1` → read,
|
||||
`% 3 == 2` → diagnostic read.
|
||||
|
||||
**The flaw: stderr is structural.** The "err is the third half"
|
||||
bakes stderr into the group shape. The "group" concept is real (data
|
||||
vs control) but the `+2 = err` slot is asymmetric — a bidirectional
|
||||
thing is two halves, but the group has three slots, one of which is
|
||||
"err." This forces every group to either allocate an err slot it may
|
||||
not use (tunnel has no err) or skip the group structure entirely.
|
||||
|
||||
### The control channel flaw in the TTY code
|
||||
|
||||
`crates/alknet-tty/src/wire.rs:13,32-33`:
|
||||
```
|
||||
STREAM_CONTROL: u8 = 3 // "bidirectional, JSON control message"
|
||||
InvalidStreamType > 3
|
||||
```
|
||||
|
||||
One stream_type both sides write to. That's the "control isn't
|
||||
actually bidirectional" flaw ADR-071 §stream_type decomposition calls
|
||||
out: *one* stream both sides write loses independent flow control,
|
||||
independent EOF, and clean separation. ADR-077 amends ADR-052 to add
|
||||
stream_type 4 (ctrl_out), splitting control into 3 (in) + 4 (out).
|
||||
The TTY code hasn't been updated — it still has the old "bidirectional
|
||||
3" comment and the `> 3` bound. The update is needed regardless of
|
||||
the mod-2/mod-3 decision.
|
||||
|
||||
### The implicit split (the key insight)
|
||||
|
||||
Bidirectionality is *always* two unidirectional halves. There is no
|
||||
"bidirectional stream_type" — bidirectionality is two stream_types
|
||||
(write, read), the same way QUIC bidi streams are two unidirectional
|
||||
halves, the same way `tokio::io::split(BiStream)` is two halves, the
|
||||
same way `SendStream`/`RecvStream` after ADR-092 is two halves. The
|
||||
stream_type space is *inherently* unidirectional; the bidirectional
|
||||
thing is the pair.
|
||||
|
||||
This means: TTY and channels present the same shape to their
|
||||
handlers. A set of unidirectional sub-streams, paired into
|
||||
bidirectional channels where the handler wants a joined pair. TTY
|
||||
wants five unidirectional leaves (`stdin`, `stdout`, `stderr`,
|
||||
`ctrl_in`, `ctrl_out`); the tunnel wants two paired into one
|
||||
`BiStream` (`tokio::io::join(send_0, recv_1)`). The handler chooses
|
||||
how to compose the leaves; the channels layer just exposes them.
|
||||
|
||||
---
|
||||
|
||||
## The instance framing (mod 2 / mod 4)
|
||||
|
||||
An **instance** is a self-contained bidirectional unit, addressed as
|
||||
a contiguous block of stream_types within a channel.
|
||||
|
||||
**No control channel** — mod 2:
|
||||
|
||||
| Instance | in (write) | out (read) |
|
||||
|----------|-----------|-----------|
|
||||
| 0 | 0 | 1 |
|
||||
| 1 | 2 | 3 |
|
||||
| 2 | 4 | 5 |
|
||||
| ... | | |
|
||||
| 127 | 254 | 255 |
|
||||
|
||||
128 instances per channel. Instance K uses stream_types `2K` (in) and
|
||||
`2K+1` (out).
|
||||
|
||||
**With control channel** — mod 4:
|
||||
|
||||
| Instance | in (write) | out (read) | ctrl_in (write) | ctrl_out (read) |
|
||||
|----------|-----------|-----------|-----------------|-----------------|
|
||||
| 0 | 0 | 1 | 2 | 3 |
|
||||
| 1 | 4 | 5 | 6 | 7 |
|
||||
| 2 | 8 | 9 | 10 | 11 |
|
||||
| ... | | | | |
|
||||
| 63 | 252 | 253 | 254 | 255 |
|
||||
|
||||
64 instances per channel. Instance K uses stream_types `4K` (in),
|
||||
`4K+1` (out), `4K+2` (ctrl_in), `4K+3` (ctrl_out).
|
||||
|
||||
**The stream_type space is the instance address space, not a role
|
||||
space.** A handler talks about "instance K of my channel" as a
|
||||
contiguous block of stream_types. This is richer than ADR-071's "data
|
||||
group / control group" framing — the instance is the addressing unit,
|
||||
and the handler can have N independent bidirectional sub-streams on
|
||||
one channel, each with or without its own control.
|
||||
|
||||
**Combined address space:** channel_id × instance. Channel 0 is
|
||||
hardcoded as `alknet/call` (ADR-072), so channels 1..255 are dynamic
|
||||
— ~255 × 128 (no control) or ~255 × 64 (with control) logical
|
||||
sub-streams per channels connection. That's a lot, and it's the
|
||||
load-bearing count for the "recursive multiplexing" point below.
|
||||
|
||||
**The "ignore stderr for a moment" resolution.** Stderr is the one
|
||||
feature that breaks the pure mod-2 framing — it's a unidirectional
|
||||
read (server→client), not a bidirectional pair. Three options:
|
||||
|
||||
1. **Stderr as its own instance (mod 2).** Instance 0 = io, instance
|
||||
1 = stderr-as-bidirectional (write half unused, declared but never
|
||||
written). The demux routes by `(channel_id, stream_type)`
|
||||
regardless of convention; the per-channel declaration at
|
||||
`channel/open` time (ADR-073 already has this field) is the only
|
||||
truth. PTY mode (stderr merged) declares instance 0 only; pipe
|
||||
mode declares instances 0 and 1. One unused stream_type per
|
||||
stderr-carrying channel; cheap.
|
||||
2. **Stderr as a unidirectional leaf (the asymmetric case).** The
|
||||
per-channel declaration allows unidirectional leaves — not every
|
||||
stream_type has to be part of a pair. Stderr rides a single
|
||||
stream_type (e.g., type 5), declared read-only. The demux doesn't
|
||||
care; the convention is "pairs are the default, unidirectional
|
||||
leaves are explicitly declared." This keeps mod 2 as the
|
||||
bidirectional convention without forcing stderr into a pair.
|
||||
3. **Combine stderr into stdout (the Docker PTY choice).** Always
|
||||
merge; lose separate stderr. This loses the pipe/non-PTY mode
|
||||
feature — `tty-local/pipe.rs:363 separate_stderr` test asserts
|
||||
stderr arrives as distinct chunks; the Docker `exec`/`attach` API
|
||||
exposes `Tty: true` (merge) vs `Tty: false` (separate). Not
|
||||
acceptable for the non-PTY use case.
|
||||
|
||||
**The stderr question is load-bearing for mod 2 vs mod 3.** If
|
||||
stderr-as-its-own-instance (option 1) is clean, mod 2 wins — every
|
||||
bidirectional thing is a pair, stderr is a pair with one unused half,
|
||||
no asymmetry. If the unused half is a real wart, the per-channel
|
||||
declaration with unidirectional leaves (option 2) wins, and the
|
||||
mod-2/mod-3 distinction is less load-bearing than "the declaration is
|
||||
the truth, the convention is a hint." This is a POC candidate (see
|
||||
below).
|
||||
|
||||
---
|
||||
|
||||
## TTY/channels convergence
|
||||
|
||||
Channels was written after TTY and was a natural extension: TTY's
|
||||
5-byte header + `channel_id:u32` prefix = the 9-byte channels header.
|
||||
The convergence has two parts.
|
||||
|
||||
### Format convergence
|
||||
|
||||
TTY-direct uses the 5-byte format (ADR-052, scoped to direct by
|
||||
ADR-077). Channels uses the 9-byte format (ADR-071). TTY-inside-channels
|
||||
uses the 9-byte format. The 5-byte format is the 9-byte format with
|
||||
the `channel_id` prefix elided (because TTY-direct has only one
|
||||
channel).
|
||||
|
||||
**Option A (current, ADR-077): two formats coexist.** TTY-direct keeps
|
||||
the 5-byte format; TTY-inside-channels uses 9-byte. The 4-byte
|
||||
`channel_id` overhead is paid only inside channels.
|
||||
|
||||
**Option B (retire the 5-byte format): TTY-direct uses 9-byte with
|
||||
`channel_id = 0`.** One demux implementation, one set of stream_type
|
||||
semantics, one crate. The 4-byte overhead per chunk is noise for the
|
||||
TTY use case (terminal I/O, not bulk transfer). But: backward-compat
|
||||
with existing TTY-direct deployments, and the "channel 0 is always
|
||||
call" rule (ADR-072) conflicts — TTY-direct's single channel is
|
||||
`alknet/tty`, not `alknet/call`. Resolutions:
|
||||
- **B1:** TTY-direct is a channels connection with `channel_id = 0` =
|
||||
`alknet/tty` (breaking ADR-072's "channel 0 is call" rule).
|
||||
- **B2:** TTY-direct is a channels connection with `channel_id = 0` =
|
||||
`alknet/call` (unused, no `channel/open` issued) + `channel_id = 1` =
|
||||
`alknet/tty` (pre-allocated). Pays one unused channel for rule
|
||||
consistency. Channel 0 is *always* call, even when unused.
|
||||
- **B3:** TTY-direct is not a channels connection at all; it keeps the
|
||||
5-byte format (Option A). The 9-byte format applies only inside
|
||||
channels. (Same as current ADR-077.)
|
||||
|
||||
**Option B is the bigger call.** It's a wire-format change with
|
||||
backward-compat implications. It's a POC candidate (validate B1/B2 in
|
||||
a minimal implementation) and a possible ADR-094. Not in scope for the
|
||||
mod-2/mod-4 stream_type redesign (ADR-093).
|
||||
|
||||
### Semantic convergence (independent of format)
|
||||
|
||||
Regardless of Option A/B, TTY and channels should present the same
|
||||
shape to their handlers: a set of unidirectional sub-streams
|
||||
declared per-channel, paired into bidirectional instances where the
|
||||
handler wants a joined `BiStream`. The `into_sub_streams()` accessor
|
||||
(ADR-074) returns the declared stream_types as
|
||||
`Vec<(u8, SubStreamHandle)>` with `SubStreamHandle::Send` (write half)
|
||||
or `Recv` (read half). The handler joins the pairs it wants via
|
||||
`tokio::io::join`. The channels layer exposes the leaves; the handler
|
||||
composes them.
|
||||
|
||||
This means ADR-074's two paths (`accept_bi` for the joined 0/1 pair,
|
||||
`into_sub_streams` for the typed leaves) become the same data,
|
||||
presented differently. `accept_bi` is a convenience that joins 0/1 for
|
||||
the common case (tunnel, SSH, echo, HTTP); `into_sub_streams` is the
|
||||
primary accessor (the unidirectional leaves). Both are the same data;
|
||||
the handler chooses how to compose.
|
||||
|
||||
---
|
||||
|
||||
## The recursive multiplexing property
|
||||
|
||||
A channels connection with N channels, each with up to 128 (or 64)
|
||||
instances, has N×128 (or N×64) logical sub-streams. An instance can
|
||||
itself be a channels connection (recursive composition, ADR-074 — a
|
||||
channel with ALPN `alknet/channels` inside `alknet/channels`). The
|
||||
"channel within a channel within a channel" shape is unbounded, and
|
||||
the mod-2/mod-4 instance framing makes each level uniform — every
|
||||
level is "pairs of stream_types (or 4-tuples with control), declared
|
||||
per-channel, addressed by instance."
|
||||
|
||||
This is a property, not a feature. The primary use case is one level
|
||||
of multiplexing. Recursive composition is a natural consequence of the
|
||||
abstraction, not a goal. Recorded here because the instance framing
|
||||
makes it cleaner than ADR-071's group framing did — the instance is
|
||||
the recursive unit, the group was not.
|
||||
|
||||
---
|
||||
|
||||
## POC candidates
|
||||
|
||||
Ordered by leverage (the load-bearing unknowns that, once validated,
|
||||
make the ADR drafting mechanical rather than speculative).
|
||||
|
||||
### POC 1: stderr split/recombine (load-bearing for mod 2 vs mod 3)
|
||||
|
||||
**Question:** Can a `tokio::io::split` half be recombined with a
|
||||
different half cleanly? Specifically: if stderr is its own instance
|
||||
(mod 2, write half unused), does the unused write half cause any
|
||||
real problem — dangling sender, premature EOF on the read half,
|
||||
flow-control weirdness? Or does declaring "instance 1 = stderr,
|
||||
write half unused" just work?
|
||||
|
||||
**Why it matters:** If the unused write half is clean, mod 2 wins
|
||||
(every bidirectional thing is a pair, stderr is a pair with one
|
||||
unused half, no asymmetry, no mod 3). If it's a wart, the
|
||||
per-channel declaration with unidirectional leaves (option 2) wins,
|
||||
and the mod-2/mod-3 distinction is less load-bearing than "the
|
||||
declaration is the truth."
|
||||
|
||||
**Scope:** ~100 lines. A test-only POC in the channels POC's shape:
|
||||
declare a channel with two instances, instance 0 = io pair (types
|
||||
0/1), instance 1 = stderr (types 2/3, write half never written).
|
||||
Verify the read half on instance 1 delivers stderr chunks; the write
|
||||
half never errors; the demux doesn't choke on an unused write
|
||||
stream_type. Run it over the POC's `tokio::io::duplex` stand-in.
|
||||
|
||||
**Output:** A `docs/research/alknet-channels/poc-stderr-split.md`
|
||||
summary with a yes/no answer and the code path. If yes, ADR-093
|
||||
drafts with mod 2. If no, ADR-093 drafts with per-channel
|
||||
declaration as the primary, mod 2 as a convention hint.
|
||||
|
||||
### POC 2: TTY-direct-as-channels (load-bearing for format convergence)
|
||||
|
||||
**Question:** Is Option B1 (TTY-direct = channels with `channel_id = 0`
|
||||
= `alknet/tty`, breaking ADR-072) or Option B2 (channel 0 = call
|
||||
unused, channel 1 = TTY) feasible and clean? Or is the 4-byte
|
||||
`channel_id` overhead per chunk in TTY-direct mode a real concern
|
||||
for terminal I/O? And is the backward-compat with existing
|
||||
TTY-direct deployments a real constraint, or are there no existing
|
||||
deployments to break?
|
||||
|
||||
**Why it matters:** If Option B is clean and backward-compat is
|
||||
non-binding, the 5-byte format retires, the demux implementation
|
||||
unifies, and the TTY crate's direct mode becomes a thin wrapper. If
|
||||
backward-compat is binding or the overhead is a concern, Option A
|
||||
(two formats coexist per ADR-077) stands.
|
||||
|
||||
**Scope:** ~200 lines. A POC that runs the TTY adapter over a
|
||||
channels-format demux with `channel_id = 0` (B1) and `channel_id =
|
||||
1` (B2), using the existing TTY `TtyBackend` (the local pipe backend).
|
||||
Verify the `pty.rs` / `pipe.rs` tests still pass in shape. Measure
|
||||
chunk overhead if relevant.
|
||||
|
||||
**Output:** A `docs/research/alknet-channels/poc-tty-direct-as-channels.md`
|
||||
summary. Recommends Option A, B1, or B2. If B, becomes the input to
|
||||
ADR-094 (retire the 5-byte format). If A, ADR-077 stands and the
|
||||
5-byte format is kept.
|
||||
|
||||
### POC 3: recursive composition (low leverage, deferred)
|
||||
|
||||
**Question:** Does `alknet/channels` inside `alknet/channels` actually
|
||||
work end-to-end? The abstraction permits it (ADR-074); the POC didn't
|
||||
validate it (`poc-summary.md` §"What the POC Does NOT Validate" #5).
|
||||
|
||||
**Why it matters:** Low leverage — the instance framing makes it
|
||||
cleaner, but the primary use case is one level. Recursive composition
|
||||
is a property, not a feature.
|
||||
|
||||
**Scope:** Deferred. Not a POC for this round. Recorded for
|
||||
completeness.
|
||||
|
||||
---
|
||||
|
||||
## What goes where (ADR plan)
|
||||
|
||||
| ADR | Scope | Status |
|
||||
|-----|-------|--------|
|
||||
| **ADR-092** | `BiStream` as the handler leaf; `accept_bi` returns `BiStream`; `Connection::from_stream` removed; `from_bidi` is the only public stream constructor | **Drafted, pushed** (`f8d4650`, amended `528cfa0`). Load-bearing, separable. |
|
||||
| **ADR-093** | stream_type as the unidirectional leaf; mod 2 / mod 4 instance framing; per-channel stream_type declaration; `into_sub_streams()` as the primary accessor; demux convention-agnostic. Amends ADR-071 (wire format stream_type decomposition), ADR-074 (`into_sub_streams`), ADR-077 (TTY uses mod 2, control channel fixed). | **Pre-ADR**, drafting after POC 1 (stderr split) validates the mod-2 decision. |
|
||||
| **ADR-094** | Retire the 5-byte TTY-direct format in favor of 9-byte channels format with `channel_id = 0` (or `channel_id = 1` with channel 0 = call unused). Backward-compat analysis. | **Pre-ADR**, drafting after POC 2 (TTY-direct-as-channels) recommends Option B. If POC 2 recommends Option A, ADR-094 is not drafted and ADR-077 stands. |
|
||||
|
||||
ADR-092 is separable from 093/094 — it's the transport seam, not the
|
||||
multiplexing seam. ADR-093 is the multiplexing redesign that ADR-092
|
||||
exposes. ADR-094 is the format-convergence call that ADR-093 enables
|
||||
but doesn't require.
|
||||
|
||||
---
|
||||
|
||||
## Open questions (for the research iteration)
|
||||
|
||||
- **Stderr as instance vs unidirectional leaf.** POC 1 resolves this.
|
||||
Default assumption: stderr as instance (mod 2, unused write half is
|
||||
clean). POC validates.
|
||||
- **Mod 2 vs mod 3.** If POC 1 confirms the unused-write-half is clean,
|
||||
mod 2 wins. If not, the per-channel declaration becomes primary and
|
||||
the mod distinction is a hint, not a rule.
|
||||
- **5-byte format retirement.** POC 2 resolves this. Default
|
||||
assumption: Option A (two formats coexist, ADR-077 stands) is
|
||||
safer; Option B (retire 5-byte) is cleaner but a wire-format change.
|
||||
POC validates feasibility and overhead.
|
||||
- **`channel 0 is always call` (ADR-072) under TTY-direct-as-channels.**
|
||||
If POC 2 recommends B1 (TTY-direct channel 0 = `alknet/tty`), ADR-072
|
||||
needs an exception or amendment. If B2 (channel 0 = call unused,
|
||||
channel 1 = TTY), ADR-072 stands. If A (keep 5-byte), no change.
|
||||
- **Recursive composition.** Deferred. The instance framing makes it
|
||||
cleaner, but it's not a goal. POC 3 is low leverage.
|
||||
- **WS-as-BiStream for the browser WASM path.** ADR-092 records
|
||||
`WsBidiStream` as the server-side adapter; the browser-side adapter
|
||||
is a separate concern (the WASM SDK, not alknet-http). Where does
|
||||
the browser-side adapter live? Default: separate, not alknet-http.
|
||||
Resolved at implementation time.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- ADR-092: `BiStream` as the handler leaf (drafted, pushed)
|
||||
- ADR-071: channels wire format (the mod-3 stream_type decomposition
|
||||
ADR-093 amends)
|
||||
- ADR-074: `ChannelBidiStreamSource` / `into_sub_streams` (the
|
||||
per-channel sub-stream accessor ADR-093 amends)
|
||||
- ADR-077: TTY inside channels (the two-mode TTY design ADR-093
|
||||
amends; the 5-byte format scoping ADR-094 may retire)
|
||||
- ADR-070: `BidiStreamSource` trait (the extension point ADR-092
|
||||
amends)
|
||||
- ADR-065: `Connection::from_stream` / `from_bidi` (amended by
|
||||
ADR-092 — `from_stream` removed, `from_bidi` only)
|
||||
- ADR-078: two-pump shutdown-on-completion (preserved by ADR-092)
|
||||
- `docs/research/alknet-crate-extraction/findings.md` Phase 6 — the
|
||||
deferred `alknet-http` rework; ADR-092 resolves the deferral
|
||||
- `docs/research/alknet-channels/poc-summary.md` — the channels POC
|
||||
(28 tests, WASM compile check); the shape ADR-093 builds on
|
||||
- `crates/alknet-tty/src/wire.rs:13,32-33` — the `STREAM_CONTROL = 3`
|
||||
"bidirectional" flaw ADR-093 fixes
|
||||
- `crates/alknet-tty-local/tests/pipe.rs:363 separate_stderr` — the
|
||||
test that proves separate stderr is a real feature, not vestigial
|
||||
Reference in new issue
Block a user