The host delivers its own successor, and versions live side by side

The supervision was already right: a clean exit means the host stood aside, and
the launcher's next turn runs what is on disk. Two things made it dead code —
nothing told the running host a successor was waiting, and the rollback resolved
its known-good version through pacman, which no machine here uses and which two
of three operating systems do not have.

Keeping a version rather than a path was the clue. Versions now live in
directories named for them:

- the launcher picks the newest delivered one every time round the loop, or the
  one a rollback pinned, or the host placed by hand when nothing is delivered;
- the running host stands aside between reconciles, never inside one, by exiting
  cleanly — and returns nil so the launcher does not count it as a crash;
- a completed reconcile retires what is older than the predecessor, keeping the
  predecessor because that is what a rollback starts, and never the running one;
- rollback pins the predecessor instead of reinstalling a package: no package
  manager, no cache anyone may clean, same script on every operating system;
- the report says which host version produced it, so 'behind' is answerable.

Newest is when it arrived, never how the name sorts: '1.10' orders before '1.9',
and ordering by name would start an older host and call it an upgrade.

novox/hq ADR 0141. The delivery half — a module carrying the next host — follows;
until then nothing delivers a version and every machine takes the fallback, which
is what it does today.
This commit is contained in:
2026-09-29 00:29:36 +02:00
parent b005d4ef17
commit fbf0fb7d63
8 changed files with 592 additions and 73 deletions
+65
View File
@@ -187,5 +187,70 @@ sleep 1
check "a crash is counted" "unlike a clean exit, which is not" "$(count)" "1"
kill -TERM "$LP" 2>/dev/null; sleep 1; pkill -f "$MESH_HOST_BIN" 2>/dev/null || true
# --- which version it runs (novox/hq ADR 0141) ------------------------------------------------
#
# Versions live side by side in directories named for them. The launcher picks one every time round
# the loop, never once: standing aside for a successor is a clean exit, and the next turn has to run
# what is on disk NOW — resolved once, the same binary would restart for ever and no upgrade would
# ever take.
# deliver a version as the mesh would, recording which one ran so a test can assert the choice.
deliver() {
mkdir -p "$MESH_HOST_LIBEXEC/versions/$1"
cat > "$MESH_HOST_LIBEXEC/versions/$1/nox-mesh-host" <<STUB
#!/bin/sh
echo "$1" >> "\$MESH_HOST_STATE_DIR/which.ran"
exit "\${STUB_HOST_EXIT:-1}"
STUB
chmod +x "$MESH_HOST_LIBEXEC/versions/$1/nox-mesh-host"
# When it arrived is what "newest" means, so it is set rather than left to the clock.
touch -d "$2" "$MESH_HOST_LIBEXEC/versions/$1/nox-mesh-host" "$MESH_HOST_LIBEXEC/versions/$1"
}
which_ran() { cat "$MESH_HOST_STATE_DIR/which.ran" 2>/dev/null || echo NONE; }
# Newest is when it arrived, not how its name sorts: "1.10" orders before "1.9" by name, so ordering
# by name would run an older host and call it an upgrade.
setup
deliver 1.10 "2 hours ago"
deliver 1.9 "1 hour ago"
"$LAUNCH" >/dev/null 2>&1 || true
check "runs the newest delivered version" "newest is when it arrived, not how the name sorts" \
"$(which_ran)" "1.9"
# A pin from a rollback beats the newest, or the launcher would start the failing binary again and
# the rollback would flap.
setup
deliver 1.9 "2 hours ago"
deliver 2.0 "1 hour ago"
echo 1.9 > "$MESH_HOST_STATE_DIR/rollback-pinned"
"$LAUNCH" >/dev/null 2>&1 || true
check "a pinned version beats the newest" "otherwise a rollback starts the binary it just rejected" \
"$(which_ran)" "1.9"
# A pin naming a version that is not there is ignored rather than fatal: the machine choosing for
# itself is better than a machine that starts nothing.
setup
deliver 2.0 "1 hour ago"
echo 1.9 > "$MESH_HOST_STATE_DIR/rollback-pinned"
"$LAUNCH" >/dev/null 2>&1 || true
check "an undeliverable pin is ignored" "a machine that starts nothing is worse than one that chooses" \
"$(which_ran)" "2.0"
# An interrupted delivery leaves a directory with no binary in it. Treating it as the newest would
# mean running nothing.
setup
deliver 1.9 "2 hours ago"
mkdir -p "$MESH_HOST_LIBEXEC/versions/2.0-half"
touch -d "1 minute ago" "$MESH_HOST_LIBEXEC/versions/2.0-half"
"$LAUNCH" >/dev/null 2>&1 || true
check "skips a version with no binary" "a directory is not a version; the binary is" \
"$(which_ran)" "1.9"
# Nothing delivered: the host placed by hand, which is how the first one always arrives. Without this
# the change would strand every machine in the mesh on the day it ships.
setup
"$LAUNCH" >/dev/null 2>&1 || true
check "falls back to the host placed by hand" "every first host arrives this way" "$(started)" "yes"
printf '\nlaunch: %d passed, %d failed\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]