Skip to content

A server serves only a share of its own version, with the surfaces it offers

A server serves only a share of its own version, with the surfaces it offers

majordomus://adr/adr-0163proposed serverdistributionuiresilience

it references

referenced by

Nothing in the layer names it. Backlinks are derived, so an empty list is an answer rather than an omission.

163. A server serves only a share of its own version, with the surfaces it offers

Context

On 2026-10-14 the owner opened /ui/kiosk on the shared server of the primary checkout and got a dark page with a header and nothing under it. The browser's console said why, four times: ui.js, ui.css, observatory.js and favicon.svg answered 404. The server itself was healthy and current by every measure it had:

whatvalue
processpid 40191, ~/.local/share/majordomus/versions/0.26.0/libexec/majordomus-cli serve
executable version0.26.0, standing ready
MAJORDOMUS_SHARE in its environment<primary checkout>/share
that share's version.txtversion=0.21.0
that share's ui/absent: the primary checkout was 638 commits behind origin/master
the installed share/ beside the executableversion=0.26.0, ui/ complete
what the server said about itone WARN line in its log at start, nothing on any surface

A 0.26.0 executable was serving a 0.21.0 share. Its UI had no assets to serve, and every other thing it reads from share/ — the kinds, the schemas, the allow-lists, the command inventory — was five releases old as well. Nothing in serve status, the server's health, the peer board or the UI said so.

The vector is the repository supervising itself. lib/rust_bin.sh (mj_rust_share) makes bin/majordomus-cli and bin/majordomus-env export MAJORDOMUS_SHARE=<checkout>/share, and .envrc runs the latter, so every shell of a checkout carries it. That is right for the executable built from the same checkout, whose version is the share's by construction. It is wrong for any other executable started from that shell: the installed release that ~/.local/bin/majordomus launches, which the session-start hook's serve ensure starts the shared server from. Share::locate honours the variable before anything else, so the installed executable took the stale checkout's share without a word.

The two obvious repairs both failed on the same evening, which is part of the record:

  • serve --share <installed share> ensure started a server without --share: ensure does not pass it to the process it spawns, so the new server located the share again from its environment.
  • env -u MAJORDOMUS_SHARE serve ensure located <repository>/share, because it holds a kinds.yaml, which is all Share::locate asks of a share.

The server came back only when MAJORDOMUS_SHARE was set to the installed share by hand.

Decision

  1. A share has a version and surfaces, and a server knows both. A share's version is what its version.txt states (version=X.Y.Z, the projection of the crate manifest, ADR 0019). Its surfaces are the asset directories the server serves pages from: ui/ for /ui (ADR 0135) and cockpit/ for /cockpit. A share is skewed for an executable when its version.txt is absent, unreadable or states another version than the executable's own, or when a surface the executable serves has no directory in it. Version equality is the measure because it is the one the release already stamps on both halves; two commits of one version can still differ, and that finer skew is left to the commit hook's generate --check, which already refuses an underived tree.

  2. Where a share is found from a convention, a skewed one is passed over for one that matches. Share::locate keeps its order — --share, MAJORDOMUS_SHARE, the repository's share/, the installation's share/ beside the executable — and gains one judgement. --share is a person's word and is used as given, skewed or not. MAJORDOMUS_SHARE and the repository's share/ are conventions: a candidate from either that is skewed is passed over when a later candidate matches, and the share that is used says so in its origin, naming the candidate it passed over and why (installation; MAJORDOMUS_SHARE=<path> states 0.21.0, this executable is 0.26.0). This is the case of a checkout's environment reaching an installed executable, and it is the only case where the judgement changes the answer: an executable built from a checkout matches that checkout's share by construction.

  3. When no share matches, the server serves degraded and says so everywhere; it is not refused. A refused server does not repair anything under project.shared-server-resilience: every client would serve itself alone, from the same share, in as many processes as there are clients, and the skew would be harder to see, not easier. So the first candidate is used as today, and the skew becomes a fact of the server rather than a line in its log: server.status (serve status, majordomus_server, /api/v1/server) reports the served share — path, origin, version, the surfaces present — and a new standing, skewed, for a server that answers for this checkout from this executable's version but serves a share that is not its own. The UI and the Cockpit carry the same fact on every page they serve, and majordomus doctor reports it.

  4. The watcher is what already runs; nothing new runs in the background. No daemon is added and nothing polls, as project.the-server-sees-the-current-tree requires of a long-lived process.

    • serve ensure, which the session-start hook and .envrc already run, treats skewed the way it treats outdated since I2163: it starts a server whose own located share matches, and that server's election takes the lease over. The replaced server keeps the sessions it has and ends with them; a client is never left without a server. When no matching share can be located, ensure reports the skew with its remedy rather than replacing one skewed server with another.
    • A running server takes a fingerprint of its share — the content of version.txt and the presence of each surface's directory — on a request's own thread, beside the tree fingerprint it already takes, at most once per bounded interval. When it changes, the server decides its asset directories again and its standing follows: a share repaired under a running server is served without a restart, and a share that goes bad under it is reported on the next request.
  5. Development mode is a share located from the checkout. A share whose origin is MAJORDOMUS_SHARE or the repository is this repository, or an adopter, supervising itself from source; that is where a checkout's environment and an installed executable meet, and where this decision binds. A share whose origin is the installation shipped with its executable (ADR 0019): skew there is a broken installation, and doctor reports it as one.

  6. The rule is project.a-server-serves-its-own-share. It states points 1–4 as required behaviour, is blocking, and names as its tests the cases the implementing changes add. A server that answers ready while serving a share of another version is the defect it refuses.

What is delivered with this decision, and what is not

This record is delivered alone, so that the changes it decides can land as small, separately reviewed pull requests in this order:

stepchangedepends on
S1serve ensure passes --share to the server it starts—
S2the Observatory reads its page again when it becomes visible, so a hidden display does not say the server is not answering—
S3server.status reports the served share and the skewed standing (point 3)—
S4Share::locate passes over a skewed conventional share for a matching one (point 2)S3
S5serve ensure replaces a skewed server, and a running server re-reads its share's fingerprint (point 4)S3
S6the rule project.a-server-serves-its-own-share, with doctor reporting skew (points 5, 6)S4, S5
S7the manual, the landing page and the published site describe the served shareS6

S2 is not a consequence of the skew. It was found while diagnosing it, it touches the same screen, and it is listed so that it is not lost.

Alternatives rejected

Refuse to start a server on a skewed share. It looks strict and is not: under project.shared-server-resilience each client then serves itself alone from the same share, and the defect multiplies while every surface that would have reported it disappears with the shared server.

Stop exporting MAJORDOMUS_SHARE from the checkout's launchers. The variable is how a checkout's own executable and scripts find that checkout's share, and it is one of several ways the variable reaches a process: a person's shell, a hook's environment, a parent session. Removing it from one launcher leaves every other way open. The judgement belongs where every caller passes through, Share::locate.

Compile the UI's assets into the executable. It would have hidden this incident and nothing else: the kinds and schemas would still have been five releases old. share/ is read at run time on purpose (apps/majordomus-cli/src/share.rs), and the assets are more of the same.

A watcher process that restarts servers. A second long-lived process to supervise the first is the daemon ADR 0003 declined: the shared server is a client's child, and nothing keeps it alive but clients. serve ensure already runs at every session start and every shell entry, which is as often as anybody can start reading a server's answers.

Consequences

  • An installed executable started from a checkout's shell serves its own share, and says which share it passed over. The most common skew therefore repairs itself at the next session start, with no person asked to do anything.
  • A skew that cannot be repaired is a standing every reader of serve status sees, a fact on every page of the UI, and a doctor finding, instead of one log line.
  • Replacing a skewed server ends its mesh links and re-establishes them from the new process, as any server replacement does; the runtime's journal is unaffected (ADR 0067).
  • skewed is a fifth word every consumer of ServerStanding must handle: the session briefing, serve status, the board's gathering of other checkouts' servers (ADR 0044) and the cases that assert on the words.

Where this comes from

canonical file
.ai/repo/adrs/0163-a-server-serves-only-a-share-of-its-own-version.md
identity
adr-0163
also served as
MCP resource majordomus://adr/adr-0163 · majordomus entity show adr/adr-0163 · GET /api/v1/entity?uri=majordomus://adr/adr-0163
in the Cockpit
/cockpit/objects/adr/adr-0163

This page is generated from the file above by scripts/generate-site-data; editing the page changes nothing, editing the file changes every surface. The relations above are edges of docs/generated/graph.json, which the executable writes from the same index.

Searches the titles and descriptions of every page this site publishes.