Wire-protocol changelog
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.
0.8.0-draft.11
2026-09-06
phux-ypsa: new Terminal-scoped command TRANSCRIBE { upload_id, terminal_id }, tag 0x19 (appended after GET_PERF), gated on new ServerFeature bit TRANSCRIBE = 0x00002000 (0x1000 stays reserved for ADR-0098’s WORKLOAD_AUTH). It closes the voice loop for a client that cannot run a good speech model itself: the client uploads a clip with PUT_FILE (ADR-0059’s sandbox and size cap are reused unchanged), the server runs the operator’s [voice] transcriber argv on the landed file with the same process runner plugin actions use (a deadline, kill_on_drop, no shell, so wrapping a local whisper.cpp or speaches endpoint is one curl line and no HTTP client enters the server), and pastes the transcript into the Terminal as one acknowledged APPLY_INPUT under a server-minted operation id, so the paste is idempotent across a reconnect. The reply is COMMAND_RESULT { OkWith(Json) } with schema_version, text, and pasted, and the paste never submits, which leaves the confirm step with the user. Refusals are spelled out in L1.md §5.1: unconfigured (INVALID_COMMAND with a remedy), unknown or unfinished upload (INVALID_COMMAND), deadline (RESOURCE_EXHAUSTED), satellite route (UNSUPPORTED_SATELLITE_ROUTE), transcript over 64 KiB. Batch, not streaming, by design: release-to-talk then one request is how local dictation tools already behave, and partial hypotheses are a new frame type for another day. Additive per §6.3 and ADR-0061: one tag under a feature bit, no existing bytes change; PROTOCOL_VERSION stays 0.8.0.
0.8.0-draft.10
2026-09-03
ADR-0098 specifies the endpoint-neutral phux-workload/v1 authentication profile and its terminal mapping. Specification allocation only; the reference implementation remains spec-only, and PROTOCOL_VERSION does not bump yet. HELLO gains optional paired fields 6/7 (profile and fresh client nonce); WORKLOAD_RESPONSE = 0x04 and WORKLOAD_CHALLENGE = 0x84 take free lifecycle slots; HELLO_OK gains optional field 9 carrying the connection-bound grant; and ServerFeature::WORKLOAD_AUTH = 0x00001000 gates the additive surface. The exact Ed25519 transcripts bind service, negotiated version, both nonces, persistent server-authority key, volatile 128-bit incarnation, and a TLS-exporter or authenticated-UDS-peer channel. Endpoint-owned scope bytes are signed strictly; the terminal endpoint defines canonical TerminalScopeSet grants over six closed verbs and Terminal/Group/Host/Global selectors, plus a total frame/command matrix enforced before handlers and satellite relay. Registry intersection, expiry, live revocation, local/paired policy, strict duplicate/trailing/unknown-bit refusal, and the no-secret-in-argv/env/output/logs boundary are normative in workload-auth.md. Detach reasons 5–7 are reserved for authentication failure, authorization revocation, and expiry. This explicitly amends ADR-0031/proto §10’s former “no in-band auth” doctrine while retaining TLS/bearer transport admission.
0.8.0-draft.9
2026-09-02
ADR-0096: new session-scoped command GET_PERF { reset: bool }, tag 0x18 (appended after REPORT_AGENT_STATE), answered with COMMAND_RESULT { OkWith(Json(report)) } where the JSON is a phux_perf::PerfReport: the server’s always-on in-process latency histograms, throughput counters, gauges, and getrusage figures, with a schema_version of its own. Gated on new ServerFeature bit GET_PERF = 0x00000800. The report’s metric names are explicitly diagnostic, not contract: L1.md §5.1 requires a consumer to tolerate names coming and going and to ignore an unknown kind, which is what lets the server add a probe at a new hop without touching the wire. The command exists because the perf campaign (phux-l96p) landed eight optimisation commits and the maintainer still reported a laggy server: the tracing spans that would have said where the time went are debug-level, count-only, and turn on a synchronous file write inside the client’s paint loop when enabled, so the numbers were never available from a session someone was actually complaining about. Additive per §6.3 and ADR-0061: one new tag under a feature bit, no existing bytes change, and — deliberately, unlike 0.8.0’s REPORT_AGENT_STATE — no minor bump: §6.1 makes major.minor a hard admission test, and a bump here would lock every deployed 0.8 client (the paired phone included) out of a server whose only new behaviour is answering a question it was never asked before. PROTOCOL_VERSION stays 0.8.0.
0.8.0-draft.8
2026-09-02
AttachTarget::Last now has an authoritative no-touch resolution rule, no wire-format or ABI change. A live most-recently-touched session still wins. Before any touch, a server started with a configured seed MUST resolve Last to that live seed using the server’s effective configured identity; a client does not load or reproduce the server’s session-name template. With touch history but no live touched session, or with no live configured seed, the server returns SESSION_NOT_FOUND. Last remains lookup-only: neither the server nor a client-side fallback may turn it into CreateIfMissing. This lets native clients reconnect to a freshly started server by sending the existing Last tag once while preserving explicit creation authority.
0.8.0-draft.7
2026-09-02
phux-l96p.7: negotiated frame compression (proto.md §6.4). A remote first paint is bandwidth-bound on the native bootstrap prefix, which libghostty emits as whole engine pages at a fixed 16 bytes per cell: one dense scrolled 200x50 pane measures 522 KiB and four panes put 2 MiB on the wire before the user sees a character — 1.5 s on a 20 Mbit uplink. That payload is structural repetition and deflates 14x at level 1 (27x at level 6). The wire gains one server-to-client frame, FRAME_COMPRESSED = 0x9A from the 0x9A..=0x9F hot-path reserve, carrying algorithm, uncompressed_len, and the deflated image of one complete inner frame body; the receiver inflates it and dispatches the result, so the frame that reaches a consumer — a BOOTSTRAP_CHUNK payload above all — is byte-for-byte the frame the server encoded, preserving §6.2’s byte-identity rule for engine records. Negotiation is two additive top-level field ids that an unaware peer skips by length: HELLO.compression (id 6) offers a CompressionSet, HELLO_OK.compression (id 9) names the one selection. They sit beside the ClientCapabilities / ServerCapabilities sub-records rather than inside them precisely because §6.2 fixes those records’ byte order. Wrapping is per frame and at the sender’s discretion, so small frames and incompressible ones stay plain and a keystroke echo never reaches a compressor; a receiver treats wrapped and unwrapped frames on one connection as normal. Bounds are explicit: uncompressed_len is checked before any allocation against the larger of the two negotiated payload bounds plus a 64 KiB envelope allowance — not against the §5 frame cap, which would let a peer spend a few hundred bytes to force a 16 MiB allocation — the inflate must produce exactly that many bytes, and a nested envelope is refused. Capability-gated addition only (compatible per ADR-0061): a peer that offers nothing sees a byte-identical connection to the one it saw before this existed, no existing tag, field or record shape changes, and PROTOCOL_VERSION remains 0.8.0. The reference TUI offers on --remote / --quic / --ws and offers nothing over the local Unix socket, where deflating would spend CPU on both ends to save a memcpy.
0.8.0-draft.6
2026-08-17
phux-8uly: a new session’s seed pane now emits pane_spawned. §7.1’s event-sourcing list said pane_spawned was sourced from the Terminal lifecycle and then named only SPAWN_TERMINAL — which is accurate about panes added to an existing session and silent about the first pane of a new one. The reference server matched the narrower reading: both broadcasts lived inside the SPAWN_TERMINAL handler, so a session created by ATTACH { CreateIfMissing } or by the headless phux.session.create/v1 L3 write (the path phux new, phux worktree, and orchestrators take) announced nothing at all. The seed pane is now announced by both creation paths using the same envelope scope and the same empty body as the spawn path, so a subscriber cannot distinguish a seeded pane’s announcement from a spawned one’s, and §7.1’s bullet is rewritten to state the rule positively: pane_spawned covers every Terminal the server creates, not only those a client asked for by name. This closes the last push-coverage hole in the session lifecycle for a server-scoped (SUBSCRIBE_EVENTS { terminal: None }) follower — death was already carried by pane_closed, rename by draft.5’s METADATA_CHANGED, and creation by nothing, so a follower could not see a session that appeared after it subscribed until the new pane happened to emit some other event. Fanout remains best-effort and resolves subscribers at emit time, so the CLI poll floor stays the correctness guarantee and the event stream stays an accelerator. Additive event emission only (compatible per ADR-0061): no tag, field, verb or capability is added or altered, pane_spawned = 0x04 keeps its empty body, and PROTOCOL_VERSION remains 0.8.0.
0.8.0-draft.5
2026-08-16
phux-q7ks: an applied session rename now notifies subscribers. The reference server, after intercepting a SET_METADATA on phux.session.name/v1 (Scope::Global, value current\0new) and applying the registry rename, broadcasts a METADATA_CHANGED carrying the applied current\0new value to L3 subscribers of that (scope, key) — previously an applied rename emitted nothing, so a subscribed consumer (e.g. ADR-0089’s always-on session roster) kept displaying the dead name until a re-attach. A refused rename (unknown session, name taken) or a no-op rename (same name) still broadcasts nothing, and the intercepted value is not retained in the store: GET_METADATA / LIST_METADATA on the key are unchanged (absent). L3.md §3.1’s phux.session.name/v1 bullet is corrected in the same pass — it still described the pre-0.3.0 shape (GroupId scope, plain UTF-8 name) instead of the shipped Global + current\0new convention this log’s 0.3.0 entry records. Additive event emission only (compatible per ADR-0061): no tag, field, verb or capability is added or altered, and PROTOCOL_VERSION remains 0.8.0.
0.8.0-draft.4
2026-08-16
phux-2o3r: §5’s ERROR half is now implemented. §5 has always required a peer receiving a frame with length outside 1..=16_777_216 to send ERROR { code: FRAME_TOO_LARGE } and close the transport; every reader met the close half and skipped the ERROR half, closing silently. The server’s per-client read loop now answers a framing violation on any transport (UDS, WebSocket, QUIC, WebTransport) with ERROR { code: FRAME_TOO_LARGE } followed by DETACHED { reason: PROTOCOL_ERROR } before closing — the §14 fatal-error close order — detecting the violation once via the typed wire::framing error rather than per transport. The hub’s satellite links (QUIC, WebSocket, SSH-stdio) likewise send the ERROR frame, best-effort, before condemning a link whose peer broke framing, and the hub’s WebSocket link now applies the same one-message-one-frame check as every other message-oriented reader. §5 gains one clarifying sentence making that check normative: a message whose size disagrees with the length it declares is the same framing violation and receives the same treatment. Observable behaviour change, no wire-format change: new bytes appear on the wire only where a violating peer previously received silence before the close; no tag, field, verb or capability is added or altered. Additive error emission is compatible per ADR-0061, so PROTOCOL_VERSION remains 0.8.0.
0.8.0-draft.3
2026-08-15
phux-w7z2.28: corrected §3.7’s reference-server paragraph, which stated per-state manifest rule counts that the shipped manifests had outgrown — it claimed five working and zero idle rules where the binary now ships eight working, five blocked and one idle. The paragraph now also names that one idle rule (claude.toml’s osc-progress-idle) and distinguishes its evidence channel: it derives from Claude Code’s OSC 9;4 progress signal, on which the CLI positively states that progress was removed, not from the title or the screen. A title- or screen-derived positive idle rule remains declined for the reasons claude.toml records. done remains unreachable by derivation; ADR-0085’s REPORT_AGENT_STATE is still its only producer. A new unit test pins this sentence to the manifests so the count cannot silently drift again. No wire-format change: no tag, field, verb or capability is added or altered, and PROTOCOL_VERSION remains 0.8.0; this entry records a corrected factual statement about the reference server, not a protocol change.
0.8.0-draft.2
2026-08-15
phux-nwpw: §5 length-prefix framing gains a single owning implementation in phux-protocol (wire::framing), replacing seven per-transport copies of the same rule. Conformance fix, no wire-format change: the server’s WebSocket reader previously bounds-checked only the binary message’s total size, so a message whose declared length disagreed with its own size was accepted and the surplus bytes silently discarded by the dispatch loop’s ignored decode tail. §5 defines no second framing layer, so one binary message is exactly one frame; such a message is now rejected as malformed and the connection closed, matching the UDS, QUIC, and WebTransport readers. A conforming peer is unaffected: no frame it can emit changes shape, no tag or field changes, and PROTOCOL_VERSION remains 0.8.0.
0.8.0-draft.1
2026-08-14
phux-w7z2.49: new conventional Terminal-scoped phux.pane-occupant/v1 record, { "foreground": str, "is_pane_shell": bool }, publishes the privacy-bounded foreground-process fact needed by an available-shell safety gate. The server reuses the detector’s existing tcgetpgrp and argv[0] query, strips a login dash and path, compares the foreground pgid with the pane’s original child pid, recognizes sh/bash/zsh/fish, and edge-filters writes. No pid, argv tail, cwd, environment, or command text crosses L3. The key is server-owned: clients may GET/SUBSCRIBE but SET/DELETE are ignored, preventing a spoofed true from authorizing input. phux agent start waits through the detector’s first tick, refuses false, accepts true when OSC-133 is absent, and retains contradictory OSC-133 busy evidence as the stronger conservative refusal. No wire-format change: the existing metadata verbs carry the new conventional key, no tag or field changes, and PROTOCOL_VERSION remains 0.8.0.
0.8.0
2026-08-14
ADR-0085: REPORT_AGENT_STATE command tag 0x17 carries a Terminal plus working, blocked, or done hook evidence into the server-side detector. New ServerFeature bit REPORT_AGENT_STATE = 0x00000400 gates the additive command. Hook evidence publishes immediately but does not write metadata or stand screen/process derivation down. Protocol minor bumps because the command creates a new negotiated wire surface.
0.7.0-draft.11
2026-08-13
phux-w7z2.57: SUBSCRIBE_METADATA naming a satellite-owned Terminal is now refused, not silently installed. New L3.md §1.3 states the rule L3 always followed but never wrote down: metadata does not federate. The hub relay carries L1 commands and SUBSCRIBE_EVENTS across a satellite link and no L3 leg in either direction, so a hub holds no metadata for a satellite pane and no code path can ever emit a METADATA_CHANGED about one. Recording the subscription anyway was the worst of the three available answers — SUBSCRIBE_METADATA has no reply frame, so acceptance and a silent drop are indistinguishable, and the consumer blocks forever on a notification that cannot arrive. That is how phux agent wait host/@N came to read the hub’s empty store and report no_agent_record — “this pane declares no agent” — about a pane with a live agent on another machine. The server now pushes ERROR { request_id: None, code: UNSUPPORTED_SATELLITE_ROUTE } naming the key and the satellite, and installs nothing. No new wire values and no new gating. UNSUPPORTED_SATELLITE_ROUTE = 106 already means “this frame carried a TerminalId::Satellite and there is no route for it” — ids.rs makes the non-hub case a MUST — and “this verb has no satellite route on any server” is that same fact on a different axis. Reuse is what keeps this decode-safe: draft.10 spent a long paragraph on why a new ErrorCode value is a hard decode error (from_wire → None → DecodeError::UnknownEnumValue) that none of §6.3’s three capability forms can gate, and the cheapest way to not have that problem is to not mint a code. The uncorrelated push is the shape SUBSCRIBE_EVENTS already uses for a missing route (phux-v45.4), for the same reason: a command with no reply frame still owes a refusal somewhere. GET_METADATA / LIST_METADATA on a satellite scope are deliberately unchanged — they report the receiving server’s store, and on that server the key genuinely is unset, so value: None is true rather than merely uninformative; converting them to correlated errors would strand pending-request entries in a consumer that only retires them on METADATA_VALUE and would buy nothing the subscribe-side refusal does not already deliver. Routing L3 across a hub stays open as a future version: a refusal becoming a METADATA_CHANGED breaks no consumer, so this rule is not a barrier to it. Behavioral, additive: no tag renumbered, no existing bytes changed, no new enum value; draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.10
2026-08-12
phux-w7z2.60: new ErrorCode INPUT_NOT_WRITTEN = 207, shipped unbitted after explicit weighing of §6.3’s capability rule. APPLY_INPUT previously collapsed every discard of a pending write into INPUT_DELIVERY_UNKNOWN — the code whose remedy is “do not resend, the batch may complete a moment later” — even for cases the server can prove never reached a writer at all: no PTY on the target pane, a full or already-closed writer-thread queue, the pane’s own actor gone before handoff, or the input lane itself gone. L1.md §6.2.1 now names INPUT_NOT_WRITTEN for exactly that provable subset and narrows INPUT_DELIVERY_UNKNOWN to results reached only after a writer thread actually took the job. The two share the ten-minute dedupe-cache treatment of OK/CANONICAL_LIMIT_EXCEEDED when reached after registration and the pre-handoff non-caching of RESOURCE_EXHAUSTED when reached before it; what matters to a caller is not caching but that INPUT_NOT_WRITTEN is safe to resubmit under any operation id (nothing already written exists to duplicate) while INPUT_DELIVERY_UNKNOWN stays terminal. On gating: §6.3 names three capability forms — a ServerFeature bit, a ClientCapabilities byte, or an additive field id a decoder skips by length — and none of the three actually protects a peer that predates this code. It is not an additive field: draft.7’s own text distinguishes ErrorCode/TombstoneReason, whose unrecognised values are hard decode errors, from additive field ids, which a decoder survives discarding — the two are not interchangeable, and draft.7’s “no bit needed” reasoning does not transfer here for exactly that reason. A ServerFeature bit was considered and rejected on the merits, not skipped: every existing bit (ACKNOWLEDGED_INPUT, SHUTDOWN, MOVE_TERMINAL, FILE_UPLOAD, TERMINAL_REPLY) gives the client a check to make before it takes an action the server might not support, so an old peer avoids triggering the new behavior at all. APPLY_INPUT is already gated on ACKNOWLEDGED_INPUT, negotiated several drafts ago; the client keeps sending it unconditionally, and it is the server that unilaterally picks which reply code fits its own internal write-path outcome, with no client action to withhold that would steer it back to 205. A ServerFeature bit is one fixed value a server announces to every connecting peer alike — it gives the server no per-connection signal of what a given client can decode, so minting one here would not change the server’s behavior at all; the only mechanism that could is a ClientCapabilities-shaped declaration the connecting client makes and the server consults before choosing 207 over 205, and ClientCapabilities today carries only rendering/codec concerns (color, layers, image/keyboard protocols, hyperlinks, output mode, bootstrap profile) with no protocol-conformance field, nor does anything in the completion-waiter/input-lane pipeline that produces this result thread per-connection capability data today — a real, separate change, not a one-line mint, and out of scope for this fix. Given that, the choice was between shipping unbitted and building that separate mechanism now. ADR-0071 point 3 already declares the compatibility unit to be the release — one deployment, server and every client, on one release — so draft-level skew inside a single 0.7.0 minor is already outside the guarantee HELLO’s major.minor-only gate can see. The failure mode for a peer that predates this code is loud, not silent: ErrorCode::from_wire returns None, the ERROR frame fails to decode, and that surfaces to the client as a transport failure whose existing published remedy (“the operation was not acknowledged, so delivery is unknown; read the pane before resending”) is already the conservative answer a bitted fallback would have manufactured deliberately — the client loses the “resubmit is safe” optimization for that one round trip, never correctness. Ships the same way CANONICAL_LIMIT_EXCEEDED = 206 did one draft earlier, in the same reply-code family, for the same command. Additive: no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.9
2026-08-12
phux-a5xj: SPAWN_TERMINAL (L1.md §3.1) gains additive optional field id 10, initial_size: optional<(u16, u16)> — the grid the new Terminal’s libghostty screen and PTY winsize are created at — gated on new ServerFeature bit SPAWN_INITIAL_SIZE = 0x00000200 (proto.md §6.2). Every pane creation was paying for a capture nobody read: the server built the pane at its 80x24 default, ran a full ADR-0070 checkpoint at that grid, published BOOTSTRAP_BEGIN/CHUNK/READY, and then received the attaching consumer’s real tile as TERMINAL_RESIZE — which, being an authoritative resize, tombstoned the generation just published and forced a second capture. The geometry was knowable one round trip earlier the whole time: a layout-owning consumer computes the tile a new leaf will occupy from its own layout before the server has allocated an id for that leaf, so it can name the grid in the spawn rather than correct it afterwards. Deferring the bootstrap until the first resize settled was the alternative and was rejected: it buys the same saving with a timer, leaves first paint waiting on a round trip that may never come for a pane no one attaches to, and still loses the race to any SIGWINCH. A zero on either axis MUST be read as “the sender does not know its geometry” rather than as a zero-cell grid, leaving the server’s default in force per §9.2’s zero-viewport no-op rule; the field is local-only and a hub MUST NOT forward it on a satellite-routed spawn. The reference server additionally narrows its resize resync to resizes that actually reflowed, since a resync is what rotates the generation and a consumer confirming the size it already asked for must not cost the pane its checkpoint. Additive per §6.3: one optional field id, one feature bit, no tag renumbered and no existing bytes changed — a server predating the field skips it by length and spawns at its default, which is the pre-field behavior, so an unadvertised send degrades rather than breaks; consumers SHOULD still gate on the bit, because it is what tells them whether their follow-up resize is redundant. Draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.8
2026-08-12
phux-w7z2.56: TERMINAL_CLOSED addressing is spelled out, no wire-format change — no frame, tag, field, or error code is allocated or altered, and no existing bytes change. L1.md §3.1 already required the server to emit TERMINAL_CLOSED “to every client subscribed to the Terminal”, and §5.1 already said ATTACH_TERMINAL “registers the caller as an output subscriber” for which “a session-scoped ATTACH is not required”. The reference server did not honour the intersection: its pane-EOF fanout resolved subscriber mailboxes through the session-attach table alone, so a consumer that reached one Terminal through ATTACH_TERMINAL sat on that Terminal’s subscriber list and was filtered straight back out of the fanout. When the Terminal died it received nothing — not an error, not a close, just the end of output, which is indistinguishable from a Terminal that has gone quiet. That is the shape an agent orchestrating panes takes (it watches one Terminal; it does not attach to a session) and the shape a federation hub’s proxy subscription takes, so a satellite Terminal’s death never reached the hub and left dead proxy state behind. §3.1 now states the union directly: both subscription paths count, an ATTACH_TERMINAL-only consumer MUST receive the frame, and a client subscribed by both paths MUST receive it exactly once rather than once per path. Conformance fix to already-normative §3.1 behavior plus the sentence that makes the requirement unmissable; draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.7
2026-08-12
phux-l83x: DETACHED (0x82) gains its two additive optional fields — reason: optional<DetachReason> (field id 1) and message: optional<str> (field id 2). proto.md §7.2 has specified DETACHED { reason, message } since the first draft and §9 makes it the normative fatal signal (“a fatal error MUST be followed by DETACHED { reason: PROTOCOL_ERROR } and transport close”), but FrameKind::Detached was a unit variant carrying neither field: the clause named a value the wire could not express. That gap became load-bearing at 0.7.0-draft.5 (phux-ijuj), which made ERROR non-fatal at the receiver — a connection now ends by DETACHED plus transport close and by nothing else, so DETACHED is the only ending a consumer may act on, and it was the one frame that could not say why. A consumer could not tell “you asked to detach” from “the server died under you”. Both fields are optional-absent, so the reason-less encoding is byte-identical to the empty body every 0.7.0 peer already emits, and the DETACHED decoder has drained-and-ignored unknown field ids since 0.4.0 — the addition is invisible to an older peer in both directions. Absence is defined rather than defaulted: a consumer MUST treat an absent reason as unstated and MUST NOT infer REQUESTED, and it MUST tolerate an unrecognised DetachReason value by treating it as absent, never by failing the frame — rejecting the termination signal would convert an explained ending into an unexplained transport error and would make every future DetachReason allocation a fleet-wide break. That is a deliberate divergence from ErrorCode and TombstoneReason, whose unknown values are decode errors; those ride frames a consumer can survive discarding. message is diagnostic text: a consumer MUST NOT parse it or condition behavior on it. No ServerFeature bit, unlike draft.6’s SHUTDOWN: that bit exists because a client must not send an unadvertised command tag an older server would silently drop, whereas DETACHED is server→client and an older consumer skips the new field ids by length — §6.3 and ADR-0061 name “an additive field id decoders skip by length” as a capability form in its own right, and the same shape shipped unbitted at 0.6.0-draft.4 (SPAWN_TERMINAL field 9). Minting a permanent bit here would buy nothing: a consumer behaves identically whether the reason is absent because the server is old or because it declined to state one. The reference server now states REQUESTED for a client’s DETACH and for a DETACH_CLIENTS sweep, and SESSION_KILLED when the group an attach was rooted in is reaped; §7.2 carries a partial impl-status marker because shutdown and fatal-error closes still drop the transport without a DETACHED at all, and role takeover (REPLACED) is unimplemented. Additive per §6.3: two field ids on an existing frame, no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.6
2026-08-11
phux-pimp: new command SHUTDOWN = 0x16, gated on new ServerFeature bit SHUTDOWN = 0x00000100. phux had four ways to start a server — auto-spawn, phux server, service supervision, and the ADR-0032 re-exec — and no way to stop one, while ADR-0080’s Decision, docs/operations.md, and a live test assertion all cited phux kill --server as though it existed. A signal could not have supplied it: the server had no SIGTERM handler (fixed separately), and even with one, death by signal is a non-zero exit, which launchd’s KeepAlive{SuccessfulExit: false} restarts after ThrottleInterval — so a signal-based stop would have contradicted the very property ADR-0080 claims, on the platform phux mostly runs on. A command can exit 0, which is what makes “a deliberately stopped server stays stopped” true. L1.md §5.1 requires the server to answer COMMAND_RESULT { Ok } before tearing down, to reap every Terminal through the ordinary close path so consumers observe the lifecycle they already handle, and — because the connection closes as part of that teardown — requires a consumer to read a following disconnect as success rather than as a failure to stop. A server MAY restrict it by transport and the reference server does: local Unix socket only, ERROR { PERMISSION_DENIED } elsewhere, because ending every session on a host is not an authority the pairing credential was designed to convey and no revision has defined one that is. Additive per §6.3: one command tag, one feature bit, no tag renumbered and no existing bytes changed; 0x0a/0x0b stay freed-and-reserved. A client MUST NOT send it unadvertised, since an older server drops the unknown tag silently and “nothing happened” is indistinguishable from “the server declined”. Draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.5
2026-08-10
phux-ijuj: ERROR receiver semantics are now normative, no wire-format change. proto.md §9 carried a sender obligation only — a fatal error MUST be followed by DETACHED { reason: PROTOCOL_ERROR } and transport close — and said nothing about what a receiver does on receipt. The reference TUI read that silence as license to treat any uncorrelated ERROR outside a small allowlist as a protocol violation and tear down the whole attach: every pane, the layout, the session, on a failure that may have concerned one pane. That contradicted input.md §6, which requires a server that did not advertise TERMINAL_REPLY to emit an uncorrelated ERROR and continue, and L1.md §9.1, where an unreachable satellite contributes an empty set and MUST NOT fail the aggregate. §9 now states the receiver half directly: a consumer MUST NOT treat receipt of an ERROR as terminating its attach, a connection ends by DETACHED plus transport close and by nothing else, and until one arrives the consumer SHOULD surface the message and continue. A fatality table keyed on the code would not have worked — the same server emits MALFORMED_MESSAGE both fatally (an undecodable frame) and non-fatally (a bad value inside a COMMAND) — so §9 instead publishes each code’s scope: Terminal, Request, or Connection, with Connection meaning the consumer expects the server to close rather than closing itself, and an unrecognised code read as Terminal. ErrorCode::scope() -> ErrorScope in phux-protocol is the Rust surface for the same table, matched without a wildcard so a new code cannot silently inherit a scope.
0.7.0-draft.4
2026-08-09
Acknowledged-input admission is per Terminal, not per server, no wire-format change. L1.md §6.2.1 said the reference server “admits at most one unresolved acknowledged operation at a time”, and it did: one flag for the whole process, held across a completion wait that ran on the single thread every keystroke for every pane also flows through. Two consequences fell out of that, both invisible in the frame layout and both fatal to the verb APPLY_INPUT exists for. Concurrent operations against unrelated panes collided into RESOURCE_EXHAUSTED, so a client driving N panes could not submit to more than one at a time. Worse, a pane whose child had stopped reading its stdin held the flag and the input thread for the full five-second wait, during which no attached INPUT_* and no ROUTE_INPUT moved for any pane, and attached input was silently dropped once the shared lane queue filled — an APPLY_INPUT to pane X causing keystroke loss in pane Y. Nothing below the lane was ever shared (mailbox, writer channel, and writer thread are per pane), so the serialization bought no safety it could not keep otherwise. The paragraph now scopes admission to the target Terminal, states that operations against distinct Terminals proceed concurrently and that one Terminal’s unresolved write MUST NOT delay input to another, and names the five-second completion bound so a client’s submit deadline and RESOURCE_EXHAUSTED backoff can be sized against a published number instead of a measured one. Per-Terminal exclusion is unchanged and load-bearing: it is what keeps a same-id retry from writing a second time while the first write is still unresolved. Dedupe keys, the ten-minute horizon, the digest conflict rule, and every result code are untouched (ADR-0053, phux-w7z2.58).
0.7.0-draft.3
2026-08-08
A deriving server MAY withdraw an explicit state declaration whose subject is provably gone, no wire-format change. L3.md §3.7 “Server as a producer” made an explicit SET_METADATA supplying state outrank any derivation for the lifetime of the record, with DELETE_METADATA the only exit — while the same section forbids a server deleting a record it did not author. A declaring writer that dies without clearing (a SIGKILL, a force-closed pane, a wrapper whose exit trap never ran) therefore left the pane pinned to its last declared value with no path back to truth: the exact failure ADR-0046’s level-triggering exists to prevent, reached through the one door level-triggering does not cover. The first bullet is narrowed from “MUST NOT overwrite” to “MUST NOT overwrite with a derived value”, and a new MAY permits withdrawal: setting state to "unknown" — never substituting a derived value, never DELETE_METADATA — when the server has positive evidence that the declared occupant of the pane is gone (for example, the PTY’s foreground process group no longer resolves to any agent, or resolves to a different one). “Positive evidence” is defined against the failure that would make the permission unsafe: it is an observation the server successfully made and which found no such agent, never an observation the server was unable to make, and a server that cannot determine occupancy MUST hold the declaration — a proc-query that fails is not evidence of a vacant pane. A withdrawal MUST preserve name, kind, and session, so the declaring writer’s label survives and only the claim about the process is dropped; it SHOULD clear attention, whose basis was the state being withdrawn, so a withdrawn record does not keep a high badge; and it MUST be idempotent at the byte level, so a repeated withdrawal broadcasts nothing and ADR-0046 decision point 7’s zero-write idle fleet survives. Once withdrawn, the declaration no longer outranks the derivation and the next derived write proceeds normally; the withdrawn shape (kind present, state: "unknown") is the one ADR-0075 point 5 already reads as a write guard. ADR-0046 point 8 gains the matching clause, and its point 10 (“a dead process does not keep a live badge”) is corrected, having asserted for every pane an invariant that held only for detector-owned records. Recorded in the same pass: the opt-in Claude hook shim now declares name and kind only, because a hook that declares a state stands the derivation down on its own pane, so phux ships no done producer at all — already covered by §3.7’s existing requirement that a consumer MUST NOT treat done as reachable on an arbitrary pane, MUST NOT wait for it exclusively, and MUST NOT read its absence as evidence an agent did not finish. Strictly permissive: a new MAY plus a narrowing of an existing MUST NOT. No frame, field, tag, event kind, error code, or encoding is allocated or altered, and a server that never withdraws remains conforming; draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.2
2026-08-08
Semantic clarification of an existing normative key, no wire-format change. L3.md §3.7 now states how a consumer is entitled to read phux.agent/v1’s state (“Reading state: levels and edges”), because three consumer-facing gates were each assuming a different, stronger meaning for the same word. A level read asserts only that no contrary state is currently being asserted: idle is the value a producer falls back to with no positive evidence of anything else, so a consumer MUST tolerate an idle that is equally true of a finished agent, a crashed agent, a pager, an editor, a splash screen, and an unidentified pane — the same “no information” reading the spec already gives an absent or unrecognized state (both unknown), differing in provenance rather than in strength. An edge — a transition the consumer itself observed — asserts strictly more: that whatever was asserting the old value stopped. Consequently a completion gate (a blocking wait, a prompt-and-wait) MUST require an observed transition into one of the states it accepts and MUST NOT be satisfied by a level read, so a wait cannot report success on a pane whose agent crashed or was never running; a safety gate (declining to scroll a repainting screen, declining to write into a pane whose occupant may have changed) MAY read the level, since absence of contrary evidence is the right predicate for “do not disturb” and errs conservatively. done is called out separately: no title- or screen-derived rule can honestly produce it, so a consumer MUST NOT treat it as reachable on an arbitrary pane nor read its absence as evidence an agent did not finish. Grounded in the reference implementation — agent_detect returns idle on no match (ADR-0046 decision point 5), the five shipped manifests (crates/phux-server/rules/*.toml) declare five working and five blocked rules and zero idle/done rules, and the only done writer is the opt-in Claude hook shim — but phrased as what a consumer MUST tolerate, not what every server does: a server that authors positive idle rules, or a hook that writes done, stays conforming. No frame, field, tag, event kind, error code, or encoding is allocated or altered, and no previously-conforming implementation becomes non-conforming; draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0-draft.1
2026-08-07
phux-mjmc: new ErrorCode CANONICAL_LIMIT_EXCEEDED = 206. APPLY_INPUT now refuses a batch, before writing any bytes, when the target pane’s line discipline is in canonical (ICANON) mode and the batch’s combined encoded PTY bytes contain a line — a run between two terminators, or from the last terminator to the end — longer than the pane’s real canonical-line byte limit (queried live via fpathconf(_PC_MAX_CANON) on the pane’s PTY master, not a hardcoded platform constant). Previously such a write succeeded from the caller’s point of view while the kernel’s canonical queue silently dropped the overflow, and — if the payload’s own line terminator fell past the truncation point — dropped the terminator too, permanently wedging the pane’s input (empirically 1024 bytes retained of 4097 written on darwin’s MAX_CANON, and 0/6 delivered when a 1800-byte overflow’s terminating CR followed). ERROR(CANONICAL_LIMIT_EXCEEDED) names the limit and is cached as the operation’s final result exactly like OK/INPUT_DELIVERY_UNKNOWN (L1.md §6.2.1). Fire-and-forget INPUT_PASTE/ROUTE_INPUT hit the identical write-time guard but have no reply channel to carry the refusal, so they log and drop per the existing fire-and-forget contract (SPEC §9) instead of erroring. A pane in raw mode (ICANON clear) is unaffected: the guard is a no-op and the write proceeds unchecked, exactly as before. Additive: no tag renumbered, no existing bytes changed, the new code decodes as unknown on an older peer; draft bump (PROTOCOL_VERSION stays 0.7.0).
0.7.0
2026-08-02
ADR-0070: clean cutover to explicit native/synthesized bootstrap profiles, exact libghostty checkpoint-v2 capability negotiation, nonzero StreamId/BootstrapId generations, READY-fenced active-state publication, bounded client-pull history, and tombstones. Adds HISTORY_REQUEST=0x16, capability-gated INPUT_TERMINAL_REPLY=0x17, ATTACH_READY=0x83, BOOTSTRAP_BEGIN/CHUNK/READY/HISTORY_PAGE/BOOTSTRAP_TOMBSTONE=0x93..0x97, cursor-scoped HISTORY_TOMBSTONE=0x98, and retryable HISTORY_REJECTED=0x99; permanently retires TERMINAL_SNAPSHOT=0x91; extends generation identity onto live output and StateSync ACK; bumps protocol to 0.7.0 with no 0.6 compatibility alias. Native history requests carry byte and row budgets; pages carry a nonzero cursor-lineage sequence and authenticated row count. History rejection/tombstoning never invalidates the live generation. INPUT_TERMINAL_REPLY is a bounded opaque path advertised by HELLO_OK.server_caps.features.TERMINAL_REPLY = 0x80; absence in an older 0.7 HELLO_OK forbids emission.
0.6.0-draft.5
2026-08-02
phux-p39 (ADR-0056): cross-window Terminal move. Two new L1 frames — MOVE_TERMINAL (C→S 0x2A: { request_id: u32, terminal: TerminalId, owner_terminal: TerminalId }) and TERMINAL_MOVED (S→C 0xA8: { request_id: u32, result: MoveResult }) — mirror the SPAWN_TERMINAL / TERMINAL_SPAWNED pair (L1.md §3.1). The server re-parents the live terminal into the window that currently owns owner_terminal, possibly in a different session, atomically under its state lock, reaping a source window the move emptied by the same rules as pane death; the pane’s process, PTY, scrollback, metadata, agent record, and TerminalId are untouched, so subscriptions and outstanding waits survive. Ownership addressing only, exactly as ADR-0050’s spawn field: no split direction, ratio, or focus — a layout-aware caller publishes geometry through two L3 envelope writes and issues the inverse MOVE_TERMINAL when the destination write fails. MoveResult reuses the Ok = 0x00 / Err = 0x01 convention, carrying the moved id or a MoveError (MOVE_FAILED (str) = 0x00, UNSUPPORTED_SATELLITE_ROUTE = 0x01 — the move is local-only; both unions #[non_exhaustive]). Per the §6.3 extension rule the pair ships behind a new negotiated ServerFeature bit, MOVE_TERMINAL = 0x00000040 (§6.2): a client MUST NOT send the frame unless the bit is advertised (an older server drops the unknown discriminant silently). Discriminants allocated past the process-execution (0x24..=0x27 / 0xA3..=0xA5) and port-forward (0x28..=0x29 / 0xA6..=0xA7) spec reservations. Backs cross-session phux move-pane. Additive: one C→S frame, one S→C frame, one feature bit; no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.6.0).
0.6.0-draft.4
2026-08-02
ADR-0068: native agent-session restore provenance. SPAWN_TERMINAL (L1.md §3.1) gains an additive optional field id 9, agent_session: optional<bytes> — 1–4096 encoded bytes of the new conventional Terminal-scoped phux.agent-session/v1 record (L3.md §3.7.1: UTF-8 JSON {plugin_id, integration_id, native_id}, all REQUIRED and control-free, named in phux-protocol as TERMINAL_AGENT_SESSION_KEY with MAX_AGENT_SESSION_RECORD_BYTES = 4096), installed in the same server state transaction that creates and interns the Terminal. Empty or oversized values return SPAWN_FAILED; the field is local-only, MUST NOT be combined with satellite, and older peers that predate field 9 ignore it — a client MAY fall back to ordinary L3 SET/GET confirmation. The phux.session.create/v1 nonce-create payload (L3.md §3.8) gains the matching optional agent_session member with identical atomic-install semantics, and nonce-bearing phux.session.created/v1/<request_token> result keys become server-owned: reserved from ordinary SET/DELETE/SUBSCRIBE, owner-read-only, bounded per connection, and excluded from LIST_METADATA. The record deliberately carries no executable path or argv — a restoring consumer rebuilds resume argv from the currently enabled, uniquely owned integration policy and MUST fail closed on missing, stale, ambiguous, or ownership-mismatched policy. Additive: one new optional field id and one conventional L3 key; no tag renumbered, no existing bytes changed; draft bump (PROTOCOL_VERSION stays 0.6.0). Landed in PR #329; this entry and the ADR settled at 0068 after two successive number collisions (first with ADR-0066, then with the decision now at ADR-0070) were resolved post-merge.
0.6.0-draft.3
2026-07-27
ADR-0061: write down the extension rule that draft.2’s admission test implies. New proto.md §6.3 makes it normative that a minor bump is a fleet-wide break with no grace window, that new frames / command tags / error codes / fields MUST ship as a negotiated capability (ServerFeature bit, ClientCapabilities byte, or additive field id) whenever an additive shape exists, and that minor is reserved for changes no additive shape can express — renumbering a tag, redefining bytes already on the wire, reallocating a freed tag, or removing a depended-on message. Also promotes the message-catalog Status column from informative-with-a-hand-written-date to CI-checked: just docs-check’s new impl-status gate resolves every shipped / partial / spec-only cell in proto.md, L1.md, L3.md, and appendix-reserved.md against the wire constants in phux-protocol, and fails in both directions. Documentation only: no frame, field, tag, or error code is allocated or altered, no existing bytes change; draft bump (PROTOCOL_VERSION stays 0.6.0).
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.
Appendix B — Reserved ranges
The reserved-discriminant ranges for future protocol extensions: which message-ID slots are earmarked for which categories (lifecycle, hot path, control...
phux-workload/v1 — workload authentication and scoped authority
The reusable workload-authentication profile for Phux endpoints.