docs: correct WS framing claims from spike; add implementation plan

Spike against alkcall source resolved ADR-067 assumptions:
- write_chunk issues header+payload as separate write_alls; channel
  0's write_frame issues prefix+body separately — a logical write can
  surface as multiple chunks, so the WS adapter must parse outgoing
  chunk boundaries (byte-stream treatment both directions), not assume
  write-per-chunk or message-per-chunk
- MAX_CHUNK_LEN is 16 MiB; the WS path needs a practical message cap
  with oversized chunks split across messages
- install_channel_zero + run_loop_single_stream confirmed as the exact
  server-side seam; EOF/teardown invariants already specified by
  alkcall (REQ-CH-01/02)

Corrections applied to websocket.md, ADR-067, OQ-01.

docs/plans/implementation.md: scoped plan guiding task decomposition —
spike findings, 4-phase build order, OQ dispositions, task conventions.
This commit is contained in:
2026-08-28 05:55:03 +00:00
parent 320ea87b08
commit eaf1a203bc
4 changed files with 160 additions and 24 deletions
+113
View File
@@ -0,0 +1,113 @@
# Plan: alkhttp Implementation
Working plan guiding task decomposition and implementation. Less
specific than the tasks it produces, more specific than the
architecture docs. Status fields in task frontmatter are the source of
truth for progress; this document explains the *why* of the ordering.
Source crate: `/workspace/@alkdev/alknet/crates/alknet-http` (~11k LOC).
Target: this crate, on alkcall 0.1.1 (crates.io).
## What the spike established
A validation pass against alkcall's actual source resolved the factual
unknowns that would have shaped tasks incorrectly:
1. **The WS adapter must parse outgoing chunk boundaries.** The mux
emits one mpsc payload per chunk, but a logical write above the mux
(channel 0's `write_frame` issues length-prefix and body as two
`write_all`s) can surface as multiple chunks. "One write = one
chunk" and "one chunk = one WS message" are both unusable as
invariants. The adapter treats the WS message stream as a byte
stream in both directions and parses the 8-byte header on both
read and write sides.
2. **Chunk size cap.** `MAX_CHUNK_LEN` is 16 MiB; browser WS stacks
and intermediaries commonly cap messages far lower. The WS path
needs its own practical cap (default ~1 MiB) with oversized chunks
split across messages — legal, since the receiver's boundary is the
chunk header.
3. **The `install_channel_zero` hook is the exact seam** for the
server WS path: alkcall's `ChannelsAdapter` runs the in-line demux
loop, and the hook receives channel 0's `Connection` + `AuthContext`
— alkhttp's job is to construct the `CallConnection`, attach the
bearer-resolved identity, and run
`Dispatcher::run_loop_single_stream`. `alkcall`'s own tests
(`channels/client.rs`) demonstrate this wiring end-to-end over
duplex pairs.
4. **EOF/teardown semantics are already specified** by alkcall
(REQ-CH-01/02): `AsyncWrite::shutdown` → zero-length sentinel; demux
EOF → all channels cleared. The adapter maps WS close to transport
EOF and lets alkcall's invariants do the rest.
5. **`Dispatcher::run_loop_single_stream` exists** and is the channel-0
dispatch loop — no new dispatch code is needed anywhere in alkhttp.
This de-risks the two "high" tasks (WS adapter, WS session) from
"unknown design" to "known shape, careful implementation."
## Build order (dependency spine)
```
Phase 1 — server foundation (no WS, no adapters)
core types → server adapter + auth → gateway dispatch + routes
→ verified over tokio DuplexStream end-to-end
Phase 2 — WS + channels (the novel part)
ws byte-stream adapter → ws upgrade handler + channels session
→ verified browser-session-style over duplex WS
Phase 3 — adapters (mostly ports)
http client host → from_openapi/from_jsonschema → to_openapi
→ from_wss (depends on ws adapter) → mcp feature (from_mcp/to_mcp)
→ /publish endpoint + to_openapi v2 of the gateway doc
Phase 4 — hardening
integration test suite (full surface over duplex) → docs sync
→ publish prep (dry-run, semver check)
```
Rationale for key orderings:
- **Server core before WS** because the WS upgrade is a route on the
`HttpAdapter` — the router, auth middleware, and decoy must exist
first, and they're verifiable without WS (gateway over duplex).
- **WS adapter before `from_wss`** — same adapter, both directions;
building the consumer first would mean validating the adapter
without its hardest user (the axum WS type).
- **`/publish` in Phase 3** — the gateway spine (`/call`/`/subscribe`)
lands in Phase 1; `/publish` adds a dispatch mode to a working
gateway and gates OQ-02's version bump. Building it early would
couple an unresolved OQ to the critical path.
- **MCP last** — feature-gated, rmcp-heavy, and the gateway dispatch
spine it consumes is stable by then.
## OQ dispositions
| OQ | Disposition | Where it resolves |
|----|-------------|-------------------|
| OQ-01 (WS adapter semantics) | Partially resolved by the spike: byte-stream treatment both directions, boundary = chunk header, split oversized chunks, shutdown → EOF sentinel + Close frame. Remaining: exact buffer bounds, flush semantics — locked during the WS adapter task. | `tasks/websocket/` |
| OQ-02 (/publish framing) | Resolve in the `/publish` task: first line carries `{operation, chunk}`; terminal error = plain HTTP status + JSON body (not an NDJSON line). Then bump the gateway doc version. | `tasks/gateway/` |
| OQ-03 (from_wss reconnect) | v1: connection drop → retryable failures; policy deferred. Documented in ADR-070; no task. | — |
| OQ-04 (browser client) | Out of scope for this crate. | — |
## Conventions for the tasks
- Topic subdirectories: `tasks/server/`, `tasks/websocket/`,
`tasks/gateway/`, `tasks/adapters/`, `tasks/client/`, plus
`tasks/infra/` for repo-level concerns (CI, publish prep).
- Every task carries the full frontmatter set (`scope`, `risk`,
`impact`, `level`) — taskgraph's analysis commands rely on them.
- Ported-source references: each task cites the alknet-http source
file(s) it ports from, so the implementing agent can diff against
the original rather than re-derive.
- Verification per task: `cargo test -p <affected>` at minimum; the
Phase 4 integration task runs the full `cargo test --all-features`.
- No task crosses a subsystem boundary except through its declared
`depends_on`.
## What the tasks will NOT cover
- The alknet-side wiring (endpoint, TLS, ALPN router registration) —
alkhttp exposes `HttpAdapter` as a `ProtocolHandler`; the dial/accept
composition is the consumer's job (AGENTS.md convention 9).
- A browser/JS client for channels-over-WS (OQ-04, deferred).
- WebTransport (ADR-069: out of scope entirely).