Files
alktty/docs/architecture
glm-5.3-flash 3baa993edb fix: local-bridge robustness, scope-gate order, sentinel contract; packaging/docs closeout (P8-P11, P2/P3/P15)
Closes review #003 (prepublish review for v0.1.0).

- P8: StdinSink::poll_shutdown parks an inflight reserve+send on a
  full channel (waker registered) — a stdin blast followed by EOF
  delivers the EOF instead of stranding it
- P9: five poisoned-lock .expect() sites -> unwrap_or_else(into_inner)
- P10: three thread-spawn .expect() sites -> TtyError::AllocFailed
- P5: tty:open scope gate runs before carriage/cmd/backend-lookup
  checks (no backend-name enumeration differential for unscoped ids)
- P11: recv_stdout terminates on the zero-length drained sentinel;
  the sentinel is no longer yielded as an item (doc was already the
  contract); stderr has no sentinel (doc noted)
- P2: exclude AGENTS.md + docs/plans/, drop dead Cargo.lock and
  docs/research/ entries (package list: 42 files, 659.2KiB)
- P3: AGENTS.md phase status (all five landed), ADR range 001..009
  (+ alktty-native ADR-009 in the mapping), alkcall guidance
  corrected to v0.4.x / pin "0.4.0"; architecture README ADR-009 row
  + landed-phase status
- P15: backend.rs doc typo; redundant tokio-stream dev-dep removed;
  NegotiationError::Io arm logs; set_identity failure logs;
  input_pump.abort() at session end; TtySessionError::Open carries
  the accept_bi StreamError (no io::Error flattening); borrowing
  deserialize in open_via_channels (no params.clone());
  error_response_bytes guards an "error" key in fields; trivial
  inline comments promoted/removed; plan-doc test counts + doc
  front-matter refreshed; session tests that raced session teardown
  under the abort change use a GatedBackend (exit held until
  released)

Verification: cargo test 104 lib / --all-features 147; clippy
(all-targets + wasm32) -D warnings; fmt; wasm check; doc 0 warnings;
publish dry-run OK.
2026-09-05 16:47:45 +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..009; ADR-009 is alktty-native). 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)
009 The Channels Open Op's input Is the Negotiation alktty-native Accepted (resolves review #001 L1; amended by review #002 R4 — parse failure is a client-visible error frame)

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). All five phases are landed: Phase 4 landed this directory (architecture docs + BAST schema + renumbered ADRs); Phase 5 landed the tests, including the integration tests in tests/ at the crate root.
  • 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.