L3 — Metadata storage
The OPTIONAL typed key-value service the server hosts but does not interpret.
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 }—GroupIdis 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):
| ID | Direction | Name | Reference | Status |
|---|---|---|---|---|
| 0x50 | C → S (cmd) | GET_METADATA | §2 | shipped |
| 0x51 | C → S (cmd) | SET_METADATA | §2 | shipped |
| 0x52 | C → S (cmd) | DELETE_METADATA | §2 | shipped |
| 0x53 | C → S (cmd) | LIST_METADATA | §2 | shipped |
| 0x54 | C → S | SUBSCRIBE_METADATA | §2 | shipped |
| 0xD0 | S → C | METADATA_CHANGED | §1 | shipped |
| 0xD1 | S → C | METADATA_VALUE | §1 | shipped |
| 0xD2 | S → C | METADATA_KEYS | §1 | shipped |
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 aSET,value: None(a tombstone) on aDELETE.METADATA_VALUE { request_id, value }— correlates to a priorGET_METADATA.request_id, withvalue: Nonewhen the key is absent.METADATA_KEYS { request_id, keys }— correlates to a priorLIST_METADATA.request_id; keys are lexicographically sorted and values are NOT included (clients fetch them withGET_METADATA, since aLISTresult 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. ASETon this key is a rename; there is noRENAME_SESSIONwire 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 aMETADATA_CHANGEDcarrying the appliedcurrent\0newvalue to subscribers of the written(scope, key); a refused or no-op rename broadcasts nothing, and the value is not retained in the store (aGETon 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>}.commandpreserves argv boundaries,cwdselects the seed pane’s working directory, andenvis added to the seed process environment. A presentagent_sessioncontains 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 oldCREATE_SESSIONverb. Underphux-workload/v1, this server-interpretedSET_METADATArequires bothCREATEandBINDon 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}. BecauseSET_METADATAhas no reply body, the creating connection reads its exact nonce-specific key and verifies bothnameandrequest_token; other connections receive an absent value even if they know the nonce. This prefix is server-owned: underphux-workload/v1, ordinarySET_METADATA,DELETE_METADATA, andSUBSCRIBE_METADATAtargeting 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 fromLIST_METADATA. The un-suffixedphux.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 aGroupId; alist<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 noClientIdscope). Records which terminal the local user is aiming input at; not synchronized across clients. The L1INPUT_FOCUSmessage (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 isMETADATA_CHANGEDon 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.
3.6 phux.tags/v1 and phux.link/v1 — terminal tags and links
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 aTerminalId. 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#tagselector (ADR-0027 decision point 5; tui.md §3) resolves to the set ofTerminalIds whosephux.tags/v1value containstag, 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 sourceTerminalId. Value: a UTF-8 JSON array of link records{ "target": u32, "kind": str }, wheretargetis the linked Terminal’s local wire id andkindis an open enum. v1 defines"group"(a soft grouping edge); a consumer that reads an unknownkindMUST 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 noLinkId.
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 theTerminalIdthe 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) }stateandattentionare OPEN string enums: a consumer reading an unrecognized value MUST treat it asunknown(forstate) ornormal(forattention) rather than fail the parse, so the vocabulary grows additively. An absentstatemeansunknown; an absentattentionis derived fromstate(consumers conventionally mapblockedtohigh). An absent key or a value that is not a JSON object with a non-emptynamemeans “no declared agent”.Writes are whole-record (last writer wins); there are no field-merge semantics.
DELETE_METADATAclears 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-asksentinel of ADR-0035, or screen scraping); heuristics remain the fallback when the key is absent. Backsphux agent set/clear, thephux agent list/show/explainprovenance-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
statewas supplied by an explicitSET_METADATA. An explicit declaration ofstateoutranks any derivation for as long as the pane is occupied by the agent it describes; the derivation resumes after aDELETE_METADATAclears the record, or after the server withdraws the declaration under the rule below. - MAY withdraw an explicit declaration of
state— by settingstateto"unknown", never by substituting a derived value and never byDELETE_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 preservename,kind, andsession; it SHOULD clearattention, 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, andsessionfields of an identity-only declaration (one that supplied nostate) when it fillsstatein. - MUST only
DELETE_METADATAa 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
stateproduces no repeatedMETADATA_CHANGEDbroadcasts (a reference server already deduplicates an equal-bytesSET, §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 readsstatein 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
statesay right now?” — asserts only that no contrary state is being asserted about that pane at this moment.idlecarries 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 anidlelevel 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 absentstateand an unrecognizedstate(bothunknown);idlediffers from those in provenance, not in the strength of what it asserts. A producer MAY publishidleon positive evidence, and nothing here forbids it — but the record carries no field distinguishing a positively-derivedidlefrom 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->idleis positive evidence about a transition even thoughidleis 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.
doneneeds 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 derivesstatefrom observation alone will never emit it. A consumer MUST NOT treatdoneas 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_detectreturnsidlewhenever 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 eightworkingrules, fiveblockedrules, exactly oneidlerule and zerodonerules between them.That single
idlerule is worth stating precisely, because it is the exception that shows where the general caution above comes from. It isclaude.toml’sosc-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.tomlstill records in prose why a title- or screen-derived positiveidlerule 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
donethrough ADR-0085’sREPORT_AGENT_STATE, which feeds the detector without declaring metadata state and therefore without disabling later screen correction.doneremains unreachable by derivation alone: no manifest rule produces it, on this server or in principle. A different server that authors further positiveidlerules or another lifecycle integration is fully conforming; the requirements above constrain what a consumer MUST tolerate, not what every server MUST do. - MUST NOT overwrite, with a derived value, a record whose
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 localTerminalIdreturned 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_idandintegration_idcontain 1–120 UTF-8 bytes;native_idcontains 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 owningplugin_id, and MUST construct resume argv through that integration’s structured policy. The identity placeholder MUST occupy one complete argv element,native_envMUST use a dedicatedPHUX_*_SESSION_IDname, 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 localTerminalId. Value: a UTF-8 JSON object:{ "foreground": str, "is_pane_shell": bool }Both fields are REQUIRED.
foregroundis the non-empty, control-free, login-dash-stripped basename of the foreground process group’sargv[0]. It MUST NOT contain a path separator. The record deliberately carries no pid, argv tail, cwd, environment, or command text.is_pane_shellis true only when the PTY’s foreground process-group id is the pane’s original child pid andforegroundis 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 astrue.The owning server is the sole writer. Clients MAY
GET_METADATAandSUBSCRIBE_METADATA, but MUST NOTSET_METADATAorDELETE_METADATAthis 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
truevalue as positive available-shell evidence and MUST refuse a currentfalsevalue. 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/v1—Globalscope. Value: an opaque, writer-chosen nonce (the reference CLI writes a UTF-8unix-nanos-pidstring). Its only requirement is to DIFFER from the previously stored value: a reference server deduplicates an equal-bytesSET(no broadcast), so a repeated constant would ring the doorbell at most once.A consumer subscribed to this key treats a non-tombstone
METADATA_CHANGEDas “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). Underphux-workload/v1, ringing this doorbell requiresSIGNALon Global andDELETE_METADATAtargeting 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 reloadand the reference TUI’s in-place reload (tui.md §4.3).