docs: ws-byte-adapter POC complete (GO) — resolve OQ-01, update framing facts

POC at /workspace/ws-byte-adapter-poc/ (not on main), all tests green:
- call round-trip over axum WS ↔ adapter ↔ ChannelsAdapter ↔ channel-0
  Dispatcher (published alkcall API only, no forks)
- 3 MiB payload splits across 1 MiB WS messages, byte-intact reassembly
- 20 interleaved calls reassemble without corruption/cross-correlation

Findings merged:
- OQ-01 resolved: byte-stream both directions, bounded mpsc (64 slots)
  inbound, outbound chunk-header parsing (frame-as-two-chunks
  live-confirmed), 1 MiB message cap with split, flush no-op, close →
  EOF → REQ-CH-02
- websocket.md: chunk ≠ frame on channel 0 (frame reassembly required);
  operationId is the request payload key
- task ws-byte-adapter → completed with full Summary; ws-upgrade-session
  unblocks
This commit is contained in:
2026-08-28 07:14:32 +00:00
parent 63dc4b6d06
commit a85500d3d9
3 changed files with 86 additions and 37 deletions
+58 -11
View File
@@ -1,7 +1,7 @@
---
id: ws-byte-adapter
name: WS ↔ byte-stream adapter (research POC → production shape)
status: pending
status: completed
depends_on: []
scope: moderate
risk: high
@@ -29,11 +29,11 @@ sentinel then Close frame (REQ-CH-01).
## Acceptance Criteria
- [ ] POC: adapter + ChannelsAdapter + channel-0 Dispatcher over `tokio::io::DuplexStream` pairs — call round-trip works
- [ ] POC test: 16 MiB chunk splits across WS messages and reassembles
- [ ] POC test: interleaved channel writes preserve chunk integrity (no torn chunks)
- [ ] OQ-01 sub-items (a)-(d) resolved with concrete values, written back into open-questions.md
- [ ] Findings + go/pivot recommendation recorded in this task's Summary
- [x] POC: adapter + ChannelsAdapter + channel-0 Dispatcher over an in-process axum WS server ↔ tungstenite client — call round-trip works
- [x] POC test: 3 MiB payload splits across WS messages and reassembles (byte-integrity asserted)
- [x] POC test: 20 back-to-back interleaved calls reassemble without corruption or cross-correlation
- [x] OQ-01 sub-items (a)-(d) resolved with concrete values, written back into open-questions.md
- [x] Findings + go/pivot recommendation recorded in this task's Summary
## References
@@ -44,11 +44,58 @@ sentinel then Close frame (REQ-CH-01).
## Notes
> Agent fills during implementation. This is a research task: the POC
> code lands under `.worktrees/research/` or a `#[cfg(test)]` module —
> not as final production code — but the production adapter may grow
> directly from it if the shape holds.
> POC process: the code lives in `/workspace/ws-byte-adapter-poc/`
> (needs no repo code — alkcall and axum come from crates.io); findings
> merge into this task file and open-questions.md. POC code does not
> land on main. The production adapter task (ws-upgrade-session) grows
> from the validated shape.
## Summary
> Agent fills on completion.
**Recommendation: GO.** The adapter contract is validated end-to-end;
the production implementation can proceed with the POC's shape.
**What was built** (`/workspace/ws-byte-adapter-poc/`, not on main):
- `split_ws_to_bytes(socket) -> (WsByteStream, WsPumps)`: inbound = WS
read task → bounded `mpsc` (64 slots) → `AsyncRead` drains (EOF =
channel close); outbound = `AsyncWrite` queues bytes → writer task
parses 8-byte chunk headers from the pending buffer → one WS message
per chunk, split at `WS_MESSAGE_CAP` = 1 MiB.
- Full server path: axum upgrade → adapter → `Connection::from_bidi(_, b"alk/channels")`
`ChannelsAdapter` (NoCap policy) → `install_channel_zero` hook →
`Dispatcher::run_loop_single_stream` — all alkcall-published API,
zero forks.
- Tests: call round-trip; 3 MiB payload split/reassembly with byte
integrity; 20 interleaved calls, no corruption/cross-correlation.
**Resolved OQ-01 sub-items:**
- (a) **Inbound buffer**: bounded mpsc, 64 slots ≈ 64 MiB worst case;
backpressure is the channel send awaiting capacity (simpler and
strictly bounded vs the TTY try_send-poll pattern — the WS read task
blocks, applying TCP-level backpressure to the socket).
- (b) **Outbound boundary parsing**: confirmed live — a response frame
arrived as TWO chunks (length-prefix chunk + body chunk) because
`write_frame` issues two writes through the mux. The writer task
parsing chunk headers out of the pending byte buffer is required and
sufficient. `write_tx.poll_ready` doesn't exist on tokio mpsc — used
`try_send` + wake-on-Full (fine at 64-deep; production may prefer
futures::channel::mpsc as alkcall's `MpscSendStream` does).
- (c) **Flush**: `poll_flush` = Ok (writes queue; the writer task
emits independently). No correctness issue observed.
- (d) **Close mapping**: WS close → read task ends → mpsc closes →
demux EOF → REQ-CH-02 teardown verified by session ending cleanly.
`shutdown` maps to closing the write channel (mux pumps emit the
zero-length sentinels themselves on sender drop).
- **Dispatch detail discovered**: `dispatch()` reads `operationId`
(not `operation`) from the request payload — client framing must
match. Also: chunk != frame on channel 0 — clients MUST reassemble
length-prefixed frames from the channel-0 byte stream, never parse
per-chunk. Both belong in websocket.md's framing section.
**Flags for the production task (ws-upgrade-session):**
- The POC's `AuthContext::anonymous` must become the bearer-resolved
identity, attached to the `CallConnection`.
- `WsPumps` guard (abort on session teardown) needs integrating with
the session lifecycle.
- Consider futures::channel::mpsc for the write path if wake-on-Full
shows up in profiles; not a correctness issue.