0021 — Control-plane commands and client-side selector resolution
0021 — Control-plane commands and client-side selector resolution
TL;DR. The CLI’s ls / new / kill verbs ride the generic
COMMAND / COMMAND_RESULT envelope (SPEC §5), not new dedicated
frames. Session / window / pane selectors are resolved client-side
against a GET_STATE snapshot; the only commands that cross the wire are
Terminal-scoped L1 commands (GET_STATE, SPAWN, KILL_TERMINAL). No
session or window concept enters the wire — ADR-0017
holds. The L2 Collection that will eventually own the durable “session”
identity is named as the forward path; its command surface is deferred.
Status: Accepted Date: 2026-05-28
Context
phux ships CLI subcommands beyond attach / server: phux ls
(list sessions), phux new (create a session), phux kill TARGET
(destroy a session / window / pane). The issue tracker (phux-k61.2,
phux-5cd, phux-3kj) frames these as session-scoped, and the selector
grammar (docs/consumers/tui.md §3: ., name, name:N,
name:N.M, name:tag, @N) names sessions, windows, and panes.
But ADR-0016 made the
Terminal the wire primary and ADR-0017
removed session / window / pane / layout / focus from the wire
entirely — they are reference-TUI conventions, realized as an L2
Collection (the “session”) plus L3 metadata (window ordering, layout,
focus). The wire’s control surface is the generic COMMAND envelope
(SPEC §5), whose catalog (§5.1) is Terminal-scoped. That envelope is
specified but not yet wire-implemented, and L2 Collections are
reserved / TBD.
So the verbs are described in TUI vocabulary, but the wire has no session or window. This ADR settles how they map.
Decision
-
The generic
COMMAND/COMMAND_RESULTenvelope is the phux control plane. It is wire-allocated now (proto tier):COMMAND { request_id, cmd }→COMMAND_RESULT { request_id, result },request_id-correlated and asynchronous (the server MAY interleave other frames before the result, per SPEC §5). All future control verbs ride it; we do not grow a parallel family of dedicated request/reply frames the waySPAWN_TERMINALdid. -
Selectors are resolved client-side. A session / window / pane selector is parsed and resolved by the consumer against a
GET_STATEsnapshot, not sent to the server. The selector resolves to a set ofTerminalIds (and, for a whole-session target, the Collection identity). Only Terminal-scoped commands then cross the wire. The server never parses a selector and never learns the word “session” or “window.” -
v0.1 wires two L1 commands behind the envelope:
GET_STATE { scope }→OK_WITH(STATE(snapshot))— backsphux lsand all selector resolution.KILL_TERMINAL { terminal_id }→OK— backsphux kill, issued once per resolved Terminal.
phux newdoes not mint a new command: it reuses the existingATTACH { CREATE_IF_MISSING { name, command, cwd } }path (SPEC §7), which already creates a named session and attaches. The spec’s collection-scopedSPAWNcommand is a terminal-under-collection operation (closer to “split” than “new session”) and is left for the L2 milestone, not conflated withnewhere. -
The L2 Collection is the forward path for durable “session” identity, but its command surface (
CREATE_COLLECTION,DESTROY_COLLECTION,COLLECTION_IDvalues) is deferred until L2’s tagged union is allocated. Until then the server’s native session registry is the v0.1 backing store thatGET_STATE/SPAWNread and write — an implementation detail behind the envelope, not a wire concept.
Why
-
It keeps the wire substrate-shaped. Adding
LIST_SESSIONS/KILL_SESSIONframes would re-privilege the TUI’s product vocabulary on the wire — exactly what ADR-0017 refuses. Resolving selectors client-side puts the TUI’s conventions where they belong: in the consumer. -
It uses the envelope the spec already designed. SPEC §5 defined
COMMAND/COMMAND_RESULTprecisely so control verbs would not each mint a frame pair.ls/new/killare its first real callers; building the envelope now pays for every later verb (run-hook,resize, attach/detach-terminal) at zero marginal wire cost. -
It unblocks the CLI without freezing L2. The hard, open design (Collection lifecycle, multi-Terminal grouping, L3 layout schema) does not gate shipping visibility and teardown. v0.1 leans on the native session registry; the migration to Collections is additive.
Tradeoffs
-
The consumer carries the selector grammar and a resolution pass. A second consumer (a GUI) re-implements selector resolution, or we factor it into a shared crate. Acceptable: it is consumer logic by ADR-0017’s definition, and the grammar is small.
-
GET_STATEreturns the server’s session-shaped snapshot in v0.1. That snapshot leaks the native session model the rest of this ADR calls an implementation detail. We accept the leak transitionally; when L2 lands, the snapshot becomes Collection + Terminal shaped and the session framing moves fully client-side. Thescopefield is the seam that lets that evolve. -
killof a whole session is one round-trip viaKILL_COLLECTION { collection, name }(the teardown counterpart toCREATE_SESSION; phux-h9s). The server resolvesnameto its session and tears down every Terminal it owns in a single command. Sub-session targets (a window, a pane, a bare@id) still ride oneKILL_TERMINALper resolved Terminal — they address a strict subset, so a session-level command would over-kill.
Alternatives
-
Dedicated session frames (
LIST_SESSIONS,CREATE_SESSION,KILL_SESSION). Fastest to ship and matches the issue text. Rejected: it contradicts ADR-0016/0017 by putting TUI vocabulary on the wire, and it forks a frame family theCOMMANDenvelope exists to prevent. The debt is a guaranteed future wire break. -
Server-side selector resolution over a
COMMAND { KILL, target: str }string DSL. Rejected: SPEC §5.2 explicitly forbids a string-based command DSL; commands are a typed enum. A server that parsesname:N.Mis a server that knows about windows. -
Block the trio on full L2 Collection design. The honest end-state, but it stalls shippable CLI value behind the largest open design in the protocol. This ADR takes the additive path and names L2 as the explicit successor instead.