phux
protocol

L3 — Metadata storage

The OPTIONAL typed key-value service the server hosts but does not interpret.

wire 0.8.0stable document
Full source summary

The OPTIONAL typed key-value service the server hosts but does not interpret. Scopes are Terminal, Group, and Global; values are opaque bytes with a conventional CBOR-and-versioned-key shape. This document owns the metadata model and the grouping/session-name conventions consumers use to build sessions, groups, and layouts on top of L1, since there is no L2 collection tier.

1. L3 message catalog

A typed key-value store the server hosts and does not interpret. Scopes:

  • Terminal { terminal_id, key, value }
  • Group { group_id, key, value }GroupId is an opaque grouping key, not a lifecycle tier (see L2.md)
  • Global { key, value }

Status: spec-only. L3 does not yet enforce the workload grant described below.

Values are opaque bytes. The metadata service does not interpret them and enforces only size limits plus the connection’s phux-workload/v1 grant: read/list/subscribe and mutation map to the closed matrix in workload-auth.md §7. The conventional value shape is CBOR-encoded structured data under a versioned key (phux.session.name/v1, phux.tui.layout/v1); see §3 for the conventions consumers share.

L3 messages, allocated by phux-4li.2 (commands + push) and phux-4li.8 (GET/LIST replies):

IDDirectionNameReferenceStatus
0x50C → S (cmd)GET_METADATA§2shipped
0x51C → S (cmd)SET_METADATA§2shipped
0x52C → S (cmd)DELETE_METADATA§2shipped
0x53C → S (cmd)LIST_METADATA§2shipped
0x54C → SSUBSCRIBE_METADATA§2shipped
0xD0S → CMETADATA_CHANGED§1shipped
0xD1S → CMETADATA_VALUE§1shipped
0xD2S → CMETADATA_KEYS§1shipped

Wire bodies (field-tagged TLV, per appendix-encoding.md; fields listed in field-id order, leaf primitives and nested unions positional within each field):

GET_METADATA       { request_id: u32, scope: Scope, key: str }
SET_METADATA       { request_id: u32, scope: Scope, key: str, value: bytes }
DELETE_METADATA    { request_id: u32, scope: Scope, key: str }
LIST_METADATA      { request_id: u32, scope: Scope }
SUBSCRIBE_METADATA { scope: Scope, key: str }
METADATA_CHANGED   { scope: Scope, key: str, value: optional<bytes> }
METADATA_VALUE     { request_id: u32, value: optional<bytes> }
METADATA_KEYS      { request_id: u32, keys: list<str> }

Scope = tagged_union {
    TERMINAL   (TerminalId),     // tag 0x00
    GROUP      (GroupId),        // tag 0x01; u32 wire body
    GLOBAL,                      // tag 0x02; empty body
}

1.1 The inline-value rationale

Three S→C frames carry their value inline rather than make the consumer issue a follow-up GET_METADATA:

  • METADATA_CHANGED { scope, key, value }value: Some(bytes) on a SET, value: None (a tombstone) on a DELETE.
  • METADATA_VALUE { request_id, value } — correlates to a prior GET_METADATA.request_id, with value: None when the key is absent.
  • METADATA_KEYS { request_id, keys } — correlates to a prior LIST_METADATA.request_id; keys are lexicographically sorted and values are NOT included (clients fetch them with GET_METADATA, since a LIST result is large and most keys are not read).

The shared reason: the layout-coordination use case (ADR-0019) is a read-on-every-change pattern, so a separate notify-then-fetch round trip is waste the consumer always pays. An earlier draft routed GET/LIST replies through a generic COMMAND_RESULT envelope and had consumers GET after a change notification; both are dropped for these three frames. COMMAND_RESULT remains the envelope for L1 commands that need its tagged union (e.g. SPAWN returning a TerminalId); it does not subsume the L3 reply frames.

1.2 Subscriptions, conformance, and scope

A client subscribing via SUBSCRIBE_METADATA { scope, key } MUST receive METADATA_CHANGED when that specific (scope, key) is written or deleted.

The reply frames and METADATA_CHANGED MUST NOT be emitted to a consumer whose HELLO.client_caps.layers does not include L3 (proto.md §11.5). A server that receives an L3 request from a non-L3 consumer MAY drop it silently (matching the SUBSCRIBE_METADATA precedent) or reply with ERROR { MALFORMED_MESSAGE }. The withdrawn OUT_OF_TIER code is permanently retired and MUST NOT be emitted.

Subscriptions are connection-scoped: a client’s subscriptions are dropped automatically on DETACH (proto.md §7.2) and on transport close. Protocol 0.7 has no explicit UNSUBSCRIBE_METADATA.

A server MAY bound the number of subscriptions one connection holds at once and refuse a SUBSCRIBE_METADATA past that bound; a refusal has no wire signal of its own for the same reason a non-L3 refusal does not (this command has no reply frame), so a client MUST NOT assume a SUBSCRIBE_METADATA it sent was actually installed. A server SHOULD also drop a subscription naming a Terminal scope once that Terminal closes, even while the subscribing connection stays open, so a long-lived watcher does not accumulate subscriptions to panes that no longer exist.

1.3 L3 does not federate

L3 metadata is server-local. A federation hub (ADR-0007) relays L1 commands and SUBSCRIBE_EVENTS across a satellite link; it carries no L3 leg in either direction. A hub therefore holds no metadata for a satellite-owned Terminal and can never emit a METADATA_CHANGED about one.

A server that receives a SUBSCRIBE_METADATA whose scope is Terminal(TerminalId::Satellite { .. }) MUST NOT install the subscription, and SHOULD push ERROR { request_id: None, code: UNSUPPORTED_SATELLITE_ROUTE } naming the key and the satellite. Installing it silently is the one outcome a consumer cannot recover from: with no reply frame to distinguish acceptance from a drop, the caller blocks on a notification no code path can produce. The uncorrelated push is the same signal L1.md already defines for a SUBSCRIBE_EVENTS with no route, for the same reason — a command with no reply frame still owes a refusal somewhere.

UNSUPPORTED_SATELLITE_ROUTE is reused rather than extended: it already means “this frame carried a TerminalId::Satellite and there is no route for it”, and “this verb has no satellite route on any server” is that same fact on a different axis. A new ErrorCode value would be a hard decode failure on any peer that predates it, and 106 has shipped since 0.7.0.

GET_METADATA / LIST_METADATA on a satellite scope keep answering normally (value: None, empty keys): they report the receiving server’s store, and on that server the key genuinely is unset. A consumer that needs a satellite pane’s metadata connects to that satellite’s own server.

A future protocol version MAY route L3 across a hub. That is a strict upgrade of this rule — a refusal becoming a METADATA_CHANGED breaks no consumer — so nothing here is a barrier to it.


2. L3 commands

Wire discriminants are allocated above in §1.

Command_L3 = tagged_union {
    GET_METADATA     { scope: MetadataScope, key: str },
    SET_METADATA     { scope: MetadataScope, key: str, value: bytes },
    DELETE_METADATA  { scope: MetadataScope, key: str },
    LIST_METADATA    { scope: MetadataScope, prefix: optional<str> },
}

MetadataScope = tagged_union {
    TERMINAL   (TerminalId),
    GROUP      (GroupId),        // opaque grouping key, not a tier
    GLOBAL,
}

The server MUST NOT interpret metadata values. Implementations MAY enforce a per-key size limit (recommended: 256 KiB) and return RESOURCE_EXHAUSTED if exceeded.


3. Grouping and session conventions (non-normative)

This section is non-normative. It documents the conventional keys consumers use to build sessions, groups, windows, panes, and layouts on top of L1 metadata. There is no L2 collection tier (ADR-0030, L2.md), so these conventions plus client logic are where session and group vocabulary lives. The wire enforces no agreement; a consumer MAY ignore this section entirely.

Per ADR-0017, the reference TUI is one consumer among several. Its vocabulary — session, window, pane, layout tree, focus — is product shape, not a wire concept. These keys exist so an alternative consumer can shadow the reference TUI by reading and writing the same metadata.

Keys are versioned (/v1) so future schemas co-exist with old clients. Values are CBOR-encoded structured data unless noted. A consumer reads on attach, watches for METADATA_CHANGED, and writes on user action.

3.1 Session conventions

The session/collection lifecycle verbs that earlier code carried as L1 commands are withdrawn (ADR-0030); their behavior decomposes into SPAWN_TERMINAL plus the following metadata keys, with atomic group teardown served by the L1 KILL_TERMINALS op (L1.md). CLI, MCP, and TUI user-facing UX is unchanged.

  • phux.session.name/v1 — the human-facing group name. A SET on this key is a rename; there is no RENAME_SESSION wire verb. Scope: Global. Value: current\0new (NUL-separated UTF-8) — the v0.3.0 re-tier convention this spec’s CHANGELOG records; the server intercepts the write and applies the authoritative registry rename. When the name actually changed, the server broadcasts a METADATA_CHANGED carrying the applied current\0new value to subscribers of the written (scope, key); a refused or no-op rename broadcasts nothing, and the value is not retained in the store (a GET on this key returns absent).
  • phux.session.create/v1 — a create-without-attach request interpreted atomically by the reference server. Scope: Global. Value: a UTF-8 JSON object {name: str, command?: list<str>, cwd?: str, env?: map<str,str>, request_token?: UUID, agent_session?: list<u8>}. command preserves argv boundaries, cwd selects the seed pane’s working directory, and env is added to the seed process environment. A present agent_session contains 1–4096 encoded bytes of the §3.7.1 record and is installed in the same state transaction that creates and interns the seed Terminal. The server creates the named session and seed Terminal without attaching or resizing. This replaces the old CREATE_SESSION verb. Under phux-workload/v1, this server-interpreted SET_METADATA requires both CREATE and BIND on Global before the value is parsed; a BIND-only grant cannot reach process creation.
  • phux.session.created/v1/<request_token> — the one-shot result published after a successful nonce-bearing create request. Scope: Global. Value: UTF-8 JSON {name: str, terminal_id: u32, request_token: UUID}. Because SET_METADATA has no reply body, the creating connection reads its exact nonce-specific key and verifies both name and request_token; other connections receive an absent value even if they know the nonce. This prefix is server-owned: under phux-workload/v1, ordinary SET_METADATA, DELETE_METADATA, and SUBSCRIBE_METADATA targeting the exact key or its slash-prefixed results are default-denied before the handler. The pre-profile reference server ignores them. The server consumes the value after a successful owner read, removes abandoned values when their owner disconnects, bounds unread results per connection, and excludes nonce-bearing result keys from LIST_METADATA. The un-suffixed phux.session.created/v1 {name, terminal_id} convention remains for legacy clients.

Group membership remains a consumer projection over server-owned sessions and Terminals. Atomic teardown is delegated to KILL_TERMINALS; no L2 collection verb is reintroduced.

3.2 phux.tui.layout/v1 — the layout tree

Scoped to a GroupId. The binary-split layout tree the reference TUI paints — one group’s “session” in tmux vocabulary:

Layout = {
    windows: list<Window>,
    focused_window_index: u32,
}

Window = {
    name: str,
    root: LayoutNode,
    focused_terminal: TerminalId,
}

LayoutNode = tagged_union {
    LEAF  { terminal_id: TerminalId, weight: u16 },
    SPLIT { direction: SplitDirection,
            children: list<LayoutNode>,
            weights: list<u16> },
    TABBED { children: list<LayoutNode>, active: u32 },  // reserved
}

SplitDirection = enum { HORIZONTAL = 0, VERTICAL = 1 }

The binary-split-not-n-ary decision from ADR-0012 applies to this layout schema — the TUI — not to the wire. Cross-terminal references inside a layout follow ADR-0027: a layout names terminals by TerminalId, and an L3 link to a terminal is a metadata value, not a second wire identity.

3.3 phux.tui.window_order/v1 and phux.tui.focus/v1

  • phux.tui.window_order/v1 — scoped to a GroupId; a list<u32> of stable window indices in display order, driving tab-bar ordering.
  • phux.tui.focus/v1 — per-client state (a Global key namespaced by client UUID, since the server exposes no ClientId scope). Records which terminal the local user is aiming input at; not synchronized across clients. The L1 INPUT_FOCUS message (input.md) is unrelated — it carries host-OS focus into the terminal so VT-aware programs can react.

3.4 What these conventions do NOT use

  • No “session” or “window” wire concept. Both are names for structure encoded in metadata.
  • No LAYOUT_CHANGED / FOCUS_CHANGED / WINDOW_* events. A change is METADATA_CHANGED on the relevant key; subscribers re-read the value.

3.5 Alternative consumers

A native GUI consumer mounting L3 MAY (and SHOULD) use its own metadata keys with a different prefix (e.g. app.foo.layout/v1) rather than reuse the TUI’s schema. Sharing schema across consumers is opt-in, not the default. An agent SDK consumer typically declares HELLO.layers = { L1 } and ignores this section.

Unlike the rest of §3, the schema of these two keys is normative (ADR-0027 decision point 4): tags and links are a cross-consumer projection over TerminalId, so their meaning MUST NOT drift between clients. The server still stores the bytes opaquely and interprets nothing; “normative” constrains the consumers, not the wire. Both ride the existing SET_METADATA / GET_METADATA / LIST_METADATA / SUBSCRIBE_METADATA verbs (§2) — no new wire tag.

  • phux.tags/v1 — scoped to a TerminalId. Value: a UTF-8 JSON array of tag strings, each non-empty and free of the # sigil, the array duplicate-free, e.g. ["build","ci"]. An empty array or an absent key both mean “no tags”. The #tag selector (ADR-0027 decision point 5; tui.md §3) resolves to the set of TerminalIds whose phux.tags/v1 value contains tag, evaluated client-side against the snapshot exactly as a session/window name resolves — the server stays selector-agnostic (ADR-0017).

  • phux.link/v1 — scoped to the source TerminalId. Value: a UTF-8 JSON array of link records { "target": u32, "kind": str }, where target is the linked Terminal’s local wire id and kind is an open enum. v1 defines "group" (a soft grouping edge); a consumer that reads an unknown kind MUST preserve it on rewrite rather than drop it, so the vocabulary grows additively. A link is a metadata value, never a second wire identity — there is no LinkId.

Per-key size limits (§2, recommended 256 KiB) apply; a tag/link set that would exceed them is the client’s concern to bound.

3.7 phux.agent/v1 — agent identity and lifecycle

Like §3.6, the schema of this key is normative (ADR-0040): agent identity is a cross-consumer projection over TerminalId, so its meaning MUST NOT drift between clients. The server stores the bytes opaquely on the L3 read/write path; a reference server MAY additionally act as a writer of this one key (ADR-0046, and see “Server as a producer” below). The key rides the existing SET_METADATA / GET_METADATA / DELETE_METADATA / SUBSCRIBE_METADATA verbs (§2) — no new wire tag.

  • phux.agent/v1 — scoped to the TerminalId the agent runs in. Value: a UTF-8 JSON object:

    {
      "name":      str,            // REQUIRED, non-empty human-facing name
      "kind":      optional<str>,  // open vocabulary slug, e.g. "claude", "codex"
      "state":     optional<str>,  // OPEN enum: "unknown" | "idle" | "working"
                                   //            | "blocked" | "done"
      "attention": optional<str>,  // OPEN enum: "none" | "low" | "normal" | "high"
      "session":   optional<str>   // free-form association label (fleet/job name)
    }

    state and attention are OPEN string enums: a consumer reading an unrecognized value MUST treat it as unknown (for state) or normal (for attention) rather than fail the parse, so the vocabulary grows additively. An absent state means unknown; an absent attention is derived from state (consumers conventionally map blocked to high). An absent key or a value that is not a JSON object with a non-empty name means “no declared agent”.

    Writes are whole-record (last writer wins); there are no field-merge semantics. DELETE_METADATA clears the declaration. The Terminal scope IS the terminal association; the per-Terminal store is dropped when the Terminal closes, so a record never outlives its pane.

    A consumer that finds this record MUST prefer it over heuristic derivations (OSC-title conventions such as the §-adjacent phux-ask sentinel of ADR-0035, or screen scraping); heuristics remain the fallback when the key is absent. Backs phux agent set/clear, the phux agent list/show/explain provenance-ranked report, and the reference TUI’s sidebar/tab labels.

    Server as a producer. A server MAY derive this record for a Terminal it owns — from the Terminal’s own OSC title, its live screen, or its PTY’s foreground process — and write it on the same L3 path any other writer uses (ADR-0046). This is a convention on top of the existing verbs, not a format change: a server-derived record is byte-identical in shape to a declared one, and a consumer neither can nor needs to distinguish them. A server that does so:

    • MUST NOT overwrite, with a derived value, a record whose state was supplied by an explicit SET_METADATA. An explicit declaration of state outranks any derivation for as long as the pane is occupied by the agent it describes; the derivation resumes after a DELETE_METADATA clears the record, or after the server withdraws the declaration under the rule below.
    • MAY withdraw an explicit declaration of state — by setting state to "unknown", never by substituting a derived value and never by DELETE_METADATA — when it 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 means an observation the server successfully made and which found no such agent; it is NOT an observation the server was unable to make. A server that cannot determine occupancy MUST hold the declaration. A withdrawal MUST preserve name, kind, and session; it SHOULD clear attention, whose basis was the state being withdrawn. A withdrawal MUST be idempotent at the byte level, so withdrawing an already-withdrawn record broadcasts nothing. Once withdrawn, the declaration no longer outranks the derivation. A record’s lifetime is bounded by its pane, but a declaration’s truth is bounded by its subject: this is the one rule that lets a server close that gap, and "unknown" is how it does so without ever asserting a state it did not derive.
    • MUST preserve the name, kind, and session fields of an identity-only declaration (one that supplied no state) when it fills state in.
    • MUST only DELETE_METADATA a record it authored itself, never one an explicit writer set.
    • SHOULD write only on a change of the derived value, so a long-running derived state produces no repeated METADATA_CHANGED broadcasts (a reference server already deduplicates an equal-bytes SET, §2).

    A consumer MUST NOT assume a server derives the record: the key is absent on a server that does not, exactly as before.

    Reading state: levels and edges. A consumer reads state in one of two ways, and the two readings do NOT assert the same thing. Which one a consumer is entitled to use follows from the shape of the gate it is building, not from convenience.

    A level read — “what does state say right now?” — asserts only that no contrary state is being asserted about that pane at this moment. idle carries the weakest claim of the vocabulary: it is the value a producer is expected to fall back to when it has no positive evidence of anything else, so a consumer MUST tolerate an idle level that is equally true of an agent that finished its turn, an agent that crashed, a pane running a pager or an editor, an agent still painting its splash screen, and a pane whose occupant the producer never identified. This is the same “no information” reading the spec already gives an absent state and an unrecognized state (both unknown); idle differs from those in provenance, not in the strength of what it asserts. A producer MAY publish idle on positive evidence, and nothing here forbids it — but the record carries no field distinguishing a positively-derived idle from a fallthrough one, so a consumer MUST NOT assume it is reading the former.

    An edge — a transition the consumer itself observed, from a value it read earlier to a different value it reads now — asserts strictly more than either level does: that whatever was asserting the old value stopped asserting it. working -> idle is positive evidence about a transition even though idle is not positive evidence about a condition.

    Therefore:

    • A consumer implementing a completion gate — anything that reports “the agent finished”, such as a blocking wait on a pane or a send-a-prompt-and-wait — MUST require an observed transition into one of the states it is waiting for, and MUST NOT be satisfied by a level read of the current state. A consumer that has only ever observed the pane in a state it accepts has observed no edge and MUST keep waiting, subject to its own timeout, rather than report completion. This is what keeps a wait from reporting success on a pane whose agent crashed, exited, or was never running.
    • A consumer implementing a safety gate — anything that declines to disturb a pane, such as refusing to scroll a screen that may be repainting, or refusing to write into a pane whose occupant may have changed — MAY read the level. Absence of contrary evidence is the correct predicate for “do not disturb this”, and a crashed or unidentified pane reading as “do not disturb” errs in the conservative direction.

    done needs its own caution. It is the one value in the vocabulary that no title- or screen-derived rule can honestly produce, so a producer that derives state from observation alone will never emit it. A consumer MUST NOT treat done as reachable on an arbitrary pane and MUST NOT read its absence as evidence that the agent did not finish. An exclusive wait for it is meaningful only when an integration supplies lifecycle evidence.

    In the reference server none of this is hypothetical. Its derivation (ADR-0046 decision point 5) is a fail-safe fallthrough: agent_detect returns idle whenever no state-bearing rule matched, and the five detection manifests the binary ships (crates/phux-server/rules/*.toml, for Claude Code, Codex, OpenCode, Pi, and OMP) declare eight working rules, five blocked rules, exactly one idle rule and zero done rules between them.

    That single idle rule is worth stating precisely, because it is the exception that shows where the general caution above comes from. It is claude.toml’s osc-progress-idle, and it derives from neither the title nor the screen: it reads Claude Code’s OSC 9;4 progress channel, on which the CLI explicitly states that the turn’s progress indicator was removed. claude.toml still records in prose why a title- or screen-derived positive idle rule was deliberately declined — the live chrome is byte-identical between idle and working, and the quiet title covers a pending permission dialog as well as an idle prompt. A consumer gains nothing from knowing which of these produced a given record, and the record carries no field distinguishing them, which is exactly why the level read above must tolerate the weakest reading regardless.

    The opt-in Claude hook shim supplies done through ADR-0085’s REPORT_AGENT_STATE, which feeds the detector without declaring metadata state and therefore without disabling later screen correction. done remains unreachable by derivation alone: no manifest rule produces it, on this server or in principle. A different server that authors further positive idle rules or another lifecycle integration is fully conforming; the requirements above constrain what a consumer MUST tolerate, not what every server MUST do.

3.7.1 phux.agent-session/v1 — native resume provenance

This record is distinct from phux.agent/v1: it authorizes reconstruction of a provider-native resume invocation after the live Terminal is gone, so its shape and writers are deliberately narrower (ADR-0068).

  • phux.agent-session/v1 — scoped to the exact local TerminalId returned by the agent’s spawn. Value: a UTF-8 JSON object:

    {
      "plugin_id":      str,
      "integration_id": str,
      "native_id":      str
    }

    All fields are REQUIRED and control-free. plugin_id and integration_id contain 1–120 UTF-8 bytes; native_id contains 1–1024 UTF-8 bytes and MUST NOT begin with -. Values are trimmed. The complete encoded record contains 1–4096 bytes; the reference server rejects empty or oversized values on both atomic spawn/create and ordinary reserved-key SET paths. Unknown fields invalidate v1 rather than extending executable authority accidentally.

    The record contains no executable path or argv. A restoring consumer MUST resolve the current enabled integration_id, MUST verify its unique owning plugin_id, and MUST construct resume argv through that integration’s structured policy. The identity placeholder MUST occupy one complete argv element, native_env MUST use a dedicated PHUX_*_SESSION_ID name, and template policy MUST NOT expose the identity as executable or evaluator source; a fixed plugin-owned interpreter script is permitted. Restore MUST fail closed on missing, stale, invalid, ambiguous, or ownership-mismatched policy. A writer SHOULD read the value back before reporting launch/restore success.

    Closing the Terminal drops this live record with its other metadata. Durable replay comes only from a consumer copying a validated record into a versioned workspace archive; the metadata key itself is not a database.

3.7.2 phux.pane-occupant/v1 — foreground process

This Terminal-scoped record publishes the privacy-bounded process fact needed by an available-shell safety gate. It uses the existing L3 read and subscription path; no frame, command, or capability is added.

  • phux.pane-occupant/v1 — scoped to a local TerminalId. Value: a UTF-8 JSON object:

    {
      "foreground":   str,
      "is_pane_shell": bool
    }

    Both fields are REQUIRED. foreground is the non-empty, control-free, login-dash-stripped basename of the foreground process group’s argv[0]. It MUST NOT contain a path separator. The record deliberately carries no pid, argv tail, cwd, environment, or command text.

    is_pane_shell is true only when the PTY’s foreground process-group id is the pane’s original child pid and foreground is a known interactive shell. It does not assert that every member of that process group is the shell. A consumer MUST treat an absent or malformed record as no answer, never as true.

    The owning server is the sole writer. Clients MAY GET_METADATA and SUBSCRIBE_METADATA, but MUST NOT SET_METADATA or DELETE_METADATA this key; workload-authenticated mutation is default-denied before the handler. The pre-profile reference server ignores such mutations. A server SHOULD reuse an already-required foreground-process query and SHOULD write only when either field changes. A failed process query is absence of evidence: the server MUST hold its last record rather than manufacture a transition. Terminal closure drops the record with the Terminal’s metadata.

    A safety gate MAY use a current true value as positive available-shell evidence and MUST refuse a current false value. Because the observation is periodic, stronger contradictory evidence observed by the consumer (for example an OSC-133 mark proving the cursor is not at a prompt) still wins.

3.8 phux.config.reload/v1 — the config-reload doorbell

A pure signal key (phux-foz.5): its broadcast, not its value, carries the meaning. The server stores the bytes opaquely and interprets nothing; the key rides the existing SET_METADATA / SUBSCRIBE_METADATA verbs (§2) — no new wire tag.

  • phux.config.reload/v1Global scope. Value: an opaque, writer-chosen nonce (the reference CLI writes a UTF-8 unix-nanos-pid string). Its only requirement is to DIFFER from the previously stored value: a reference server deduplicates an equal-bytes SET (no broadcast), so a repeated constant would ring the doorbell at most once.

    A consumer subscribed to this key treats a non-tombstone METADATA_CHANGED as “re-read your local configuration now”: it re-runs its own config load and rebuilds its config-derived state in place. Configuration itself NEVER crosses the wire — each consumer reads its own file, so hosts with different configs each apply their own. A consumer whose re-read fails MUST keep its previous configuration intact (surface the error locally; never crash, never half-apply). Under phux-workload/v1, ringing this doorbell requires SIGNAL on Global and DELETE_METADATA targeting it is default-denied. The pre-profile reference server ignores tombstones; deleting the key is housekeeping, not a reload request.

    Writers SHOULD validate their local config before ringing the doorbell so an obviously broken file fails at the writer with a useful error instead of fanning out no-op reload attempts. Backs phux config reload and the reference TUI’s in-place reload (tui.md §4.3).

View exact source