Review #002 R4 — a NegotiateRequest parse failure of the open op's
schema-validated input died silently (log + return, channel teardown,
consumer observed NoExitChunk — indistinguishable from a crashed
producer), while the other post-open failure classes (unknown backend,
allocate_failed, ownership denial) wrote the 0x00-prefixed error frame.
- make_tty_open_handler now accepts the channel's BiStream and writes
a malformed_negotiation frame via the shared
crate::adapter::send_negotiation_error (now pub(crate)) before
returning; the consumer's M1 peek surfaces NegotiationRejected
unchanged
- the frame type and layout are unchanged (ADR-001 wire-stable
contract); no new frame type, no wire change
- tests: make_tty_open_handler seam test with a hand-built
schema-bypassing input (cwd: 42) + a real-registry end-to-end test
via ChannelClient::open_channel (bypasses open_via_channels's local
fail-fast parse — R5's path — so it exercises the producer handler)
- docs: ADR-009 amended (Parse-failure error frame section);
tty-adapter.md malformed_negotiation row covers both paths;
session.rs post-open failure lists updated; review #002 R4 resolved
Note: the review's "unreachable end-to-end" premise was refined —
open_via_channels parses params locally (fail-fast) so a TtySession
consumer never hits the producer-side parse failure, but direct
ChannelClient callers do; the schema is deliberately partial so a
schema-valid value (cwd typed as a number) reaches the handler.
Verification: cargo test 95 lib (default) / 138 (--all-features);
clippy -D warnings native + wasm clean; fmt clean; doc 0 warnings.
Review of the 2026-09-05 session commits:
- the ADR-009 decision cited by 96692d3 and docs/reviews/001 was never
written — added decisions/009-channels-open-op-is-the-negotiation.md
(context: the L1 two-gate disagreement, alkcall 0.4.0/0.4.1
prerequisites, producer/consumer design, consequences, door type)
- channels.rs module doc + register_openable doc still described the
old wire-frame negotiation read (drive_session) — aligned with
drive_session_pre_negotiated and the enforced input schema
- tty-adapter.md: session-driver section + ADR tables now reference
ADR-009 and the pre-negotiated driver; overview.md ADR index row
Verification: cargo test 93 lib (default) / 136 (--all-features);
clippy -D warnings native + wasm clean; fmt clean; doc 0 warnings.
Port the alknet-tty architecture docs into alktty and add the BAST
document for the alk/tty wire format. Docs-only; no Rust source
changes.
Spec docs (docs/architecture/, flat layout — single-crate repo):
- overview.md — crate purpose, two-carriage model, deps, ALPN,
backend location map, feature gates
- tty-wire.md — 5-byte chunk codec, control channel split
(STREAM_CTRL_IN=3 / STREAM_CTRL_OUT=4), sentinels
- tty-backend.md — TtyBackend trait, TtyHandle, TtyControl,
REQ-TTY-01 (backends need not be natively async)
- tty-adapter.md — TtyAdapter, three-pump driver, exit-chunk
ordering (ADR-004), cancel cleanup (ADR-005), access control
- tty-local.md — LocalTtyBackend (local feature module), PTY +
pipe modes, REQ-TTY-02 (signal forwarding to process group)
- README.md — architecture index
ADRs (docs/architecture/decisions/, renumbered 001..008 from
alknet 052,053,054,055,056,057,077,093 in order):
- 001 wire format + two-carriage model (incl. Phase 7 control-
channel split amendment)
- 002 TtyBackend trait + TtyHandle
- 003 local backend placement (records both the alknet sibling-
crate decision and the alktty single-crate consolidation behind
a local feature)
- 004 exit code on a control chunk
- 005 backend cleanup on session cancel
- 006 self-contained negotiation framing
- 007 tty inside channels (reversed by 008; kept for historical
context with reversal notice)
- 008 channels pure channel multiplexing (reverses 007; TTY
always uses its 5-byte format)
BAST document (docs/architecture/tty-bast.md):
- Normative JSON spec for the alk/tty wire format, conforming to
the BAST meta-schema at https://alk.dev/bast/v1/schema
- 5-byte chunk header (struct, big-endian: stream_type uint8,
length uint32) + StreamType enum (Stdin=0..CtrlOut=4)
- ControlMessage union (field-name discriminator on type:
resize/signal/eof/exit) with documented deviation that on-wire
control payloads are UTF-8 JSON, not BAST's binary union
encoding
- NegotiationFrame (4-byte BE length + UTF-8 JSON body) +
NegotiateRequest / TerminalParams JSON shapes
- StreamType enum deviation noted: on-wire uint8, not BAST's
standard u32 enum index (chunk header is 5 bytes, not 8)
- alktty does not depend on alktype; the hand-rolled wire.rs is
the runtime codec, the BAST is the human-readable contract
AGENTS.md: fixed the ADR mapping table to match the plan's 8-to-8
mapping (the previous table substituted ADR-050 for 054, relabeled
056 as control-message split, dropped 077, and added a new
control-split ADR at 006 — inconsistent with both the plan and the
prose). ADR-050 (dynamic resource ownership) is an alkcall/alknet-
core ADR, not tty-specific, and is not ported; the Phase 7 control
split stays as an amendment inside ADR-001, mirroring alknet.
Verification (all pass, no Rust source changed):
- cargo test (80 passed)
- cargo test --all-features (103 passed)
- cargo clippy --all-targets -- -D warnings (clean)
- cargo fmt --check (clean)
- cargo check --target wasm32-unknown-unknown (clean)
- cargo clippy --target wasm32-unknown-unknown -- -D warnings
(clean)
- cargo doc --no-deps: 9 pre-existing intra-doc-link warnings in
src/session.rs and src/channels.rs (untouched by this commit;
not introduced here)
- BAST JSON parses; StreamType indices match wire.rs constants
(0=Stdin..4=CtrlOut)
- all markdown cross-reference links resolve