Wire-protocol changelog
Wire-protocol changelog
TL;DR. Wire-format change log for the phux protocol; the top
entry’s version must match PROTOCOL_VERSION in phux-protocol. CI
gate spec-version-sync enforces this.
Newest version is at the top. The top entry’s <major>.<minor>.<patch>
must equal PROTOCOL_VERSION in crates/phux-protocol/src/lib.rs;
just docs-check enforces it.
| Version | Date | Notes |
|---|---|---|
| 0.6.0-draft.2 | 2026-07-25 | phux-j4on: enforce the existing HELLO compatibility boundary. The concrete client and server versions must match on major.minor; patch differences remain compatible. A mismatch now receives ERROR { VERSION_INCOMPATIBLE } naming both versions and whether to update the client or server, then the server flushes the error and closes before session state. No wire-format change: the existing HELLO/HELLO_OK fields and error code are unchanged. |
| 0.6.0-draft.1 | 2026-07-25 | ADR-0059: add capability-gated PUT_FILE at Command tag 0x15 and FILE_UPLOAD at server feature bit 0x00000020. A non-zero 16-byte upload id, target TerminalId, validated extension, u64 offset, bounded bytes, final marker, and final SHA-256 transfer a file into the terminal-owning server’s mode-0700 upload sandbox without PTY transit. Chunks are capped at 8 MiB and files at 64 MiB; matching overlap makes reconnect and restart retries idempotent. CommandValue::FileUpload at tag 0x05 returns the contiguous next_offset and exposes the server-owned final path only after digest verification, sync, and atomic rename. Satellite targets relay to and persist on the owning satellite. This bumps the protocol minor because older peers cannot decode the new command or result variants; FILE_UPLOAD negotiation prevents probing them. |
| 0.5.0-draft.25 | 2026-07-24 | ADR-0053: add reconnect-safe APPLY_INPUT at Command tag 0x14 under the existing COMMAND / COMMAND_RESULT envelope. Body: non-zero 16-byte consumer operation id, local TerminalId, u16 event count, and the ordered existing InputEvent atoms; limits are 256 events and 64 KiB for both command body and encoded PTY bytes. HELLO_OK.server_caps appends a u32 feature bitset after its legacy layers byte and advertises ACKNOWLEDGED_INPUT = 0x00000010; old one-byte caps decode empty. The server validates and encodes the whole batch against one mode snapshot, writes it as one PTY job, and replies Ok only after write_all + flush. A non-zero 128-bit HELLO_OK.server_id now identifies one in-memory server incarnation, replacing the PID. Same-id/same-payload retries within ten minutes return a cached final result without a second write; state loss changes the incarnation, after which clients report unknown instead of replaying. UNSAFE_PASTE = 203 is now emitted for whole-batch paste refusal; new INPUT_DELIVERY_UNKNOWN = 205 marks a post-handoff write whose completion cannot be proven. APPLY_INPUT is local-only and rejects satellite ids. Additive draft bump; existing input and ROUTE_INPUT bytes and fire-and-forget semantics are unchanged; PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.24 | 2026-07-15 | phux-87r: a new Command verb DETACH_CLIENTS { session: optional<str> } (tag 0x13, appended after REPORT_ASKED’s 0x12) rides the existing COMMAND / COMMAND_RESULT envelope and backs the phux detach CLI verb. Body: a presence byte (0/1) followed, when set, by a u32-BE-length-prefixed UTF-8 session name. session = Some(name) force-detaches every client attached to that session; session = None detaches every attached client on the server. Each target receives a DETACHED (0x82) frame and its attachment is torn down server-side, so its TUI exits cleanly — distinct from the DETACH (0x03) frame, which detaches only the sending connection. Reply: COMMAND_RESULT { OkWith(Json(count)) } where count is the number of clients detached; an unknown session name detaches nobody and replies 0 (not an error), mirroring KILL_TERMINALS’s skip-silently shape. Additive: no tag renumbered, no existing bytes changed; the new Command tag decodes as unknown on an older peer. Draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.23 | 2026-07-15 | phux-oih5.3 / phux-oih5.13: SPAWN_TERMINAL gains additive optional field id 8, owner_terminal: optional<TerminalId>. When present, a local server creates the new Terminal in the exact window owning that existing Terminal and refuses a missing/non-local owner rather than falling back to attached or recently active state. This is ownership addressing only: split direction, ratio, and focus remain client-owned L3 layout metadata. None preserves legacy unplaced behavior; ownership targeting cannot be combined with satellite routing. The phux spawn and phux launch CLIs expose --target, --split, and --ratio, publish placement through the shared last-write-wins LayoutOps path without changing serialized focus, verify authoritative post-spawn ownership before publishing layout (rolling back if an older server ignored field 8), and clean up a known spawned Terminal if layout publication fails. Additive field; no existing bytes or tags change and PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.22 | 2026-07-12 | ADR-0046 (extending ADR-0040): a server MAY act as a writer of phux.agent/v1 (L3.md §3.7). ADR-0040 made the record’s schema normative but named no writer, so state was unknown on every pane; a reference server now derives state (and name/kind) for a Terminal it owns from that Terminal’s OSC title, live screen, and PTY foreground process, and writes it on the same SET_METADATA path any other writer uses. No wire-format change: no frame type, field, tag, event kind, or error code is allocated or altered, and a server-derived record is byte-identical in shape to a declared one — a consumer neither can nor needs to distinguish them. L3 §3.7’s “the server stores the bytes opaquely and interprets nothing” is narrowed to the L3 read/write path, with this one conventional key carved out. Four normative constraints on a deriving server: an explicit SET_METADATA supplying state MUST NOT be overwritten (the derivation stands down until a DELETE_METADATA clears the record); an identity-only declaration’s name/kind/session MUST be preserved when the derivation fills state; a server MUST only DELETE a record it authored; and it SHOULD write only on a change of the derived value, so a long-running working agent produces no repeated METADATA_CHANGED broadcasts. Consumers MUST NOT assume a server derives the record — the key is simply absent on one that does not, exactly as before. Convention clarification; PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.21 | 2026-07-11 | phux-v45.15 (ADR-0007 §4): two federation snapshot-gate edge conformance fixes to L1.md §9.1, no wire-format change. (1, upgrade-path gating) the two-hop attach snapshot-ordering gate (draft.19 / phux-v45.12) now also holds when a consumer upgrades an already-Open stream to an attach: a client already event-subscribed to a satellite Terminal (or already attached with its snapshot landed) that re-issues ATTACH_TERMINAL on the same Terminal re-enters the gate, so the new attach’s TERMINAL_OUTPUT deltas are suppressed until its own fresh TERMINAL_SNAPSHOT lands rather than riding ahead of it on the still-open gate (the L1 §9.1 “snapshot MUST precede the first delta” violation draft.15 fixed for a fresh second attach, previously resurfacing on the upgrade path). A satellite error on the upgrade restores the pre-existing stream to Open instead of stranding it behind a snapshot the refused attach never sends. (2, snapshot-independent frames bypass the gate) BELL now joins TERMINAL_CLOSED in being delivered best-effort past the gate to a still-gated (AwaitingFirst) consumer: a bell is an ephemeral notification the TERMINAL_SNAPSHOT does not capture, so gating it would drop it permanently — unlike a TERMINAL_OUTPUT delta, which the snapshot safely supersedes (freshest full grid wins). Ordering against the snapshot is irrelevant for a lifecycle signal or a side-channel notice. Both are conformance fixes to already-normative §9.1 behavior; no frame type, field, tag, or error code is allocated or altered, and no existing bytes change. Draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.20 | 2026-07-11 | phux-51n6.3 / phux-v45.8 (ADR-0043, extending ADR-0018): normative meaning of the negotiated OutputMode::StateSync emitter and its loss-tolerant reference-advance strategy, no wire-format change. (51n6.3, state-diff output mode) proto.md §6.2 now states that under StateSync, TERMINAL_OUTPUT.bytes is the minimum-VT transition from the consumer’s reference grid to the live grid, synthesized once per tick and RTT-paced, so a runaway producer bounds the consumer’s re-parse rate (structural coalescence) rather than streaming every intermediate frame — converging to a grid equivalent to what the Raw byte stream would produce. (v45.8, loss-tolerant advance-on-ack) proto.md §6.2 and L1.md §9.1 document that whether the origin server advances a consumer’s reference on emit (emit-once; correct and cheapest on a reliable ordered transport) or on FRAME_ACK (loss-tolerant; each tick re-diffs the live grid against the last-acked reference, which does not advance until the ack lands, and an un-acked frame is retransmitted after a 3·srtt timeout) is a server-side emission strategy chosen per consumer from the transport/topology — it needs no ClientCapabilities field and changes no wire bytes (FRAME_ACK and seq already round-trip). On the federation forwarded leg, whose fan-out can drop whole frames under consumer backpressure, this lets a dropped frame re-diff against the last-acked reference and self-heal instead of diverging the mirror. A consumer MUST NOT assume which strategy serves it; both converge to the same grid. Semantic/normative documentation only: no frame type, field, tag, or error code is allocated or altered, and no existing bytes change; draft bump (PROTOCOL_VERSION stays 0.5.0). Scope honesty: production auto-activation of loss-tolerance for forwarded consumers and the wire base_seq that would close the residual A→B→A-across-an-unacked-frame bound are deferred (ADR-0043 Tradeoffs). |
| 0.5.0-draft.19 | 2026-07-11 | phux-v45.12 / phux-v45.13 (ADR-0007 §4): two federation two-hop hardening conformance fixes to L1.md §9.1, no wire-format change. (v45.12, snapshot ordering under backpressure) the two-hop attach’s “the authoritative TERMINAL_SNAPSHOT MUST precede the first TERMINAL_OUTPUT delta” guarantee (draft.15) is now held on a hub even when the attaching consumer’s outbound mailbox is briefly full: the hub retains the un-deliverable re-tagged snapshot for that consumer and suppresses that consumer’s deltas until it lands (retried on the next return-leg frame and on the link keepalive tick; a fresher satellite resync replaces it), instead of the prior try_send fan-out that could drop the snapshot while a later delta slipped through. The reliability is scoped to the one attaching consumer — no link-wide head-of-line stall — so a sustained-slow consumer may still lag on content (the ordinary backpressure condition) but never observes a delta before its snapshot. (v45.13, SEIZE takeover notification) a ACQUIRE_INPUT { mode: SEIZE } takeover from a different hub consumer now notifies the evicted prior holder: the hub delivers it a re-tagged TerminalControl { action: SEIZED, input_holder: <new holder> } event (§9.1, the existing draft.6 terminal_control event tag 0x08), mirroring the local takeover broadcast. The satellite cannot raise this itself — the relayed SEIZE arrives under the shared link identity, so its own lease change reads as a same-identity re-acquire naming the link, not the evicted hub consumer — so before this fix the prior holder kept believing it held the wheel while its relayed INPUT_* was silently dropped at the hub ledger gate. Both are conformance fixes to already-normative §9.1 behavior; no frame type, field, tag, or error code is allocated or altered, and no existing bytes change. Draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.18 | 2026-07-10 | phux-v45.9 (ADR-0007): the SSH-stdio transport (§4), spec’d since 0.1.0 as ssh host phux serve --stdio, is realized and its normative wording corrected to the shipped verb: the dialing side invokes ssh host phux stdio-bridge, and the remote bridge process splices its stdin/stdout byte-transparently to the server’s Unix domain socket on the target host. There is no wire-format change: no frame type, field, tag, or error code is allocated or altered — the bridge never parses, frames, or injects bytes, so the server sees an ordinary local UDS client and the peer sees the identical length-prefixed framing (§5). Per §4’s transport-responsibility rule, authentication and confidentiality on this transport are SSH’s, and the bridge’s UDS connection sits under the socket’s owner-only permissions; no bearer token is carried (the ADR-0031/ADR-0038 token+pin machinery is TLS-transport material — recorded as a dated ADR-0038 addendum). First consumer: the federation hub’s outbound dialer for ssh:// satellite endpoints (phux server --hub), which spawns the system ssh (override: $PHUX_SSH) with BatchMode=yes and charset-validated argv. The draft.14 relay’s keepalive / idle-timeout reachability contract is met at the SSH layer on this transport (ServerAliveInterval/ServerAliveCountMax on the dial argv; an exited ssh child is a dropped link) — no in-band ping is added to the stream, keeping the bridge byte-transparent. Editorial/transport realization; PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.17 | 2026-07-10 | phux-v45.6 (ADR-0007 §4): satellite-targeted SPAWN_TERMINAL closes the second draft.14 deferral. SPAWN_TERMINAL (L1.md §3.1) gains an optional satellite: optional<str> field at field id 7 (appended after term’s id 6; a body that stops before it decodes as None per the field-tagged TLV rule, so no existing bytes change). None spawns on the receiving server, the only shape a non-federated consumer sends; Some(host) asks a federation hub to relay the spawn over host’s link — the relayed frame carries satellite: None (hub-and-spoke never chains) with a hub-allocated link-side request_id, and the satellite’s TERMINAL_SPAWNED correlates back with the new id re-tagged LOCAL { id } -> SATELLITE { host, id }, immediately routable through the hub by every draft.14 relayed verb. Failures stay typed inside the spawn reply: SpawnError grows two additive tags mirroring the error-code vocabulary into TERMINAL_SPAWNED.result - UNSUPPORTED_SATELLITE_ROUTE = 0x02 (empty body; non-hub server, or host absent from the registry) and SATELLITE_UNREACHABLE = 0x03 (UTF-8 diagnostic; link down/dialing/refused/unanswering within the relay deadline - fail-fast, never a hang). Normative routing semantics in L1.md §9.1 (“Satellite-targeted spawn”). Reference-implementation placement note (informative): a spawn from a non-attached consumer - the headless phux spawn CLI or a hub’s relayed spawn - now lands in the server’s most recently active session instead of being refused; wire shape is unchanged. Backs the new phux spawn [--satellite NAME] verb. Additive: one new optional field id, two new SpawnError tags (#[non_exhaustive]); no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.5.0). Scope: two-hop ATTACH remains deferred (phux-v45.7). |
| 0.5.0-draft.16 | 2026-07-10 | phux-v45.5 (ADR-0007 §4): aggregated LIST across satellites closes the first deferral from draft.14. On a federation hub, GET_STATE { scope: SERVER } now returns the hub’s local snapshot plus every dialed satellite’s terminals: the hub relays GET_STATE { scope: SERVER } over each link (queried concurrently, each bounded by the existing per-command relay deadline) and appends the returned panes re-tagged LOCAL { id } -> SATELLITE { host, id } (L1.md §9.1, “Aggregated LIST”). Result-shape honesty: only terminals aggregate — session/window identities are not federation-routable (ADR-0016), so satellite sessions/windows lists and focus fields are discarded and a satellite pane’s window_id is passed through verbatim (satellite-local, not joinable against the hub’s windows; consumers group satellite terminals by the host in their TerminalId). cols/rows/title/cwd are relayed verbatim; the hub synthesizes nothing. Per-satellite degradation: an unreachable satellite contributes an empty set and never fails the aggregate — the hub pushes one un-correlated ERROR { SATELLITE_UNREACHABLE } naming the host before the COMMAND_RESULT (the draft.14 observable-degradation shape) and the merged snapshot simply lacks that host’s terminals. Chained SATELLITE-tagged ids in a satellite’s own list are dropped. Semantic change only: no new frame, field, tag, or error code; no existing bytes change; draft bump (PROTOCOL_VERSION stays 0.5.0). Scope: satellite-targeted SPAWN_TERMINAL and two-hop ATTACH remain deferred (phux-v45.6/.7). |
| 0.5.0-draft.15 | 2026-07-10 | phux-v45.7 / phux-v45.11 (ADR-0007 §4): two-hop attach over the federation hub, riding two newly wired Command verbs from the reserved §5.1 catalog slots: ATTACH_TERMINAL { terminal_id } (tag 0x01) and DETACH_TERMINAL { terminal_id } (tag 0x02). ATTACH_TERMINAL is the per-Terminal output subscription: the server registers the caller, primes it with one authoritative TERMINAL_SNAPSHOT (which MUST precede the first TERMINAL_OUTPUT delta for that consumer), streams deltas from then on, and opens the attach-scoped INPUT_* / FRAME_ACK gates — without a session-scoped ATTACH (the input/ack gates now key on subscription, not session attachment). Idempotent (re-attach re-sends a fresh snapshot, no duplicate stream); does NOT resize (callers follow with TERMINAL_RESIZE); the catalog’s role_policy field is NOT yet encoded — absence means {PRIMARY, takeover: NEVER} per §8.1, and the field lands additively. DETACH_TERMINAL drops the caller’s per-Terminal output subscription and its per-Terminal SUBSCRIBE_EVENTS scope, idempotently. On a hub, ATTACH_TERMINAL relays with the consumer’s proxy subscription registered atomically with the command (rolled back on a satellite error); DETACH_TERMINAL resolves hub-side, and the hub relays a satellite-side detach only when the last proxy subscriber for that terminal is gone (also on consumer disconnect). Input-lease aliasing is resolved hub-side (L1 §9.1): all hub consumers share the link’s client identity on the satellite, so the hub keeps a per-(host, terminal) ledger of which consumer holds the relayed ADR-0033 lease and gates relayed ACQUIRE_INPUT / RELEASE_INPUT / ROUTE_INPUT / INPUT_* on it — a non-holder’s cooperative acquire and input refuse locally with INPUT_LEASE_HELD, and a non-holder’s release is the idempotent Ok without forwarding (forwarding would release the real holder’s satellite-side lease). Known, documented limitation: the satellite-side lease is connection-scoped, so a link redial frees it satellite-side while hub-consumer exclusion persists. Scope honesty: session-scoped two-hop ATTACH, LIST aggregation, satellite SPAWN_TERMINAL, and per-consumer link sub-identities stay deferred (phux-v45.5/.6, ADR-0038); FRAME_ACK across the hop remains a shared, loss-tolerant hint (hub consumers of one satellite terminal share one link-side ack stream). Additive: two reserved tags wired, no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.14 | 2026-07-10 | phux-v45.4 (ADR-0007 §4): hub->satellite frame relay replaces the blanket UNSUPPORTED_SATELLITE_ROUTE rejections on a federation hub. A phux server --hub now routes frames carrying TerminalId::SATELLITE { host, id } over its outbound satellite link (phux-v45.3): the id is rewritten to the satellite’s LOCAL { id } space and the frame forwarded verbatim (opaque relay, no VT re-encoding), covering the per-terminal COMMAND catalog (GET_SCREEN, ROUTE_INPUT, KILL_TERMINAL, GET_TERMINAL_STATE, SUBSCRIBE_TERMINAL_EVENTS, ACQUIRE_INPUT, RELEASE_INPUT, SIGNAL_TERMINAL, REPORT_ASKED, and per-host partitions of KILL_TERMINALS), the fire-and-forget INPUT_* / FRAME_ACK / TERMINAL_RESIZE frames, and terminal-scoped SUBSCRIBE_EVENTS; COMMAND.request_id never crosses the link (the hub remaps to its own link-side id space and correlates replies back). Return-leg frames (COMMAND_RESULT, correlated ERROR, EVENT, TERMINAL_OUTPUT, TERMINAL_SNAPSHOT, TERMINAL_CLOSED, BELL) are re-tagged LOCAL -> SATELLITE { host, id } before reaching the consumer; a SATELLITE-tagged id arriving from a satellite is dropped (hub-and-spoke never chains). One new ErrorCode is allocated: SATELLITE_UNREACHABLE = 107 (proto.md §9) - the typed, fail-fast reply when the named satellite’s link is down/dialing/refused, also pushed un-correlated to every proxy-subscribed consumer when a satellite link drops (subscriptions are then cleared; consumers re-subscribe). UNSUPPORTED_SATELLITE_ROUTE = 106 narrows to the configuration refusal: non-hub server, or host absent from the hub’s registry. Normative routing semantics land in L1.md §9.1, including the reachability bound: relayed COMMANDs carry a hub-side deadline and every link transport enforces a keepalive / idle-timeout contract, so a silently partitioned or never-answering satellite resolves as SATELLITE_UNREACHABLE instead of hanging the consumer. Scope: LIST aggregation, satellite-targeted SPAWN_TERMINAL, and two-hop ATTACH semantics are deferred (phux-v45.5/.6/.7). Additive: one new error code value, no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.13 | 2026-07-09 | phux-foz.5: the conventional L3 config-reload doorbell is documented (L3.md §3.8) and named in phux-protocol (CONFIG_RELOAD_KEY = "phux.config.reload/v1"). It is a pure signal key (Scope::Global): the value is an opaque writer-chosen nonce whose only job is to differ from the previous bytes (the reference server dedups equal-bytes SETs), and a subscribed consumer treats a non-tombstone METADATA_CHANGED as “re-read your local configuration now” — the configuration itself never crosses the wire; each consumer re-runs its own layered config load and MUST keep its previous configuration when the re-read fails (never half-apply). Tombstones are ignored. It rides the existing SET_METADATA/SUBSCRIBE_METADATA verbs — no new wire tag, no wire-format change (the server stores the bytes opaquely; “normative” constrains consumers so the doorbell’s meaning cannot drift). Backs phux config reload and the reference TUI’s in-place reload-config action (tui.md §4.3). PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.12 | 2026-07-09 | phux-foz.4: a new AgentEvent variant, cwd_changed (event tag 0x0a, appended after asked’s 0x09), carries the scoped Terminal’s new working directory (cwd: str, positional body) on the EVENT (0xB3) stream so a projection consumer (the cwd status widget) can render the focused pane’s live directory without kernel access or OSC-7 shell integration. The reference server sources it from the kernel cwd of the PTY child (the defaults.cwd-inheritance query: /proc/<pid>/cwd on Linux, proc_pidinfo on macOS), polled at OSC-133 D prompt boundaries and on output-idle, coalesced on change; consumers seed from ATTACHED’s TerminalInfo.cwd and refine from the stream. In the same pass the reference server begins emitting the long-allocated command_started (tag 0x00) / command_finished (tag 0x01) events — closing the draft.15 deferral — by scanning OSC-133 C/D prompt marks directly out of the raw PTY byte stream (chunk-split-safe stateful scanner), and command_finished.exit_code is now Some(n) when the shell integration reports one (OSC 133 ; D ; n ST); the field was allocated optional<i32> in draft.15, so no byte shapes change. Forward-compat: an older decoder skips tag 0x0a by its length prefix as AgentEvent::Unknown. Additive: no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.11 | 2026-07-09 | phux-dpop: interactive clients may append default_colors to the positional HELLO.client_caps value after output_mode as present: u8 plus foreground/background RGB24 bytes. Absent (legacy/non-TTY) defaults to None; the most recently attached palette-advertising client supplies the shared Terminal defaults, while palette-less attaches leave them unchanged. The reference client probes outer-terminal OSC 10/11 before raw mode and the server seeds its emulator before parsing a newly spawned child, allowing in-pane OSC 10/11 theme probes to match native execution. L1 also makes DEC synchronized output (CSI ?2026h/?2026l) atomic across arbitrary TERMINAL_OUTPUT boundaries: mirrors ingest continuously but paint only after the mode clears, with a bounded stuck-producer watchdog. Additive caps suffix; no existing bytes or tags change, and PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.10 | 2026-07-09 | phux-3ert (ADR-0040): the normative L3 schema for agent identity and lifecycle is documented (L3.md §3.7) and its conventional key is named in phux-protocol (TERMINAL_AGENT_KEY = "phux.agent/v1"). phux.agent/v1 (Terminal scope) holds a UTF-8 JSON object {name: str, kind?: str, state?: str, attention?: str, session?: str}; state (unknown/idle/working/blocked/done) and attention (none/low/normal/high) are OPEN string enums — an unrecognized value reads as unknown/normal, never a parse failure, so the vocabulary grows additively. It rides the existing SET_METADATA/GET_METADATA/DELETE_METADATA/SUBSCRIBE_METADATA verbs — no new wire tag, no wire-format change (the server stores the bytes opaquely; “normative” constrains consumers so agent-identity meaning cannot drift). A consumer that finds the record MUST prefer it over OSC-title/screen heuristics (the ADR-0035 phux-ask sentinel and the phux agent detector remain compatibility fallbacks). Backs phux agent set/clear and structured sidebar/tab labels in the reference TUI. PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.9 | 2026-06-21 | phux-84pb / ADR-0034: Kitty graphics now survive the cell-renderer projection with no wire-format change. Implementations that negotiate ImageProtocol::KittyGraphics continue to receive opaque APC _G bytes on TERMINAL_OUTPUT; the change is that client/server libghostty mirrors enable Kitty image storage + PNG decode, TERMINAL_SNAPSHOT.vt_replay_bytes may now include synthesized Kitty APC replay for live images, and the client renderer re-emits stored image placements from its local engine while still painting cells through the existing grid walker. Capability downsampling remains authoritative: snapshot replay bytes are run through the same image-protocol gate before reaching clients that did not advertise Kitty graphics. |
| 0.5.0-draft.8 | 2026-06-17 | phux-2sl6: a new AgentEvent variant, asked (event tag 0x09, appended after terminal_control’s 0x08), carries an agent’s pending human-answerable question on the EVENT (0xB3) stream so a projection consumer can render the waiting prompt without re-deriving it from the grid. Unlike the other (positional) AgentEvent bodies, the asked body is itself field-tagged TLV (field_id: varint || wire_type: u8 || length-delimited value, skip-by-length on an unknown id), so the suggestion list and the optional elapsed counter are additive: id (field 1, str — the stable id the answer correlates against), question (field 2, str), suggestion (field 3, str, repeated once per suggestion in order, absent for none — the actual options, not yes/no), elapsed_seconds (field 4, u64, optional — absent = 0/unknown). Forward-compat: an older decoder that does not know tag 0x09 reads the event’s outer length prefix and surfaces it as AgentEvent::Unknown { tag, body } (preserved verbatim) rather than failing the frame parse. Mirrors the projection consumer’s pending-question model one-for-one. Additive: no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.7 | 2026-06-17 | Spec/code coherence clarification, no wire-format change. The normative encoding of a leaf str / bytes (a string or byte slice carried inside a positional value) is documented as a u32 big-endian length prefix (proto.md §Conventions; appendix-encoding.md §2.1), matching the reference codec (Encoder::write_bytes / Decoder::read_bytes, which have always written/read a u32 BE count). The spec previously described this leaf prefix as a varint, which never matched the shipped bytes. The varint length stays correct for the field-tagged TLV envelope (the BYTES wire-type field length of appendix-encoding.md §1) — that is a separate length, unchanged. Wire bytes are unchanged; this corrects the spec to the code. Also corrected: proto.md §6 said “version 0.4.0” (now 0.5.0); the ErrorCode catalog (proto.md §9) is reconciled with the shipped #[non_exhaustive] enum — OUT_OF_TIER = 5, UNSAFE_PASTE = 203, and the renamed COLLECTION_NOT_FOUND / METADATA_KEY_NOT_FOUND are marked reserved / not-yet-emitted with the shipped names noted, no byte changes. PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.6 | 2026-06-16 | phux-take-the-wheel (ADR-0033): supervisory input authority + process signals. Three Command verbs appended to the §5.1 catalog: ACQUIRE_INPUT { terminal_id, mode: InputMode, ttl_ms: u32 } (tag 0x0f), RELEASE_INPUT { terminal_id } (tag 0x10), SIGNAL_TERMINAL { terminal_id, signal: TerminalSignal } (tag 0x11), all riding the existing COMMAND / COMMAND_RESULT envelope. InputMode = COOPERATIVE = 0 | SEIZE = 1; TerminalSignal = INTERRUPT = 0 | FREEZE = 1 | RESUME = 2 | TERMINATE = 3 | KILL = 4. ACQUIRE_INPUT asserts an exclusive input lease over a Terminal — while held, only the holder’s INPUT_* / ROUTE_INPUT reaches the PTY; others are dropped but still acked (the §9 fire-and-forget input invariant holds). COOPERATIVE fails with the new ERROR { INPUT_LEASE_HELD = 204 } (proto §14; 203 was already reserved for UNSAFE_PASTE) when another client holds it; SEIZE preempts. ttl_ms is advisory (the reference server holds until release/disconnect). SIGNAL_TERMINAL delivers a POSIX signal to the Terminal’s process group — distinct from KILL_TERMINAL (which removes the pane); FREEZE (SIGSTOP) / RESUME (SIGCONT) is the reversible brake. A new AgentEvent tag terminal_control = 0x08 is broadcast to every SUBSCRIBE_TERMINAL_EVENTS subscriber on each lease change and lifecycle transition: body { lifecycle: TerminalLifecycle (RUNNING=0|FROZEN=1|EXITED=2), exit_status: optional<i32>, input_holder: optional<ClientId>, action: ControlAction (acquired=0..exited=8), actor: optional<ClientId> }; it bypasses the per-subscriber type filter. Additive: no tag renumbered, no existing bytes changed; the new Command / AgentEvent tags and ErrorCode (which is #[non_exhaustive]) decode as unknown on an older peer. Draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.5 | 2026-06-11 | phux-y8v6 (ADR-0007): QUIC added as a third concrete transport (§4), alongside the Unix domain socket and SSH-stdio. It carries the identical length-prefixed frames (§5) over a single bidirectional QUIC stream — a reliable, ordered octet stream — so there is no wire-format change: no frame type, field, tag, or error code is allocated or altered, and the FrameKind codec is byte-for-byte the same as on the UDS/WebSocket paths. TLS 1.3 is intrinsic to QUIC (confidentiality + server identity via a pinned self-signed cert); a routable listener additionally authenticates each attachment with a bearer token the dialer sends as the opening preamble of its stream (len: u32 BE + token bytes, consumed at connection establishment before any frame — the QUIC analogue of the wss:// Authorization: Bearer header, and a transport responsibility per §10, not a protocol frame). Opt-in via phux server --quic <HOST:PORT> / $PHUX_QUIC_ADDR; the UDS stays always-on. Implemented with quinn (the stack ADR-0007 names) on the existing rustls 0.23 + ring provider. Editorial/transport addition; PROTOCOL_VERSION stays 0.5.0. |
| 0.5.0-draft.4 | 2026-06-11 | phux-fak5 (ADR-0032): UPGRADE command allocated at Command tag 0x0e (L1 §5.1 catalog, appended after SUBSCRIBE_TERMINAL_EVENTS’s 0x0d). It is a bare trigger — no payload — riding the existing COMMAND / COMMAND_RESULT { Ok } envelope. It asks the server to graceful-upgrade itself in place: snapshot every pane, re-exec the on-disk binary, and re-adopt the live PTYs so sessions (shells, editors, agents) survive a binary update. The handoff state blob (the session/window/pane tree + per-pane PTY descriptors + replay snapshot) is built and passed entirely server-side through an inherited descriptor — it never crosses the wire — so no new frame types or field encodings are introduced. Clients observe a brief disconnect and reconnect (ATTACH resyncs via TERMINAL_SNAPSHOT). The reply acks Ok best-effort before the re-exec. Backs phux upgrade. Additive: no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.3 | 2026-06-11 | phux-ign: SPAWN_TERMINAL (L1 §3.1) gains a first-class optional term: optional<str> field at field id 6 (appended after env’s id 5; a body that stops before it decodes as None per the field-tagged TLV skip-by-length rule, so no existing bytes change). It is a typed per-spawn TERM override — a consumer can advertise a specific terminfo entry for the new Terminal without hand-rolling a TERM env pair. The reference server applies it as a TERM-resolution tier between defaults.term and the wire env: lowest-to-highest the order is compiled-in DEFAULT_TERM → server defaults.term → SPAWN_TERMINAL.term field → a TERM entry inside SPAWN_TERMINAL.env (applied last, so an explicit env pair still wins over the field). term = None defers to defaults.term. Additive: no field renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.2 | 2026-06-10 | Pixel-geometry semantics documented (L1 §9.2.1; conformance fix, no wire-format change). The pixel_w / pixel_h viewport fields (allocated since 0.1.0) now have defined server-side meaning: the server derives a per-cell pixel size (pixel / cells, floored) from the most recent usable pixel report among a Terminal’s subscribers — recency, not the window-size policy, since cell pixel size is a property of one physical display — and applies cells x cell size to the PTY winsize pixel fields, XTWINOPS size replies (CSI 14/16/18 t), and mode-2048 in-band size reports. The reference server previously discarded the pixel fields at the PTY boundary (winsize pixels hardcoded 0x0) and dropped all terminal-generated replies (no libghostty write_pty / size effect handlers), so XTWINOPS size queries, DECRQM mode reports, and CSI 21 t title reports were silently swallowed — pixel-aware programs (kitten icat) refused to run inside phux. Reports without usable pixel metrics never displace an established cell size; with no report yet the server advertises zero pixels (a terminal that does not know its pixel geometry). No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0-draft.1 | 2026-06-09 | phux-3ns5 (conformance fix, no wire-format change): the post-resize grid resync now ships as TERMINAL_SNAPSHOT (L1 §4.4 case 3, “the grid resized in a way that requires full retransmission”) instead of being folded into the raw TERMINAL_OUTPUT byte stream. The reference server’s resize resync (phux-8v1) previously broadcast the synthesized post-reflow grid through the live output pump, so it arrived as TERMINAL_OUTPUT; a client applies TERMINAL_OUTPUT with vt_write but resizes its mirror Terminal ONLY on TERMINAL_SNAPSHOT (the mirror grid size is server-authoritative, never inferred from a client rect — phux-wurs). Net effect of the old path: a resize that GREW a pane (kill-pane reflow promoting the survivor, or enlarging the outer viewport) left the client mirror frozen at its old, smaller size and the freed space blank. The actor’s output broadcast now carries a PaneOutput::{Live, Resync} enum; the per-attach pumps map Live → TERMINAL_OUTPUT and Resync{cols,rows,bytes} → TERMINAL_SNAPSHOT, so the client resizes its mirror to the post-reflow dims and repaints from authoritative state. No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.5.0). |
| 0.5.0 | 2026-06-07 | phux-q1ni (ADR-0030): the INPUT_SELECTION frame (C→S 0x15), its Selection input-event tag (0x04, used inside ROUTE_INPUT’s InputEvent), and the SelectionEvent / SelectionMode wire types are REMOVED. Selection is a client-side projection over the consumer’s own libghostty engine, never a wire tier (ADR-0030 decision 1): the client builds a Selection from its own Terminal, formats the text with format_selection_alloc, and copies it locally via OSC 52 (phux-v6jw) — no server round-trip and no per-client server selection state. The reference server’s dead per-client/per-Terminal selection storage (SelectionSpan, client_selections, set/get/clear_selection) is deleted with it. The 0x15 frame discriminant and the 0x04 InputEvent tag are retired (left unassigned). Note: INPUT_SELECTION was never documented as a normative frame in this spec — it existed only in the reference code and was never wired to a server handler — so this removal closes a code/spec gap rather than retiring a spec’d surface. Removing a wire frame is wire-breaking, so PROTOCOL_VERSION bumps 0.4.0 → 0.5.0 (pre-1.0 minor). |
| 0.4.0-draft.1 | 2026-06-07 | phux-p0yq (ADR-0027 decision point 4): the normative L3 schema for terminal tags and links is documented (L3.md §3.6) and its two conventional keys are named in phux-protocol (TERMINAL_TAGS_KEY = "phux.tags/v1", TERMINAL_LINK_KEY = "phux.link/v1"). phux.tags/v1 (Terminal scope) holds a UTF-8 JSON array of tag strings; phux.link/v1 (source-Terminal scope) holds a JSON array of {target: u32, kind: str} link records with an OPEN kind enum (v1: "group"). Both ride the existing SET_METADATA/GET_METADATA/LIST_METADATA/SUBSCRIBE_METADATA verbs — no new wire tag, no wire-format change (the server stores the bytes opaquely; “normative” constrains consumers so tag/link meaning cannot drift). Backs the #tag selector (tui.md §3) and the phux tag command. PROTOCOL_VERSION stays 0.4.0. |
| 0.4.0 | 2026-06-07 | Field-tagged TLV wire migration. Every message body changes from positional, fixed-order fields to field-tagged TLV per Appendix A: each top-level field is now field_id: varint || wire_type: u8 || length-delimited value. Decoders match top-level fields by stable id and skip any id they do not recognise by its declared length (the forward-compat lever — replacing the old positional “append-only trailing fields with defaults” rule). Optional / trailing fields become simply-absent tagged fields: an absent id decodes to the documented default (e.g. ERROR.request_id, TERMINAL_SNAPSHOT.scrollback_bytes, SPAWN_TERMINAL.command/cwd/env, METADATA_CHANGED/METADATA_VALUE.value, the SUBSCRIBE_EVENTS/EVENT terminal scope, and the trailing HELLO/HELLO_OK capability/version fields). Field-id allocation discipline: ids are per message, start at 1, run contiguously in field-declaration order, and are stable within a major version — an additive minor change appends a new id but MUST NOT renumber or reuse one; a removed field’s id is retired. Nested tagged unions and sub-records (TerminalId, ViewportInfo, AttachTarget, Scope, Command/CommandResult/CommandValue, SpawnResult, AgentEvent, SessionSnapshot, LayoutNode) stay positional inside a field’s length-delimited value; only the message body itself is field-tagged. The single wire_type phux emits at the top level is BYTES (4, length-delimited), so an unknown field always skips by length regardless of its logical type. Every body’s bytes change (the reference snapshot fixtures in §16 / Appendix A re-baseline accordingly), so this is wire-breaking: PROTOCOL_VERSION bumps 0.3.0 → 0.4.0 (pre-1.0 minor). |
| 0.3.0 | 2026-06-06 | ”Option B” wire re-tier (ADR-0019 / ADR-0027): the L2 collection lifecycle tier is dissolved. The three leaked L2 lifecycle commands are REMOVED from the §5.1 catalog and their tags freed: CREATE_SESSION (0x09), KILL_COLLECTION (0x0a), RENAME_SESSION (0x0b). In their place a single atomic multi-terminal op is added: KILL_TERMINALS { ids: list<TerminalId> } (Command tag 0x09, reusing the freed CREATE_SESSION slot; rides the existing COMMAND / COMMAND_RESULT { Ok } envelope, no new frame discriminant). Its body is a u16 count prefix followed by that many tagged TerminalIds. The server tears down every listed Terminal inside one state-lock acquisition — all-or-nothing for a local server; unknown / already-dead ids are skipped silently (idempotent), so an empty list is a no-op that still acks Ok. The async TERMINAL_CLOSED frames confirm teardown, as for KILL_TERMINAL. Grouping (session membership + names) moves to L3 metadata + client logic: session rename is now a SET_METADATA write of the conventional key phux.session.name/v1 (Scope::Global, value current\0new UTF-8), and create-without-attach is a SET_METADATA write of phux.session.create/v1 (value JSON {name, command?, cwd?}) whose seed-pane id the server publishes under phux.session.created/v1 for the caller to read back via GET_METADATA (SET_METADATA has no reply frame). The server intercepts these conventional keys and applies the authoritative registry mutation so ls / attach keep one source of truth for names. CollectionId is RETAINED as a documented opaque grouping key (not a lifecycle tier): it is still threaded through Scope::Collection, SpawnTerminal.collection, and CommandValue::CollectionId; full removal is a follow-up bead. Removing wire verbs is wire-breaking, so PROTOCOL_VERSION bumps 0.2.0 → 0.3.0 (pre-1.0 minor). |
| 0.2.0-draft.17 | 2026-06-03 | phux-fseo: consumers negotiate their server output emitter via a new output_mode: OutputMode field on ClientCapabilities (§6.2). OutputMode is an additive enum (u8) — Raw = 0 (raw PTY byte broadcast; the byte-faithful, low-latency human-TUI default) and StateSync = 1 (per-consumer synthesized grid-delta tick, ADR-0018; for agent / remote state-sync consumers). It rides the HELLO ClientCapabilities codec as one trailing positional byte after hyperlinks; a body that stops before it decodes as Raw, and an unknown tag also falls back to Raw (#[non_exhaustive]), so no existing bytes change and a pre-fseo HELLO keeps the raw path. Server-side, a StateSync consumer is tick-managed (the actor’s per-consumer tick_emit is its sole emitter and the broadcast pump is suppressed for it); a Raw consumer keeps the broadcast pump. Mixed-mode consumers can share one pane without double-painting. Raw stays the human default because synthesized ticks add a local-typing latency floor and can lose byte-exact styling (phux-yeca). Deferred: per-attach (rather than per-connection) mode switching, and the FRAME_ACK-accounting cleanup for raw-vs-synth emitters (phux-38k6). Additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.16 | 2026-06-03 | HELLO_OK (§6.1, S→C 0x80) gains its spec’d body. Previously the discriminant was reserved and the reference server replied with nothing after HELLO; now the server sends HELLO_OK { protocol_major: u16, protocol_minor: u16, protocol_patch: u16, server_caps: ServerCapabilities, server_id: bytes } before processing ATTACH, and the client REQUIRES it (was “proceed optimistically”). The body mirrors HELLO’s positional encoding: the version triple is fixed-width, then trailing length-skippable fields — server_caps.layers (one byte, the LayerSet bitset the server mounts; the negotiated tier set is the intersection with the client’s layers per ADR-0015) and a length-prefixed opaque server_id. A shorter (version-only) body decodes with ServerCapabilities::default() (L1) and an empty server_id per the §6 “skip them by length” rule, so the shape grows monotonically. The reference server echoes its own PROTOCOL_VERSION as the selected version and advertises LayerSet::all(); server_id is the server process id. Deferred: version negotiation — selecting the highest mutually-supported version and refusing an incompatible client with ERROR { VERSION_INCOMPATIBLE } (code 1, already allocated) — is NOT yet enforced; the single-version server accepts any HELLO. Canonical wire dump pinned by the snap_hello_ok snapshot test (Appendix A). Also lifts PONG (§7.5, S→C 0xFF) from a server-local raw-byte encode into the FrameKind::Pong { nonce } variant — byte-identical on the wire, no format change. No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.15 | 2026-05-31 | phux-y2t (ADR-0022 ‘events’): the push half of the agent surface is wire-allocated (§7.5). proto discriminants SUBSCRIBE_EVENTS = 0x41 (C→S), EVENT = 0xB3 (S→C), from the events reserved ranges (Appendix B). SUBSCRIBE_EVENTS { terminal: optional<TerminalId> } opts a client into the server-pushed event stream (Some(id) = per-pane, None = server-scoped, e.g. cross-collection pane_spawned/pane_closed); it is idempotent, implicitly torn down on detach (matching SUBSCRIBE_METADATA), and does NOT attach/resize/snapshot. EVENT { terminal: optional<TerminalId>, event: AgentEvent } carries one extensible tagged event. AgentEvent is a #[non_exhaustive] tagged union encoded TLV (tag: u8 + length-prefixed body: bytes) — an unrecognised tag decodes as Unknown { tag, body } (body preserved verbatim) rather than failing the parse, so a later minor may add event kinds and an older client skips them. Tags allocated: command_started = 0x00 (empty), command_finished = 0x01 (exit_code: optional<i32>), title_changed = 0x02 (title: str), bell = 0x03 (empty), pane_spawned = 0x04 (empty; id on envelope), pane_closed = 0x05 (exit_status: optional<i32>), dirty = 0x06 (empty), idle = 0x07 (empty). The stream is an additive accelerator of the CLI poll-floor wait (match conditions stay client-side; an event just wakes the waiter sooner). Deferred: command_finished.exit_code is None from the reference server — the OSC-133 D-mark shell-integration exit code is not extractable from the grid cell-semantic projection the server reads today; the field is allocated so a future server fills it without a wire change. command_started/command_finished are emitted from the OSC-133 prompt marks the grid exposes; pane_spawned/pane_closed from the existing Terminal lifecycle; title_changed from libghostty’s OSC 0/2 title; bell from a BEL in the PTY stream; dirty/idle from the per-pane tick (coalesced). Backs phux watch [TARGET] [--json]. No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.14 | 2026-05-31 | phux-4r1 (ADR-0015 L1): PTY EOF / process exit is reshaped from a server-baked detach into the existing L1 lifecycle event TERMINAL_CLOSED { terminal_id, exit_status } (§1.1). The server now broadcasts TERMINAL_CLOSED (carrying the exit status) to every client subscribed to the dying Terminal and STOPS deciding detach — it no longer sends DETACHED on EOF. The “no Terminals left in my attached collection ⇒ detach this client” policy moves OUT of the server runtime and INTO the consumer: the reference TUI folds the closed Terminal out of its layout and detaches itself when the last pane closes (behaviorally identical to the prior server-baked path for v0.1 single-pane, now consumer-owned and multi-Terminal-ready). No new frame, field, tag, or error-code allocation — TERMINAL_CLOSED (S→C 0xA1) and its exit_status: optional<i32> body were wire-allocated in draft.2; this is the server-emission + consumer-policy wire-up plus a §1.1 normative clarification. No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.13 | 2026-05-30 | rename-session (ADR-0021 §3): RENAME_SESSION { collection: CollectionId, name: str, new_name: str } command appended to the §5.1 catalog at tag 0x0b (after KILL_COLLECTION’s 0x0a). The rename counterpart to CREATE_SESSION: resolves the session named name under collection (the same name resolution KILL_COLLECTION uses) and reassigns its human-readable name to new_name in one round-trip. Reply rides the existing COMMAND_RESULT { OK } envelope (no new CommandValue); the server is authoritative and each attached client reconciles the new name on its next ATTACHED snapshot. An unknown name is refused with ERROR { SESSION_NOT_FOUND }; a new_name already in use is refused with ERROR { INVALID_COMMAND } (the same code CREATE_SESSION uses for a taken name). v0.1 servers host only the default CollectionId(1). A live SESSION_RENAMED push to other attached clients is out of scope for this pass (snapshot reconciliation covers it). Backs phux rename SESSION NEW-NAME and the TUI rename-session action (prefix-table $). No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.12 | 2026-05-30 | phux-nlo: editorial clarification, no wire change. ROUTE_INPUT (§5.1 / tag 0x08) is PRIMARY-only input authority — input.md §7 already mandates the PRIMARY/VIEWER gate and PERMISSION_DENIED for ROUTE_INPUT; L1.md §5.1 now lists ERROR { PERMISSION_DENIED } alongside the existing ERROR { TERMINAL_NOT_FOUND } in the ROUTE_INPUT reply set so the error catalog is complete. No frame, field, tag, or error-code allocation; PERMISSION_DENIED (201) already exists. PROTOCOL_VERSION stays 0.2.0. |
| 0.2.0-draft.11 | 2026-05-30 | phux-h9s (ADR-0021 §3): KILL_COLLECTION { collection: CollectionId, name: str } command appended to the §5.1 catalog at tag 0x0a (after CREATE_SESSION’s 0x09). The teardown counterpart to CREATE_SESSION: destroys the named session under collection, tearing down every Terminal it owns in one round-trip — the same effect as a KILL_TERMINAL per pane, but resolved server-side rather than over N client round-trips. Reply rides the existing COMMAND_RESULT { OK } envelope (no new CommandValue; the async TERMINAL_CLOSED frames confirm teardown), issued as soon as the teardown begins. An unknown collection is refused with ERROR { INVALID_COMMAND }, an unknown name with ERROR { SESSION_NOT_FOUND }; v0.1 servers host only the default CollectionId(1). Backs phux kill SESSION, collapsing its prior N KILL_TERMINAL round-trips into one (window / pane / @id targets keep the per-KILL_TERMINAL path). No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.10 | 2026-05-29 | phux-fdh (ADR-0021 §3): CREATE_SESSION { collection, name, command: optional<list<str>>, cwd: optional<str> } command appended to the §5.1 catalog at tag 0x09 (after ROUTE_INPUT’s 0x08). Creates a named session under a collection and seeds its primary pane without attaching, subscribing, or resizing — the create-only counterpart to the always-attaching ATTACH { CreateIfMissing } path. The server allocates the session + seed pane atomically, so two concurrent CREATE_SESSION for the same name cannot both succeed (closing the GET_STATE→ATTACH TOCTOU window the v0.1 client-side always-new logic carried). Reply rides the existing COMMAND_RESULT { OK_WITH(TERMINAL_ID(..)) } envelope carrying the seed pane’s TerminalId, asynchronously correlated by request_id (the same shape SPAWN uses, session-level); no new CommandValue variant — TerminalId tag 0x00 is reused. A name already in use or an unknown collection is refused with ERROR { INVALID_COMMAND } (create-only, never create-or-attach; v0.1 servers host only the default CollectionId(1)). Backs phux new --json (create + print id, no attach). No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.9 | 2026-05-29 | phux-8yl (ADR-0022 §2): GET_SCREEN gains a trailing additive cells: bool field (one byte, appended after request_scrollback; an absent byte on a pre-phux-8yl body decodes as false, so no existing bytes change). When true, the COMMAND_RESULT { OK_WITH(JSON(..)) } reply’s ScreenState carries a new additive cells? array: per-cell OSC-133 semantic marks + styles for the viewport, sparse (only cells with a non-default style or a semantic mark, row-major, wide-cell tails skipped). Each CellInfo is { col, row, semantic?, style }; semantic is present only for shell-integration input / prompt cells (libghostty’s default output collapses to absent); style carries the bool attribute set (bold/faint/italic/underline/blink/inverse/invisible/strikethrough/overline) plus tagged fg/bg ({ kind: "default" | "palette" | "rgb", … }). ScreenState.schema_version bumps 2 → 3; cells is serde-default + skip_serializing_if (a cells = false snapshot serializes to exactly the pre-phux-8yl shape, no key), so a v2 consumer ignores it. Backs the new phux snapshot --cells flag. Additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.8 | 2026-05-29 | phux-o1v (ADR-0022): GET_SCREEN gains a trailing additive request_scrollback: optional<u32> field (presence-byte + u32, appended after terminal_id; no existing bytes change). None reads the viewport only (the prior shape), Some(0) reads all retained history, Some(n) reads the most-recent n history rows. Requested history rides the existing COMMAND_RESULT { OK_WITH(JSON(..)) } reply in a new additive ScreenState.scrollback[] field (oldest first, right-trimmed); the server reads history cells in place (Point::History grid refs), so the read stays side-effect-free. ScreenState.schema_version bumps 1 → 2; scrollback[] is serde-default so a v1 consumer ignores it. Backs the new phux snapshot --scrollback[=N] flag (bare flag = all). Additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.7 | 2026-05-29 | phux-3j3 (ADR-0022): ROUTE_INPUT { terminal_id, event: InputEvent } command appended to the §5.1 catalog at tag 0x08 (after GET_SCREEN’s 0x07). InputEvent tagged union allocated (Key = 0x00, Mouse = 0x01, Focus = 0x02, Paste = 0x03), reusing the INPUT_KEY/INPUT_MOUSE/INPUT_FOCUS/INPUT_PASTE atom encodings. Delivers an already-built input event to a Terminal without an ATTACH, subscription, or resize — the write counterpart to the side-effect-free GET_SCREEN read; unlike the attach-then-INPUT_KEY path it never resizes the live pane to the caller’s viewport. Reply rides the existing COMMAND_RESULT { OK } (or ERROR { TERMINAL_NOT_FOUND } for an unknown id). Allowed for primaries; input fire-and-forget per §9 (a full mailbox drops the event but still acks OK). No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.6 | 2026-05-29 | phux-oki (ADR-0022 §5): GET_SCREEN { terminal_id } command appended to the §5.1 catalog at tag 0x07 (after RUN_HOOK’s reserved 0x06). Reply rides the existing COMMAND_RESULT { OK_WITH(JSON(str)) } envelope carrying a serialized ScreenState ({ schema_version, pane, cols, rows, cursor?, lines[] }) — the stable agent-surface read contract; no new CommandValue variant. Side-effect-free: the server walks its own emulator grid, so unlike ATTACH_TERMINAL it neither subscribes the caller nor resizes the Terminal (safe to poll). Allowed for viewers. No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.5 | 2026-05-28 | phux-k61 (ADR-0021): the generic COMMAND / COMMAND_RESULT envelope (§5) is wire-allocated. proto discriminants COMMAND = 0x31 (C→S), COMMAND_RESULT = 0xC2 (S→C), from the control-plane reserved ranges (Appendix B). Command tagged union follows the §5.1 catalog order (SPAWN = 0x00 … RUN_HOOK = 0x06); v0.1 implements KILL_TERMINAL = 0x03 and GET_STATE = 0x05 only — other tags decode as UnknownEnumValue. CommandResult allocated (Ok = 0x00, OkWith(CommandValue) = 0x01, Error(ErrorCode, str) = 0x02); CommandValue allocated (TerminalId = 0x00, CollectionId = 0x01, State(SessionSnapshot) = 0x02, Json(str) = 0x03, Bytes = 0x04); StateScope allocated (Server = 0x00). CommandValue::State reuses the existing SessionSnapshot wire shape. Control verbs route through this envelope; phux new reuses ATTACH { CreateIfMissing } rather than a command. No existing bytes changed; additive draft bump (PROTOCOL_VERSION stays 0.2.0). |
| 0.2.0-draft.4 | 2026-05-27 | phux-4rj: ClientCapabilities HELLO payload appends images: bitset<ImageProtocol>, kbd_protocols: bitset<KeyboardProtocol>, and hyperlinks: bool after the existing color and layers bytes. Server downsampling now gates sixel/kitty/iTerm2 image escapes, kitty keyboard APC replies, and OSC 8 hyperlink framing directly from the negotiated ClientCapabilities. The phux-i58 TLV migration remains out of scope. |
| 0.2.0-draft.3 | 2026-05-27 | phux-dmb: RolePolicy added to ATTACH and ATTACH_TERMINAL as an additive field. Terminal subscriptions now have PRIMARY / VIEWER roles; only the primary may send input or terminal-mutating commands. TakeoverPolicy::DELIBERATE is required to displace an existing primary; silent takeover is forbidden. |
| 0.2.0-draft.2 | 2026-05-27 | phux-4li.10: L1 Terminal lifecycle frames wire-allocated (§7.2.1). C→S discriminants SPAWN_TERMINAL = 0x22, TERMINAL_RESIZE = 0x23; S→C TERMINAL_CLOSED = 0xA1 (honours the spec-only reservation from §7.2), TERMINAL_SPAWNED = 0xA2. SpawnResult and SpawnError tagged unions allocated (Ok = 0x00/Err = 0x01 for SpawnResult — the convention extends to future Result<T, E> reply frames; CollectionNotFound = 0x00/SpawnFailed(str) = 0x01 for SpawnError, both #[non_exhaustive]). TERMINAL_CLOSED.exit_status: optional<i32> is a deliberately compact subset of §10.1’s ExitStatus tagged union — Some(n) for _exit(n), None for signal kills and unknown causes; the wider tagged union grows in a follow-up if needed. TERMINAL_RESIZE is per-Terminal PTY resize, sent in addition to VIEWPORT_RESIZE. Appendix B reserved-range guidance updated for 0x24..=0x2F and 0xA3..=0xAF. Server / client wire-up lands in follow-up tickets; the codec is wire-complete on this commit. |
| 0.2.0-draft.1 | 2026-05-27 | phux-4li.2: L3 metadata frames wire-allocated. C→S discriminants GET_METADATA = 0x50, SET_METADATA = 0x51, DELETE_METADATA = 0x52, LIST_METADATA = 0x53, SUBSCRIBE_METADATA = 0x54; S→C METADATA_CHANGED = 0xD0. Scope tagged union allocated (Terminal tag 0x00, Collection tag 0x01, Global tag 0x02). METADATA_CHANGED carries the new value inline (optional<bytes>) — supersedes the earlier “consumers re-GET after notification” sketch. ClientCapabilities.layers now wire-encoded as a trailing u8 after color_support (additive trailing field per SPEC §6, no version bump beyond the L3 allocation). CollectionId(u32) allocated; L2 (which defines its full tagged-union shape) is still TBD. Reply path for GET/LIST defers to the COMMAND_RESULT envelope (§11). |
| 0.2.0-draft | 2026-05-27 | phux-vp0.4: TerminalId becomes a tagged union (LOCAL { id: u32 } tag=0, SATELLITE { host: str, id: u32 } tag=1) per ADR-0016. Every TerminalId field on the wire gains a 1-byte tag prefix; the reference snapshot fixtures (§16) re-baseline accordingly. ErrorCode::UnsupportedSatelliteRoute = 106 is added (was already SPEC-reserved). v0.1 servers only construct LOCAL; v0.1 decoders MUST accept SATELLITE and, if not a federation hub, reply ERROR { UnsupportedSatelliteRoute }. PROTOCOL_VERSION bumped 0.1.0 → 0.2.0 (pre-1.0 wire break). |
| 0.1.0-draft.7 | 2026-05-26 | L1 vocabulary cascade Wave C (phux-vp0.2). §7 catalog reorganized by tier (proto / L1 / L2 / L3); §7.L1 messages renamed PANE_* → TERMINAL_* and pane_id → terminal_id per ADR-0016 (wire bytes unchanged). §7.3/§7.4 declare L2 (Collections) and L3 (Metadata) as reserved tiers with TBD discriminants. §6.1 HELLO gains layers: bitset<Layer> inside ClientCapabilities / ServerCapabilities (Appendix A field-tag extensibility keeps the wire compatible). §10 collapses Sessions/Windows/Panes/Layout/Focus into §10.1 Terminal lifecycle and §10.2 Viewport resize; the demoted TUI vocabulary lands non-normative in new §17. §6.2 reclaims the CC_FRONTEND capability slot per ADR-0017. §14 renames PANE_NOT_FOUND → TERMINAL_NOT_FOUND (numeric discriminant 104 preserved). §16 conformance restructured per-tier (16.0 common, 16.1 L1, 16.2 L1+L3, 16.3 L1+L2+L3). No wire bytes changed; no version bump. |
| 0.1.0-draft.6 | 2026-05-26 | Editorial: §7.1 / §7.2 message catalogs grow a Tier column mapping each message to its ADR-0015 layer (proto / L1 / L2 / tui→L3 / cmd); legend and tier-notes added. Previews the layered restructure that will rename PANE_* → TERMINAL_* (ADR-0016) and demote WINDOW_* / LAYOUT_CHANGED / FOCUS_CHANGED out of the wire (ADR-0017). No bytes changed. |
| 0.1.0-draft.5 | 2026-05-26 | Editorial: §7.1 / §7.2 message catalogs grow a Status column tracking reference-implementation coverage (informative, non-normative). No wire change. |
| 0.1.0-draft.4 | 2026-05-26 | Post-ADR-0013 cleanup: §2 Frame term re-anchored on per-pane seq; §6.2 inline comment on deprecated RenderingMode; §11.1 ADR cross-reference points at ADR-0013; §12 flow control rewritten for PANE_OUTPUT / per-pane seq (was PANE_DIFF / frame_id); Appendix B reserved-range guidance drops DiffOp. |
| 0.1.0-draft.3 | 2026-05-25 | §8 rewritten for bytes-on-wire pane state sync; PANE_DIFF superseded by PANE_OUTPUT; PANE_SNAPSHOT carries vt_replay_bytes; §6.2 capability downsampling described as a server-side VT byte stream rewrite; §13 replay sequence and §16 conformance updated. ADR-0013. |
| 0.1.0-draft.2 | 2026-05-24 | §7.7, §9, §10.5 revised to mirror libghostty input/OSC APIs. ADR-0006. |
| 0.1.0-draft | 2026-05-24 | Initial draft. Subject to change. |