Files
alktty/docs/architecture
glm-5.3-flash 8ee9216a07 fix: channels parse-failure path writes the negotiation error frame (R4)
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.
2026-09-05 08:37:02 +00:00
..

alktty — Architecture

Terminal session protocol for the alk/tty ALPN: a producer/consumer protocol crate on top of alkcall channels. This directory holds the architecture spec docs, the BAST (Binary Abstract Syntax Tree) document for the wire format, and the ADRs.

Documents

Document Status Description
overview.md draft Crate purpose, the two-carriage model in brief, dependencies, ALPN, backend location map, feature gates
tty-wire.md draft The wire format: negotiation frame (JSON carriage), raw chunk codec ([stream_type: u8][length: u32 be][payload]), control channel split into STREAM_CTRL_IN / STREAM_CTRL_OUT halves, sentinels
tty-bast.md draft The BAST (Binary Abstract Syntax Tree) document for the binary framing layer of the alk/tty wire format (5-byte chunk header + negotiation length prefix); a normative JSON spec conforming to the BAST meta-schema at https://alk.dev/bast/v1/schema. JSON payloads are specified in tty-wire.md and the Rust source, not in the BAST
tty-backend.md draft TtyBackend trait, TtyParams, TtyHandle, TtyControl — the inversion point between the wire-format adapter and the backends. Carries REQ-TTY-01 (backends need not be natively async)
tty-adapter.md draft TtyAdapter (ProtocolHandler on alk/tty): session lifecycle, three-pump bidirectional driver, negotiation errors, exit-chunk ordering (ADR-004), access control, session-cancel cleanup (ADR-005)
tty-local.md draft LocalTtyBackend (the local feature module): portable_pty (PTY) and tokio::process::Command (pipe/runner). Carries REQ-TTY-02 (signal forwarding to the process group)

Applicable ADRs

Ported from the alknet mono-repo and renumbered into alktty's ADR range (001..008). The alknet originals at /workspace/@alkdev/alknet/docs/architecture/decisions/ remain the authoritative source for any ADR not yet ported, and for the alknet ADRs referenced by alknet number in the docs below (which are not tty-specific and therefore not ported into alktty's ADR range).

ADR Title Origin Status
001 alktty Wire Format and Two-Carriage Model alknet ADR-052 Accepted (amended 2026-07-18 — Phase 7 control-channel split)
002 TtyBackend Trait and TtyHandle — the Backend Inversion Point alknet ADR-053 Accepted
003 Local TTY Backend Placement (Single Crate with local Feature) alknet ADR-054 Accepted (records both the alknet sibling-crate decision and the alktty single-crate consolidation)
004 Exit Code on a Control Chunk (the Last Chunk Before Stream Close) alknet ADR-055 Accepted
005 Backend Cleanup on Session Cancel (Drop of exit_code Kills) alknet ADR-056 Accepted
006 Self-Contained Negotiation Framing (No alkcall-Internal-Wire-Types Dependency) alknet ADR-057 Accepted
007 TTY Inside Channels — Sub-Streams, Not Wire Format alknet ADR-077 Accepted (reversed by ADR-008 — kept for historical context)
008 Channels Pure Channel Multiplexing (8-Byte Header, No stream_type) alknet ADR-093 Accepted (amends alknet ADR-071/074; reverses ADR-007)

Key Design Principles

  1. A terminal session is a terminal concern, not an SSH or Docker concern. SSH and Docker are two backends that can allocate a PTY. alktty owns the terminal session lifecycle; the backends (DockerTtyBackend, SshTtyBackend, LocalTtyBackend) implement a TtyBackend trait. This dissolves the PTY hedge in the alknet-ssh research (DP-5): PTY is not an SSH feature delegated to a separate crate, it's a tty feature that SSH happens to be able to provide. See overview.md and ADR-002.

  2. Two-carriage model: JSON negotiation, then raw chunks. The bidi stream opens with a single length-prefixed JSON negotiation frame (terminal params, backend selector, command), then switches to a raw chunk format ([stream_type: u8][length: u32 be][payload]) for the life of the session. The call protocol's JSON-RPC shape handles the structured request; raw bytes handle the body, which is what a terminal actually is. No per-chunk EventEnvelope overhead, no base64. See tty-wire.md and ADR-001.

  3. Fixed channel set, not extensible. Five stream types (0=stdin, 1=stdout, 2=stderr, 3=ctrl-in, 4=ctrl-out), no negotiation. A 6th channel type is a wire-format change (one-way door); the ALPN model handles extensibility at the protocol level (a new ALPN is cheap, a wire-format change is not). The impoverishment vs SSH channels is the feature: alktty multiplexes one service (a terminal session) with a fixed channel structure, not arbitrary services. See tty-wire.md.

  4. The backend trait is the inversion point. alktty defines TtyBackend; the backend crates implement it. alktty depends on alkcall; backends depend on alktty for the trait; alktty does not depend on any backend. This preserves alknet ADR-003's no-handler-depends-on-another-handler rule. alktty does not depend on alkcall's internal wire types either (the negotiation framing is self-contained — ADR-006). See tty-backend.md and ADR-002.

  5. Backends need not be natively async (REQ-TTY-01). The trait's adapter-facing types (AsyncWrite, Stream<Item = Bytes>, BoxFuture, TtyControl) are the adapter's contract. A backend may expose blocking handles internally and bridge them via std threads + tokio mpsc/oneshot (the pattern portable_pty requires, and the local-PTY POC validated). The bridging pattern is a documented, supported implementation strategy. See tty-backend.md and tty-local.md.

  6. Exit code on a control chunk, last before stream close (ADR-004). {"type":"exit","code":N} rides on STREAM_CTRL_OUT (stream_type 4) and is the last chunk before the server closes the write half. This gives coordinators deterministic completion notification — no polling, no plugin state. The adapter owns the ordering; backends resolve exit_code and the adapter awaits, sends the chunk, closes. See tty-adapter.md.

  7. Drop of exit_code future kills the session target (ADR-005). On session cancel (connection drop, stream reset), the adapter drops the TtyHandle, which drops the exit_code future without driving it to completion. The backend's exit_code future's Drop-on-cancel MUST kill the child/container/SSH process. This is a behavioral contract on the TtyBackend trait — the adapter has no separate kill method; the cleanup is wired into the exit_code future's Drop by the backend. See tty-adapter.md and tty-local.md.

  8. The runner pattern is preserved, not specialized. The local backend in pipe mode (terminal: None) is a process-streaming endpoint — the same shape as GitHub/Gitea Actions runners, just over alk's transport instead of HTTP polling. alktty provides the mechanism (framed byte stream + exit code); runner policy (job management, log persistence, task graph) is a downstream crate's job. See tty-local.md and ADR-003.

  9. TTY always uses its 5-byte format, including inside channels (ADR-008). The same wire.rs code runs in both direct alk/tty and channels alk/channels modes; only the BiStream source differs. The channels layer strips its 8-byte header and hands TTY the payload transparently. This reverses the earlier two-mode design (ADR-007, kept for historical context). See tty-adapter.md.

Relevant Open Questions

OQ Title Status Relevance
OQ-43 TtyControl trait object Clone constraint resolved control: Option<TtyControlHandle> via a #[derive(Clone)] newtype wrapping Arc<dyn TtyControl + Send + Sync>; the trait is NOT Clone (not object-safe) — the newtype carries Clone-ability
OQ-44 Terminal modes (TTY modes) deferred(scope) TerminalParams.modes reserved; default terminal modes suffice for current scope; blocked on a concrete mode-control use case
OQ-45 Flow control for high-throughput stdout resolved QUIC per-stream flow control is the backpressure mechanism (chain complete by construction); no application-level windowing. Reversal is an additive ControlMessage variant, not a wire-format change
OQ-46 Runner API surface deferred(scope) The runner mechanism (pipe mode) is in alktty; runner policy (job management, log persistence, task graph) is a downstream crate, not in scope here
OQ-47 Stdin closure canonical signal resolved Either a zero-length stdin chunk or a {"type":"eof"} control chunk; both are accepted; the spec recommends eof for explicitness

References

  • docs/plans/project-setup.md — the current plan (phases 05). Phases 03 are landed; Phase 4 (this directory: architecture docs + BAST schema + renumbered ADRs) is landed by this commit; Phase 5 (tests, including integration tests in tests/ at the crate root) is not yet done.
  • alknet originals of the ported ADRs (alknet ADR-052, 053, 054, 055, 056, 057, 077, 093) at /workspace/@alkdev/alknet/docs/architecture/decisions/ — the authoritative source for any ADR not yet ported, and for the alknet ADRs referenced by alknet number in the docs above (which are not tty-specific and therefore not ported into alktty's ADR range).
  • alktype BAST format spec — the normative format spec for BAST documents (the meta-schema tty-bast.md conforms to); see also /workspace/@alkdev/alktype/docs/architecture/bast-format.md.
  • alkcall architecture README at /workspace/@alkdev/alkcall/docs/architecture/README.md — the producer/consumer protocol-crate pattern alktty follows.