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:
| what | value |
|---|---|
| process | pid 40191, ~/.local/share/majordomus/versions/0.26.0/libexec/majordomus-cli serve |
| executable version | 0.26.0, standing ready |
MAJORDOMUS_SHARE in its environment | <primary checkout>/share |
that share's version.txt | version=0.21.0 |
that share's ui/ | absent: the primary checkout was 638 commits behind origin/master |
the installed share/ beside the executable | version=0.26.0, ui/ complete |
| what the server said about it | one 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> ensurestarted a server without--share:ensuredoes not pass it to the process it spawns, so the new server located the share again from its environment.env -u MAJORDOMUS_SHARE serve ensurelocated<repository>/share, because it holds akinds.yaml, which is allShare::locateasks of a share.
The server came back only when MAJORDOMUS_SHARE was set to the installed share by hand.
Decision
-
A share has a version and surfaces, and a server knows both. A share's version is what its
version.txtstates (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) andcockpit/for/cockpit. A share is skewed for an executable when itsversion.txtis 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'sgenerate --check, which already refuses an underived tree. -
Where a share is found from a convention, a skewed one is passed over for one that matches.
Share::locatekeeps its order —--share,MAJORDOMUS_SHARE, the repository'sshare/, the installation'sshare/beside the executable — and gains one judgement.--shareis a person's word and is used as given, skewed or not.MAJORDOMUS_SHAREand the repository'sshare/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. -
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, andmajordomus doctorreports it. -
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-treerequires of a long-lived process.serve ensure, which the session-start hook and.envrcalready run, treatsskewedthe way it treatsoutdatedsince 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,ensurereports 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.txtand 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.
-
Development mode is a share located from the checkout. A share whose origin is
MAJORDOMUS_SHAREor 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, anddoctorreports it as one. -
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 answersreadywhile 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:
| step | change | depends on |
|---|---|---|
| S1 | serve ensure passes --share to the server it starts | — |
| S2 | the Observatory reads its page again when it becomes visible, so a hidden display does not say the server is not answering | — |
| S3 | server.status reports the served share and the skewed standing (point 3) | — |
| S4 | Share::locate passes over a skewed conventional share for a matching one (point 2) | S3 |
| S5 | serve ensure replaces a skewed server, and a running server re-reads its share's fingerprint (point 4) | S3 |
| S6 | the rule project.a-server-serves-its-own-share, with doctor reporting skew (points 5, 6) | S4, S5 |
| S7 | the manual, the landing page and the published site describe the served share | S6 |
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 statussees, a fact on every page of the UI, and adoctorfinding, 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).
skewedis a fifth word every consumer ofServerStandingmust handle: the session briefing,serve status, the board's gathering of other checkouts' servers (ADR 0044) and the cases that assert on the words.