Files
alkhttp/docs/plans/implementation.md
T
glm-5.3-flash 63dc4b6d06 docs: implementation plan (TTY precedents folded in); task decomposition (17 tasks)
Plan updated with the alknet-tty findings: the drainer pattern
(single ordered writer) makes outbound chunk-boundary parsing sound;
TestStdinSink's try_send→Full→Pending is the inbound backpressure
precedent; OQ-01(a) now cites the reference.

Task graph (taskgraph-validated, 17 tasks, 6 generations, no cycles):
- tasks/server/: core-types, auth, healthz-decoy, adapter
- tasks/gateway/: dispatch, routes, publish
- tasks/websocket/: byte-adapter (research POC), upgrade-session, overlay-ops
- tasks/adapters/: from-openapi, from-jsonschema, to-openapi, from-wss, mcp
- tasks/client/: http-host
- tasks/infra/: integration-suite (phase 4)

Critical path runs through server core → adapter → WS session →
overlay tests → integration suite. High-risk tasks are the three WS
tasks, de-risked by the ws-byte-adapter POC blocking upgrade-session.
2026-08-28 06:05:45 +00:00

6.7 KiB

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_alls) 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 dispatch code is needed anywhere in alkhttp.
  6. The alknet-tty crates provide the precedents for the adapter. The channels protocol was abstracted from TTY's earlier 5-byte demux work, and alknet-tty's adapter embodies the two patterns OQ-01 needed:
    • The drainer pattern (pump_session): all producer pumps feed one bounded mpsc; a single drainer writes chunks in arrival order. The transport write side is therefore sequential — a boundary-parsing state machine in the WS adapter's write path is sound (no interleaving to handle).
    • Bounded backpressure (TestStdinSink::poll_write): try_sendFullPoll::Pending — the inbound buffer pattern for the WS→bytes direction.

This de-risks the two "high" tasks (WS adapter, WS session) from "unknown design" to "known shape, careful implementation." A targeted POC (research task) still validates the full loop — axum WS ↔ adapter ↔ alkcall ChannelsAdapter + channel-0 dispatcher over duplex — before the production implementation builds on it, since axum's WS API specifics (message sizing, backpressure interplay) are the one part neither the specs nor the TTY precedent exercise.

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).