- Rewrote the remaining em dashes outside docs/reviews/ (preserved as historical record, matching the alktty precedent): tests/ (313), fuzz/ (34 plus one shell comment), tasks/ (671 plus 12 kept in fences), docs/research + sdd_process + plans (525 plus 25 in fences), docs/architecture/overview.md fence labels (13), README, CHANGELOG, AGENTS.md, fuzz/README.md (176), and the .opencode/agents prompts (110) - Rewrote fenced content too: ASCII tree and module-map label separators, Rust snippet comments, and template placeholders; wire data, JSON/BAST examples, and transcripts survive verbatim - Rewrote 6 assert/expect message literals in tests/ and 2 format strings in fuzz/shared/src; no test data changed - Added Project Convention 18: no em dashes in prose (doc comments, markdown including docs/ and tasks/, agent prompts, commit messages); fence content rule, en-dash clause rule, and the no-spaced-hyphen prohibition follow alktty's convention 15 Follows6b20924(src doc comments) andcd0afe0(docs/architecture). Verification: cargo test (141 pass); cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/
7.9 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tunnels/consumer-session | Consumer half, TunnelSession (open/adopt, data planes, teardown) | completed |
|
broad | high | phase | implementation |
|
Description
Implement src/consumer.rs per consumer.md + ADR-005: TunnelSession,
the typed client, both construction paths, the substrate-shaped data
plane, and teardown ownership. The reverse POC's consumer
(/workspace/alktunnels-reverse-poc/src/consumer.rs, ReverseTunnel)
is the seed; this task generalizes it to the spec's session type.
Construction
- Forward path:
TunnelSession::open(client: &ChannelClient, params: TunnelParams) -> Result<Self, TunnelOpenError>viaclient.open_channel(OP_TUNNEL_OPEN, params, TUNNEL_ALPN)→ adopt → split the channelBiStream→ data plane by substrate. Typed errors (ADR-049 §4; never a phantom session). - Reverse path:
TunnelSession::adopt(manager: &ChannelManager, channel_id: u32, alpn: impl Into<String>) -> Result<Self, TunnelOpenError>: adopt a worker-allocated ID (early arrivals parked), install the data plane from the adopted halves.open_reverse_channel(hub_call, params, auth_token) -> Result<u32, ReverseOpenError>as the free function the assembly layer calls beforeadopt(the POC's shape: the hub's call surface is aCallConnection, not aChannelClient, so the two-step is the honest API).
Data plane
- Stream variant:
stream_halves()→(&mut dyn AsyncRead, &mut dyn AsyncWrite)(borrowed access);take_halves(self)→ owned boxed halves (the session's halves ARE the tunnel, i.e. raw pass-through). - Datagram variant:
send_datagram(&[u8]) -> Result<(), TunnelIoError>(frame → write → flush;Oversize>65535);recv_datagram() -> Result<Option<Bytes>, TunnelIoError>:Some(bytes)per datagram (possibly empty,len=0legal),Noneonly on stream EOF (the F-2 layering; the POC'sread_one_datagramincremental loop). - Wrong-substrate operations are
WrongSubstrateerrors (the POC's shape).
Pump ownership + teardown (ADR-005, the point)
pump_against(self, accepted: impl AsyncRead + AsyncWrite + ...), for the reverse path: spawnpump_bidi(channel_bistream, accepted_read, accepted_write), hold the returned handle. Returns the session (builder-style) or takes self and returns the handle. Pick the shape that makes holding easy; document it.close(self) -> bool: abort the pump (if session-owned) +teardown_channel(ungraceful path).join(self) -> (u64, u64, bool): await pump completion, then reap; copy counts for observability. Pump-less sessions (aftertake_halves): completes immediately, reaps only,(0, 0, reaped)(consumer.md's pinned semantics).Drop: abort + syncteardown_channel(best-effort; never leak the entry). NoClone.
Tests
Extend the producer task's integration suite: forward open (session
halves drive a duplex), reverse open+adopt+pump_against (the POC's
ReverseTunnel tests: round-trip, half-close W4, join_and_reap copy
counts, out-of-band close + self-reaping, pump-less join), datagram
variant (round-trip incl. empty datagram via the codec), teardown
matrix (close/join/Drop paths, with no leaked channel entries asserted
via channel_ids()).
Acceptance Criteria
open+adoptconstruction both present; typed error surfaces exactstream_halves/take_halves/send_datagram/recv_datagramper consumer.md- Teardown matrix: close (ungraceful), join (graceful + copy
counts), pump-less join
(0, 0, reaped), Drop (no leak) - Half-close semantics test (W4's shape) passes
- No
CloneonTunnelSession(compile-asserted) - Clippy/fmt clean; wasm32 check passes;
cargo testgreen
References
- docs/architecture/consumer.md (the normative API)
- docs/architecture/decisions/005-consumer-session-owns-teardown.md
- POC reference:
/workspace/alktunnels-reverse-poc/src/consumer.rs(the seed shape to generalize)
Notes
Agent fills during implementation.
adopttakessubstrate: Substrate(a deviation from the task sketch'sadopt(manager, channel_id, alpn)): the substrate shapes the data plane (raw halves vs the UDP codec) and cannot be inferred from the channel ID, so passing it toadoptkeeps the two calls consistent. The ALPN is observability-only (alkcall manager semantics); the substrate rode the open call that produced the ID.pump_againsttakes the whole accepted stream (S: AsyncRead + AsyncWrite) and splits internally withtokio::io::splitat the pump boundary, matching the POC'spump_against(accepted). Returns the session (builder-style); the pump handle is session-owned and private (close/join/Drop are the only handles needed).take_halves(self)returnsTakenHalves { session, read, write }(not bare halves): the session must stay alive as the channel-entry owner (reap viaclose/join/Drop); returning bare halves would strand the entry.joinon the held session completes immediately ((0, 0, reaped), because pump-less).open_reverse_channel's hub_call is&CallConnection(not&Arc<CallConnection>); borrowing is the honest shape; callers hold the Arc.- Fields are
Option+ idempotentDrop: aDrop-impl type cannot be destructured, sotake_halves/pump_against/close/jointake fields out via.take(); an emptied session drops as a no-op. Teardown is idempotent (teardown_channelon a reaped entry isErr(UnknownChannel), ignored). - Bogus-ID adopt succeeds by design (the manager parks early
arrivals for any not-yet-seen ID, the adoption-race cover,
alkcall ADR-047 §5); the honest
AdoptFailedprobe is the ID-collision path (same ID adopted twice), pinned in the tests. recv_datagramkeeps a pending queue: one chunk may batch several frames;DatagramReader::feeddecodes all completions per feed, so frames beyond the first queue for subsequentrecv_datagramcalls (dropped frames were a bug the tests caught).- Harness gained
wire_forward, aForwardTopology: the forward path's consumer is the connect side (from_connection, pure consumer) and the producer accepts via the adapter; the accept-side transport-identity posture attaches the dialer's identity in the install hook (modeled; the transport resolves it out-of-band for real).all_substrate_dial= stream echo + framed UDP echo.
Summary
Agent fills this on completion.
Implemented TunnelSession (src/consumer.rs, ~415 lines) per
consumer.md + ADR-005: both construction paths (open via
ChannelClient::open_channel, adopt via ChannelManager:: adopt_channel + open_reverse_channel on the CallConnection),
the substrate-shaped data planes (raw halves for stream; the ADR-003
codec with a pending frame queue for UDP), pump ownership
(pump_against spawns pump_bidi and holds the handle), and the
full teardown matrix: close (abort + ungraceful reap), join
(await + reap + copy counts; pump-less (0, 0, reaped)), and Drop
(abort + sync reap, never leaks). No Clone (compile_fail doc-test).
Tests: tests/consumer_session.rs has 12 integration tests over the
two harness topologies: forward round-trip (session halves), take-
halves + pump-less join, datagram round-trip incl. the F-2 empty
datagram, wrong-substrate typed errors, oversize-at-frame-time,
reverse open+adopt+pump round-trip, W4 half-close, out-of-band close
- self-reap (the W3 split), Drop-no-leak (pump + pump-less),
AdoptFailed-on-collision, failed-open-typed (no phantom session),
concurrent reverse sessions. Harness additions:
wire_forward(ForwardTopology) +all_substrate_dial/framed_udp_echo_dial.
Verified: cargo test green (38 total), clippy -D warnings clean
(native + wasm32), fmt clean, wasm32 check passes (default crate
stays wasm-clean).