alktty 0.5.0

Terminal session protocol: wire format, TtyBackend trait, TtyAdapter, and typed consumer client. Producer/consumer protocol crate on top of alkcall channels.
Documentation
# 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]overview.md | draft | Crate purpose, the two-carriage model in brief, dependencies, ALPN, backend location map, feature gates |
| [tty-wire.md]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]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]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]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]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 and ADR-010 are 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]decisions/001-wire-format-and-two-carriage.md | alktty Wire Format and Two-Carriage Model | alknet ADR-052 | Accepted (amended 2026-07-18 — Phase 7 control-channel split) |
| [002]decisions/002-ttybackend-trait-and-ttyhandle.md | TtyBackend Trait and TtyHandle — the Backend Inversion Point | alknet ADR-053 | Accepted |
| [003]decisions/003-local-backend-placement.md | 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]decisions/004-exit-code-on-control-chunk.md | Exit Code on a Control Chunk (the Last Chunk Before Stream Close) | alknet ADR-055 | Accepted |
| [005]decisions/005-backend-cleanup-on-session-cancel.md | Backend Cleanup on Session Cancel (Drop of `exit_code` Kills) | alknet ADR-056 | Accepted |
| [006]decisions/006-negotiation-framing-self-contained.md | Self-Contained Negotiation Framing (No alkcall-Internal-Wire-Types Dependency) | alknet ADR-057 | Accepted |
| [007]decisions/007-tty-inside-channels.md | TTY Inside Channels — Sub-Streams, Not Wire Format | alknet ADR-077 | Accepted (**reversed by ADR-008** — kept for historical context) |
| [008]decisions/008-channels-pure-channel-multiplexing.md | Channels Pure Channel Multiplexing (8-Byte Header, No `stream_type`) | alknet ADR-093 | Accepted (amends alknet ADR-071/074; reverses ADR-007) |
| [009]decisions/009-channels-open-op-is-the-negotiation.md | 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; amended by ADR-010 — semantic failures move into the establisher) |
| [010]decisions/010-channels-establisher-migration.md | Channels-Path Establishment Failures Move into an Establisher | alktty-native | Accepted (adopts alkcall 0.5.0 ADR-049; amends ADR-009's R4 amendment; amended 2026-09-07 §2A for alkcall 0.6's plan payload — allocation moves into the establisher, no in-band frame from a registered producer) |

## 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]overview.md and [ADR-002]decisions/002-ttybackend-trait-and-ttyhandle.md.

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]tty-wire.md and
   [ADR-001]decisions/001-wire-format-and-two-carriage.md.

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]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]decisions/006-negotiation-framing-self-contained.md).
   See [tty-backend.md]tty-backend.md and
   [ADR-002]decisions/002-ttybackend-trait-and-ttyhandle.md.

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]tty-backend.md and [tty-local.md]tty-local.md.

6. **Exit code on a control chunk, last before stream close
   ([ADR-004]decisions/004-exit-code-on-control-chunk.md).**
   `{"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]tty-adapter.md.

7. **Drop of `exit_code` future kills the session target
   ([ADR-005]decisions/005-backend-cleanup-on-session-cancel.md).**
   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]tty-adapter.md and
   [tty-local.md]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]tty-local.md and
   [ADR-003]decisions/003-local-backend-placement.md.

9. **TTY always uses its 5-byte format, including inside channels
   ([ADR-008]decisions/008-channels-pure-channel-multiplexing.md).**
   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]decisions/007-tty-inside-channels.md, kept for
   historical context). See [tty-adapter.md]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 0–5). 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]https://alk.dev/bast/v1/schema — 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.