Files
alkcall/docs/architecture/decisions/044-channels-subcrate-decomposition.md
T
glm-5.3-flash 5f019c02f1 docs: re-key architecture docs off the alknet mono-repo — speak as alkcall
The ported docs still framed alknet siblings (alknet-core/-call/-channels/
-http/-tls/-client) as live load-bearing context of this crate. With the
extraction complete and alknet headed for glue-level replacement, the docs
now speak as alkcall and cite the real sibling crates (alkhttp, alktls,
alktty, alktunnels, alktrader).

- ADR-054 (new): re-own the verifier-selection rule in-crate — three
  remote roles (public X.509 endpoint / transport relay / hub),
  ServerCertVerifier by PeerEntry presence, RemoteIdentity None
  load-bearing. Provenance: alknet ADR-034 (never ported), demoted to
  footnote. Resolves OQ-21's cross-repo authority gap.
- OQ-43 (new, deferred(scope)): composition-layer ownership and shape
  after the alknet replacement — the named receiver for the
  downstream-side deferrals (OQ-33 endpoint layer, OQ-41 QUIC-native
  substrate, OQ-42 hub broker, OQ-19 persistence adapters, ADR-045 dial
  seam). README layering diagram redrawn with the composition layer as
  TBD-ownership instead of "alknet / alknode" in concrete.
- Spec docs reframed: titles, self-references, dial/TLS/from_jsonschema
  homes, malformed legacy cite rows removed; dead relative research
  paths qualified as source-repo paths.
- ADR amendments (append-only): 027 (alknet-http -> alkhttp), 031 (post-
  extraction state), 044 (split landed as modules in alkcall), 045
  (name normalization; citation-space note).
- Citation hygiene: unprefixed/colliding source-repo cites prefixed
  ("alknet ADR-NNN (provenance)") across 009/011/022/034/035/038/040/
  043/044/045; stale OQ cites corrected (session registries OQ-04, batch
  semantics OQ-03, verifier rule ADR-054); stale anchors fixed.
- README provenance note consolidated; citation conventions documented.

Verification: markdown link check (all local links resolve, all in-crate
ADR cites point at existing decisions/ files); table integrity check
(uniform row widths across open-questions.md and edited tables); residual
alknet-mention sweep (only provenance/historical/intentional framing
remains); architecture review subagent pass with zero remaining criticals.
Docs-only change — no code paths touched.
2026-10-13 10:53:47 +00:00

13 KiB

ADR-044: alknet-channels Sub-Crate Decomposition

Status

Accepted (amended 2026-07-18 by ADR-035 — the 9-byte wire format is now 8-byte; ChannelSubStreams / SubStreamHandle removed; the channels layer has no stream_type concept — see "Amendment (ADR-035, 2026-07-18)" below)

Amendment (ADR-035, 2026-07-18)

The wire format in channels-core is now 8-byte (not 9-byte); the ChannelSubStreams / SubStreamHandle typed destructure accessor is removed (the channels layer has no stream_type concept — accept_bi yields a BiStream, and the handler owns its sub-stream multiplexing). The channel 0 pre-negotiation in channels-call no longer constructs "reassembly buffers with stream_types [0, 1]" — it constructs one reassembly buffer for channel_id = 0, yielding a BiStream to the CallAdapter. The "What moves where" table's "9-byte wire format" row is now "8-byte wire format"; the ChannelSubStreams row is removed. The two-crate split (channels-core / channels-call), the dep graph, and the "hub and worker are consumers" principle are unchanged. See ADR-035 for the resolution rationale.

Context

The initial channels spec (ADR-034-080) framed alknet-channels as a single crate depending on both alknet-core and alknet-call. The dependency on alknet-call arises because channel lifecycle operations (channel/open, channel/close, channel/control, channel/resources/subscribe) register on the call protocol's OperationRegistry, and channel 0 is pre-negotiated as alk/call (ADR-036).

This creates two issues:

  1. The dependency graph conflates the pure multiplexer with the call- protocol coupling. The wire format, demux/mux, and ChannelBidiStreamSource are ALPN-blind and call-protocol-blind — they depend on alknet-core only. The channel 0 pre-negotiation and lifecycle op registrations are the call-protocol coupling. Baking both into one crate means any consumer that wants the multiplexer also pulls in the call-protocol coupling, even if they don't need channel 0 to be alk/call.

  2. The "no special-casing for downstream crates" principle is violated. The user's constraint: "we don't want to be doing anything special for downstream crates unless it's needed/useful to the point of they all kind of benefit." The call-protocol coupling is a channels-crate concern (channels needs call for orchestration), not a downstream-crate concern. Separating them makes the dependency graph honest: the pure multiplexer has no opinion about channel 0; the call-protocol coupling is isolated where it belongs.

The substrate simplification (ADR-034 revised) strengthened this: the wire format is the same across all substrates, and the ChannelsAdapter is substrate-agnostic. The pure multiplexer (channels-core) is cleanly separable from the call-protocol orchestration (channels-call).

Decision

Two crates, not four

alknet-channels-core  — the pure multiplexer (wire format, demux/mux,
│                       ChannelBidiStreamSource, ChannelManager). Depends
│                       on alknet-core only. ALPN-blind, call-protocol-blind,
│                       transport-blind.
└── alknet-channels-call — channel 0 pre-negotiation + lifecycle op
                         registrations on the call protocol's
                         OperationRegistry. Depends on channels-core +
                         alknet-call.

There are no channels-hub or channels-worker sub-crates. The hub and worker are consumers of channels, not sub-crates of it. The existing alknet-hub crate (source-repo path: docs/architecture/crates/hub/README.md — alknet, provenance) IS the hub — it depends on channels-call and uses the channels protocol as its substrate. A worker is just a worker — it depends on channels-call and uses ChannelClient (ADR-043) to dial. The hub relay logic (ADR-042) lives in alknet-hub, not in a channels sub-crate.

alknet-channels-core

The pure multiplexer. Contains:

  • The 9-byte chunk wire format (parse_header / write_header — ADR-034)
  • The demux/mux (Demux, MuxHandle/MuxRunner — ADR-039)
  • ChannelBidiStreamSource (implements BidiStreamSource — ADR-038)
  • ChannelSubStreams / SubStreamHandle (the typed destructure accessor)
  • ChannelManager (the shared state — channel_id → ChannelState, but without the call_ops: Arc<OperationRegistry> field; the manager is ALPN-blind and call-protocol-blind)
  • ChannelsAdapter (the ProtocolHandler on alk/channels — the read/demux loop, substrate-agnostic per ADR-034 revised)

Depends on alknet-core only. No alknet-call dependency. No opinion about what channel 0 carries — that's the consumer's concern.

The ChannelsAdapter::handle in channels-core does NOT preinstall channel 0 as alk/call. It runs the demux loop and routes chunks by channel_id. Channel 0 is just another channel; what ALPN it carries is determined by the consumer (the channels-call crate pre-negotiates it as alk/call; a hypothetical other consumer could pre-negotiate it differently).

alknet-channels-call

The call-protocol coupling. Contains:

  • Channel 0 pre-negotiation as alk/call (ADR-036) — the preinstall_channel_0 logic that constructs channel 0's reassembly buffers with stream_types [0, 1] and hands the Connection to the CallAdapter.
  • The four lifecycle operations (ADR-037): channel/open, channel/close, channel/control, channel/resources/subscribe — registered on the call protocol's OperationRegistry at assembly time.
  • ChannelOperations (the registration helper that closes over a ChannelManager clone).
  • ChannelClient (ADR-043) — the client-side type that dials a transport, establishes the channels connection, and exposes open_channel(alpn, params) -> Channel. This is the worker/client entry point; it lives here because it needs channel 0 pre-negotiation (which is in channels-call).

Depends on channels-core + alknet-call. This is where the call-protocol coupling lives, isolated from the pure multiplexer.

Hub and worker are consumers, not sub-crates

The hub and worker are architectural roles, not channels sub-crates:

  • The hub is the existing alknet-hub crate. It depends on channels-call and uses the channels protocol as its substrate. The hub relay logic (ADR-042 — translate channel/open on channel 0, byte-forward data channels with channel_id rewrite) lives in alknet-hub, alongside its existing peer lifecycle, aggregated env, and service discovery responsibilities. There is no channels-hub sub-crate; alknet-hub IS the channels hub.

  • A worker is any crate that uses ChannelClient (ADR-043, in channels-call) to dial a hub. There is no channels-worker sub-crate; a worker depends on channels-call and uses ChannelClient directly. The worker may be a CLI binary, a docker-side connector, an SSH-side connector, or any other role that dials into a hub's channels connection.

This means the channels crate provides the substrate (channels-core + channels-call); the hub and worker crates are consumers that build on it. The dependency direction is: alknet-hub → channels-call → channels-core → alknet-core; a worker → channels-call → channels-core → alknet-core. The channels crate has no dependency on alknet-hub or any worker crate.

What moves where

Component Original (ADR-034-080) Now
9-byte wire format alknet-channels channels-core
Demux/Mux alknet-channels channels-core
ChannelBidiStreamSource alknet-channels channels-core
ChannelSubStreams alknet-channels channels-core
ChannelManager (without call_ops) alknet-channels channels-core
ChannelsAdapter (demux loop only) alknet-channels channels-core
Channel 0 pre-negotiation alknet-channels (ADR-036) channels-call
channel/open/close/control/resources/subscribe ops alknet-channels (ADR-037) channels-call
ChannelOperations registration helper alknet-channels channels-call
ChannelClient (ADR-043) alknet-channels channels-call
Hub relay (ADR-042) alknet-channels (spec) alknet-hub (the existing hub crate, consuming channels-call)

Relationship to alknet-hub

The existing alknet-hub crate (source-repo path: docs/architecture/crates/hub/README.md — alknet, provenance) is the hub pattern: peer lifecycle, aggregated env, service discovery. With channels as the substrate, alknet-hub gains a dependency on channels-call and incorporates the relay logic (ADR-042). The hub spec (that source-repo doc) will be updated to reflect that the hub uses channels as its transport substrate — one channels connection per leg (browser↔hub, hub↔spoke), with the relay translating channel/open and byte-forwarding data channels. The hub's existing responsibilities (peer lifecycle, aggregated env, service discovery, worker supervision) are unchanged; channels is the substrate they run on.

This makes "channels hub" and "hub" the same thing — the hub IS built on channels. There is no separate channels-hub concept.

Consequences

Positive:

  • The dependency graph is honest: channels-core is the pure multiplexer with no call dependency; the call-protocol coupling is isolated in channels-call. A consumer that wants the multiplexer without the call-protocol orchestration can depend on channels-core only.
  • The "no special-casing for downstream crates" principle is preserved: channels-core doesn't know about alknet-call, alknet-tty, or any handler crate. The call-protocol coupling is a channels-crate concern, not a downstream-crate concern.
  • Hub and worker are consumers, not sub-crates. The existing alknet-hub crate IS the channels hub — it depends on channels-call and uses channels as its substrate. A worker depends on channels-call and uses ChannelClient. The channels crate has no dependency on alknet-hub or any worker crate. This is the cleanest dependency direction: channels provides the substrate; hub and worker consume it.
  • The WASM and cross-platform story gets easier: channels-core is WASM-compatible by construction (pure byte manipulation, no platform deps); channels-call inherits the call protocol's WASM constraints; the hub and worker crates are platform-specific as needed.

Negative:

  • Two channels crates instead of one. The assembly layer must depend on channels-core + channels-call instead of one alknet-channels. This is the cost of the clean separation; the assembly layer already wires multiple crates, so this is consistent with the existing pattern.
  • The ChannelsAdapter in channels-core doesn't preinstall channel 0 — the consumer does. This means channels-core's ChannelsAdapter::handle exposes a hook (callback or trait method) for the consumer to install channel 0. channels-call provides the preinstall_channel_0 implementation; a different consumer could provide a different one. This is a slightly more complex adapter shape than "channel 0 is always call," but it's the cost of the clean separation.

Door type

One-way (crate structure). The two-crate split (channels-core / channels-call) is one-way — once consumers depend on channels-core without channels-call, re-merging them is a breaking change. The hub and worker being consumers (not sub-crates) is also one-way — it establishes the dependency direction (hub/worker → channels, not channels → hub/worker).

References

  • ADR-031: crate decomposition (no-handler-depends-on-another-handler — preserved; the channels sub-crates depend on core/call, not on handlers)
  • ADR-034: channels wire format (revised — substrate simplification; the wire format is in channels-core; amended by ADR-035 — 8-byte header, no stream_type)
  • ADR-035: channels pure channel multiplexing (amends this ADR — 8-byte wire format; ChannelSubStreams / SubStreamHandle removed; the channels layer has no stream_type concept)
  • ADR-036: channel 0 pre-negotiated (moves to channels-call)
  • ADR-037: channel lifecycle operations (move to channels-call)
  • ADR-038: ChannelBidiStreamSource (in channels-core; amended by ADR-035 — into_sub_streams removed, accept_bi yields BiStream)
  • ADR-039: ChannelsAdapter and ChannelManager (split: core demux in channels-core, call coupling in channels-call)
  • ADR-042: hub relay (in alknet-hub — the hub crate consumes channels-call; there is no channels-hub sub-crate, per this ADR's Decision §"No hub/worker sub-crates")
  • ADR-043: ChannelClient (in channels-call; there is no channels-worker sub-crate — a worker is any crate that uses ChannelClient to dial)
  • alknet hub crate (source-repo path docs/architecture/crates/hub/README.md, provenance) — the existing hub crate (the relay's consumer)

Amendment 2026-10-13: the split landed inside alkcall, not as separate crates

This ADR's decision text was authored when the channels protocol lived in the mono-repo crate alknet-channels and framed the split as sub-crates of it. The extraction (ADR-031, amendment 4) landed the channels-core (pure multiplexer) / channels-call (call coupling: channel 0 pre-negotiation, lifecycle ops, ChannelCore, ChannelClient) decomposition as modules inside alkcall — downstream depends on alkcall as a single crate. Hub and worker remain consumers (no worker/hub sub-crate), as decided here.