THE single source of truth for gateway <-> client version compatibility.
Vis ships three INDEPENDENTLY updatable halves that share one HTTP/SSE
wire: the gateway daemon, the TUI/CLI client, and the Vis Companion app.
Any of them can lag. A daemon started by yesterday's binary keeps running
across brew upgrade; a phone holds a cached web build for weeks; a
Tailscale peer runs last month's release. Without an explicit contract a
breaking wire change surfaces as a mystery 404, a missing field, or an
event type nobody renders.
So every peer publishes two numbers next to its human release version:
protocol the wire protocol IT speaks
min-* the OLDEST counterpart it still speaks to
Compatibility is then a pure comparison (verdict) — never feature
sniffing, never guessing from a release string. Both halves advertise, both
halves judge, so whichever side is newer can explain the mismatch even when
the other side is too old to know the concept exists.
Bump protocol-version on any BREAKING wire change (a removed field, a
renamed event type, a changed status shape). Raise min-client-protocol
or min-gateway-protocol only when the old shape genuinely stops
working — additive changes keep the number and are negotiated through
/v1/capabilities features instead.
Wire shape (snake_case strings, per the gateway wire contract):
{"protocol": 1, "min_client": 1, "min_gateway": 1, "version": "0.1.5"}
THE single source of truth for gateway <-> client version compatibility.
Vis ships three INDEPENDENTLY updatable halves that share one HTTP/SSE
wire: the gateway daemon, the TUI/CLI client, and the Vis Companion app.
Any of them can lag. A daemon started by yesterday's binary keeps running
across `brew upgrade`; a phone holds a cached web build for weeks; a
Tailscale peer runs last month's release. Without an explicit contract a
breaking wire change surfaces as a mystery 404, a missing field, or an
event type nobody renders.
So every peer publishes two numbers next to its human release version:
`protocol` the wire protocol IT speaks
`min-*` the OLDEST counterpart it still speaks to
Compatibility is then a pure comparison ([[verdict]]) — never feature
sniffing, never guessing from a release string. Both halves advertise, both
halves judge, so whichever side is newer can explain the mismatch even when
the other side is too old to know the concept exists.
Bump [[protocol-version]] on any BREAKING wire change (a removed field, a
renamed event type, a changed status shape). Raise [[min-client-protocol]]
or [[min-gateway-protocol]] only when the old shape genuinely stops
working — additive changes keep the number and are negotiated through
`/v1/capabilities` features instead.
Wire shape (snake_case strings, per the gateway wire contract):
{"protocol": 1, "min_client": 1, "min_gateway": 1, "version": "0.1.5"}Request header naming the client (vis-tui, vis-cli, vis-companion).
Request header naming the client (`vis-tui`, `vis-cli`, `vis-companion`).
(client-headers client-name)Headers every Vis client stamps on every gateway request so the gateway can
judge it without a separate negotiation round-trip. client-name is a short
stable id (vis-tui, vis-cli).
Headers every Vis client stamps on every gateway request so the gateway can judge it without a separate negotiation round-trip. `client-name` is a short stable id (`vis-tui`, `vis-cli`).
(client-verdict client-name gateway-handshake)A client's judgement of the gateway it just probed, given that gateway's
ENGINE handshake map (the wire->handshake of its advertised protocol
block; nil/all-nil = a gateway too old to advertise one). client-name names
this client in the rendered copy.
A client's judgement of the gateway it just probed, given that gateway's ENGINE handshake map (the [[wire->handshake]] of its advertised `protocol` block; nil/all-nil = a gateway too old to advertise one). `client-name` names this client in the rendered copy.
Request header carrying the client's human release version.
Request header carrying the client's human release version.
(explain {:keys [reason gateway-protocol gateway-min-client client-protocol
client-min-gateway client-name]})Human copy for a verdict: a title, one plain-language summary, and ORDERED remedy steps. One writer for every surface (terminal panel, gateway 426 body, companion screen) so the wording never drifts between them.
Human copy for a verdict: a title, one plain-language summary, and ORDERED remedy steps. One writer for every surface (terminal panel, gateway 426 body, companion screen) so the wording never drifts between them.
(gateway-verdict request)The gateway's own judgement of one inbound request: verdict with this
build filled in as the gateway half.
The gateway's own judgement of one inbound request: [[verdict]] with this build filled in as the gateway half.
(handshake)What THIS build advertises about itself. Emitted by the gateway on
/healthz, /v1/admin/status, and /v1/capabilities so even the cheapest
probe a client already makes carries the verdict inputs.
What THIS build advertises about itself. Emitted by the gateway on `/healthz`, `/v1/admin/status`, and `/v1/capabilities` so even the cheapest probe a client already makes carries the verdict inputs.
(incompatible-ex verdict-map)The ex-info a client throws when it refuses to drive an incompatible
gateway. Carries :vis/user-error (so the top-level CLI prints it clean and
exits 2) plus :vis/panel, the pre-rendered panel-lines screen.
The ex-info a client throws when it refuses to drive an incompatible gateway. Carries `:vis/user-error` (so the top-level CLI prints it clean and exits 2) plus `:vis/panel`, the pre-rendered [[panel-lines]] screen.
Oldest CLIENT protocol this gateway still serves. Raise only when a client at the old protocol can no longer be served correctly — a client below this is refused with 426 instead of being fed a shape it cannot read.
Oldest CLIENT protocol this gateway still serves. Raise only when a client at the old protocol can no longer be served correctly — a client below this is refused with 426 instead of being fed a shape it cannot read.
Request header carrying the oldest gateway protocol the client accepts.
Request header carrying the oldest gateway protocol the client accepts.
Oldest GATEWAY protocol this client still accepts. Raise only when this client can no longer drive an older daemon correctly.
Oldest GATEWAY protocol this client still accepts. Raise only when this client can no longer drive an older daemon correctly.
(panel-lines {:keys [gateway-protocol gateway-min-client gateway-version
client-protocol client-min-gateway client-version
client-name upgrade]
:as v})The terminal SCREEN for an incompatible peer: a boxed, colour-free panel of plain lines. Returned as data (never printed here) so the CLI, the TUI, and tests all render the exact same block.
The terminal SCREEN for an incompatible peer: a boxed, colour-free panel of plain lines. Returned as data (never printed here) so the CLI, the TUI, and tests all render the exact same block.
Request header carrying the client's own protocol number.
Request header carrying the client's own protocol number.
Wire protocol number THIS build speaks. Monotonic; bump on every BREAKING change to the gateway HTTP/SSE surface.
Wire protocol number THIS build speaks. Monotonic; bump on every BREAKING change to the gateway HTTP/SSE surface.
(release-version)Human release version of this build: the vis/VERSION resource written at
build time (git describe), else "dev". Purely informational — never
compared; protocol-version is the contract.
Human release version of this build: the `vis/VERSION` resource written at build time (git describe), else "dev". Purely informational — never compared; [[protocol-version]] is the contract.
(request->client request)Read the client's advertised protocol out of a Ring request's headers. A
client that stamps nothing yields nils and is GRANDFATHERED by verdict:
an unknown peer is never refused, only an explicitly too-old one.
Read the client's advertised protocol out of a Ring request's headers. A client that stamps nothing yields nils and is GRANDFATHERED by [[verdict]]: an unknown peer is never refused, only an explicitly too-old one.
(verdict {:keys [gateway-protocol gateway-min-client gateway-version
client-protocol client-min-gateway client-version
client-name]})PURE compatibility verdict between a gateway and a client. Both sides pass the same keys and get the same answer, so the TUI, the companion app, and the gateway itself never disagree about who is out of date.
Reasons:
ok both halves speak a protocol the other still accepts
client-too-old client protocol < the gateway's min-client
gateway-too-old gateway protocol < the client's min-gateway
unknown a peer advertised nothing — grandfathered as compatible
:upgrade names WHICH half the user must update, so a UI can render one
unambiguous instruction instead of "something is out of date".
PURE compatibility verdict between a gateway and a client. Both sides pass the same keys and get the same answer, so the TUI, the companion app, and the gateway itself never disagree about who is out of date. Reasons: `ok` both halves speak a protocol the other still accepts `client-too-old` client protocol < the gateway's `min-client` `gateway-too-old` gateway protocol < the client's `min-gateway` `unknown` a peer advertised nothing — grandfathered as compatible `:upgrade` names WHICH half the user must update, so a UI can render one unambiguous instruction instead of "something is out of date".
(wire->handshake m)Read a peer's advertised handshake out of the canonical string-keyed wire
map ({"protocol" 1 "min_client" 1 ...}), tolerating a peer old enough
to advertise nothing at all (every field comes back nil).
Read a peer's advertised handshake out of the canonical string-keyed wire
map (`{"protocol" 1 "min_client" 1 ...}`), tolerating a peer old enough
to advertise nothing at all (every field comes back nil).cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |