Compare commits

...

1 Commits

Author SHA1 Message Date
joakimp 34cf1e3810 release: v1.8.8, and a notice that named the wrong remedy
Lint / hadolint (push) Successful in 14s
Lint / actionlint (push) Successful in 17s
Publish Docker Image / resolve-versions (push) Successful in 10s
Publish Docker Image / base-decide (push) Successful in 19s
Publish Docker Image / build-base (push) Successful in 1h3m31s
Publish Docker Image / smoke (push) Successful in 4m47s
Publish Docker Image / smoke-studio (push) Successful in 8m8s
Publish Docker Image / build-variant-studio (push) Successful in 20m4s
Publish Docker Image / build-variant (push) Successful in 26m9s
Publish Docker Image / promote-base-latest (push) Successful in 9s
Publish Docker Image / update-description (push) Successful in 14s
Freezes the v1.8.8 section and clears the two non-code checklist items
pi@emb-7kj4vr4g handed over (evt_20260826T134919_a614ecfc2d4f), plus the two
carried nits from its round-2 verification (evt_20260826T133356_d56792791a49).
Every claim below was re-measured here rather than taken from the handoff.

THE STALENESS NOTICE ASSERTED A DIRECTION IT NEVER TESTED — Blocker 1's shape,
one layer down, in the message I added to replace the message that named the
wrong cause. The notice fired on "recorded != HEAD" and then announced HEAD as
the newer side without testing ancestry, so a clone that was merely BEHIND got
"has moved to 82a8d3c; the snapshot describes the older 5fd0d5c" when 82a8d3c is
5fd0d5c's ANCESTOR. Found by EMB against the real state of its own host, not a
fabrication. The verdict was never wrong (rc 0, nothing mis-verified) but the
remedy it implies is a ~67-minute base rebuild when the actual fix is `git pull`
— the only one of the two carried nits with a price tag, which is why it went
first. Now tests ancestry with the merge-base --is-ancestor primitive the refresh
path 60 lines below already used, and reports three verdicts: stale (refresh),
clone behind (pull, do NOT refresh), diverged (reconcile). All three verified by
execution; only the first was correct before. --help no longer errors on the one
script whose argument order was itself a landmine. The `-s "$VENDORED"` guard is
now commented as load-bearing: it makes the empty-stdin collision unreachable by
construction, which also means no test below exercises it any more, so deleting
it as "redundant with the probes" would silently restore the false OK.

CHANGELOG: retitled, and three stale spots fixed in what becomes the permanent
record. It cited the skill at 82a8d3c (twice superseded); it RE-ASSERTED the
retracted mailbox measurement as live evidence 200 lines after withdrawing it,
which is the exact non-contradiction failure this release exists to fix; and its
warning block still described the pre-e8ddeaf world ("still records c04cd15",
"now exits 1") while quoting as exemplary the very notice whose direction was
unverified. Per EMB's steer the conclusion was kept and only the evidence
replaced: the status filter does drop broadcast noise, it just never computed
owed-ness. Honest replacement, measured on both machines: raw filter returns 2
here and 1 there, EVERY ONE already answered, derivation returns 0 for both.

SNAPSHOT RESYNCED AGAIN, 5fd0d5c -> 6eb20af, because skillset 6eb20af adds the
limit of my own seq ordering test: seq is REPLICA-LOCAL, equal to origin_seq only
because one replica authors for all four machines, so use hlc once mesh_peers
reports a peer. Recorded as reasoning not measurement — a second replica cannot
be stood up here. The durable half is the asymmetry: seq skew makes an ANSWERED
item resurface (noise, visible, self-correcting) while created_at SUPPRESSES AN
UNANSWERED ask forever (silent, permanent), so the skill now says outright that
"fixing" a resurfacing item with a timestamp trades the safe failure for the
dangerous one. The resync was free: rootfs/ was already changing, so the base
rebuild was forced regardless — the ordering warning about accidental staleness
does not apply to a deliberate refresh before the tag.

--check is a clean OK at 6eb20af with no notice, canary re-verified bidirectionally
(present 3, withdrawn 0), baked snapshot 0644, tree hash recomputed at build time
and re-verified in-container. bash -n clean; shellcheck/hadolint/actionlint remain
absent locally, so CI is still the only evidence for those.
2026-08-26 15:56:03 +02:00
4 changed files with 102 additions and 23 deletions
+56 -19
View File
@@ -11,7 +11,7 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
--- ---
## Unreleased ## v1.8.8 — 2026-08-26
The vendored `mempalace` skill snapshot stops being anonymous, and the The vendored `mempalace` skill snapshot stops being anonymous, and the
container starts saying which copy of each skill it is actually reading. container starts saying which copy of each skill it is actually reading.
@@ -45,6 +45,23 @@ being fixed.**
only, `2` cannot determine (ref absent from this clone). `AGENTS.md` step 2 only, `2` cannot determine (ref absent from this clone). `AGENTS.md` step 2
rewritten to state all three, since its promise that "the message rewritten to state all three, since its promise that "the message
distinguishes the two" was exactly what the branch was breaking. distinguishes the two" was exactly what the branch was breaking.
- **The staleness `NOTICE` then asserted a direction it had never tested** — the
same defect one layer down, found by pi@emb-7kj4vr4g against the real state of
its own host. The branch fired the notice on "recorded ≠ HEAD" and announced
that HEAD was the newer side, so a clone that was merely *behind* was told
*"has moved to 82a8d3c; the snapshot describes the older 5fd0d5c"* when
`82a8d3c` is `5fd0d5c`'s **ancestor**. Harmless to the verdict (`rc` stayed 0,
nothing was mis-verified) but it points the operator at a refresh — a
~67-minute base rebuild — when the real remedy is `git pull`. It now tests
ancestry with the `merge-base --is-ancestor` primitive the refresh path two
sections above already used, and reports three distinct verdicts: **stale**
(recorded is an ancestor — refresh), **your clone is behind** (HEAD is an
ancestor — pull, do not refresh), **diverged** (neither). All three verified by
execution; only the first was right before.
- **`--help` died with `unknown option: --help`.** The strict argument loop that
closed the silent-ignore hole never added a `--help` case, so the one script
whose argument *order* was itself a landmine had an erroring discoverability
path. It now prints its own header block.
- **`VENDORED.md` contradicted itself, in the release whose stated invariant is - **`VENDORED.md` contradicted itself, in the release whose stated invariant is
non-contradiction.** Its hand-maintained "Snapshot provenance at last refresh" non-contradiction.** Its hand-maintained "Snapshot provenance at last refresh"
line named skillset `670f7f1` — seven commits behind the ARG, and *the very line named skillset `670f7f1` — seven commits behind the ARG, and *the very
@@ -88,8 +105,22 @@ earlier. Fixed in skillset `5fd0d5c`, which derives owed-ness by joining on
terminal reply suppresses every later ask on the same thread forever. Because terminal reply suppresses every later ask on the same thread forever. Because
the skillset is mounted live on every enrolled host, that correction was already the skillset is mounted live on every enrolled host, that correction was already
deployed fleet-wide before this image was built; the vendored snapshot is deployed fleet-wide before this image was built; the vendored snapshot is
resynced to it (`c04cd15` → `5fd0d5c`) so the no-clone fallback does not ship resynced to it (`c04cd15` → `5fd0d5c` → `6eb20af`) so the no-clone fallback does
the withdrawn rule. Canary re-verified bidirectionally against the new bytes. not ship the withdrawn rule. Canary re-verified bidirectionally against the new
bytes.
`6eb20af` adds the limit of that ordering test, found when pi@emb-7kj4vr4g
verified it rather than adopting it: **`seq` is replica-local.** It equals
`origin_seq` today only because one replica authors events for all four machines,
so a second replica could order the same pair differently and derive a different
owed-set from the same log — use `hlc` (already on every event, total and
causally consistent) once `mesh_peers` reports any peer. Documented as reasoning,
not measurement, since a second replica cannot be stood up to test it. The part
worth keeping is the **asymmetry**: local-`seq` skew makes an answered item
*resurface* (noise, self-correcting, visible), while a timestamp comparison
*suppresses an unanswered ask forever* (silent, permanent) — so anyone tempted to
"fix" a resurfacing item with `created_at` would be trading the safe failure for
the dangerous one.
**Also carried, previously undocumented:** `dbb7879` resynced the vendored **Also carried, previously undocumented:** `dbb7879` resynced the vendored
`mempalace` snapshot to skillset `c04cd15` ("the withdrawal only holds where the `mempalace` snapshot to skillset `c04cd15` ("the withdrawal only holds where the
@@ -275,14 +306,17 @@ the skillset actually is — a maintainer's clone, or any running container.
because when every host is a thin client of one palace the stamped agent name because when every host is a thin client of one palace the stamped agent name
is the only thing distinguishing them. Set both or neither: a container is the only thing distinguishing them. Set both or neither: a container
without the device var can read the log but is addressable by nobody. without the device var can read the log but is addressable by nobody.
- the skillset's `mempalace` skill (`82a8d3c`, live on every host that mounts - the skillset's `mempalace` skill (`6eb20af`, live on every host that mounts
the skillset, no rebuild needed) — the **norms**: a mailbox query at wake-up, the skillset, no rebuild needed) — the **norms**: a mailbox query at wake-up,
and the sender-declared ack contract, where a *directed* event with and the sender-declared ack contract, where a *directed* event with
`status="open"` is owed a reply and a `*` broadcast owes nothing. Measured `status="open"` is owed a reply and a `*` broadcast owes nothing. The
while designing it: an unfiltered mailbox returned 5 events, 4 of them `status` filter earns its place by dropping broadcast noise — measured, an
finished broadcasts from eight days earlier, where `status="open"` returned unfiltered mailbox returned 5 events, 4 of them finished broadcasts from
exactly the 1 that needed an answer — an unfiltered mailbox trains you to eight days earlier — but that is **all** it does; it does not compute
ignore it, so the filter is the feature. owed-ness, and the version of this entry that claimed otherwise is withdrawn
above. Measured today, both machines: the raw filter returns 2 asks here and
1 there, **every one already answered**, while the derivation returns 0 for
both. Dropping noise and deciding what is owed are two different jobs.
- mempalace-toolkit `extensions/pi/README.md` (`e70bef2`) — the **mechanism**, - mempalace-toolkit `extensions/pi/README.md` (`e70bef2`) — the **mechanism**,
including that the bridge is *write-only* today (it stamps events going out including that the bridge is *write-only* today (it stamps events going out
and never reads the log, so nothing in this image polls on the agent's and never reads the log, so nothing in this image polls on the agent's
@@ -290,16 +324,19 @@ the skillset actually is — a maintainer's clone, or any running container.
implements `GET /logstream/stream`, but a reverse proxy exposing only `/mcp` implements `GET /logstream/stream`, but a reverse proxy exposing only `/mcp`
makes it unreachable — verified by 404s against the real endpoint. makes it unreachable — verified by 404s against the real endpoint.
⚠️ **This makes the vendored snapshot stale on purpose.** The skill edit is in ⚠️ **The snapshot was refreshed rather than left stale.** The skill edits landed
the skillset (`82a8d3c`), so `SKILLSET_SNAPSHOT_REF` still records `c04cd15` in the skillset (`5fd0d5c`, then `6eb20af`), so `SKILLSET_SNAPSHOT_REF` was
and `scripts/vendor-mempalace-skill.sh --check` now exits 1 with resynced to match and `scripts/vendor-mempalace-skill.sh --check` is a clean
*"has moved to 82a8d3c; the snapshot describes the older c04cd15"*. Refreshing `OK` with no notice: the no-clone fallback carries the **corrected** protocol,
it is a deliberate release-day decision, not an oversight — hence the new not the withdrawn one. That mattered more than currency usually does, because
step 2 in `AGENTS.md` § *Release-day checklist*, which states both that the the superseded copy contained an instruction — the `mesh_peers` gate — that
refresh costs a base rebuild and that skipping it is legitimate because every actively told a reader to skip the feature. Refreshing remains a deliberate
enrolled host reads its live clone. What is not legitimate is skipping it release-day decision rather than an automatic one: it costs a base rebuild, and
*silently*, which is precisely what the new manifest fields and skipping it is legitimate because every enrolled host reads its live clone.
`pi-devbox-version` output make impossible. What is not legitimate is skipping it *silently*, which is what the new manifest
fields and `pi-devbox-version` output make impossible — hence step 2 in
`AGENTS.md` § *Release-day checklist*. In this release the refresh was free:
`rootfs/` was already changing, so the base rebuild was forced anyway.
--- ---
+1 -1
View File
@@ -305,7 +305,7 @@ ARG MEMPALACE_TOOLKIT_REF=main
# no ~67-minute base rebuild. (scripts/check-base-hash.sh scans only # no ~67-minute base rebuild. (scripts/check-base-hash.sh scans only
# Dockerfile.base, so no folding into the base hash is required — nor would # Dockerfile.base, so no folding into the base hash is required — nor would
# it be correct, since this ARG changes nothing about the base's contents.) # it be correct, since this ARG changes nothing about the base's contents.)
ARG SKILLSET_SNAPSHOT_REF=5fd0d5c406df506fd0e70977b6bd87f5fbc3b803 ARG SKILLSET_SNAPSHOT_REF=6eb20af181f0147cb8c1377f6e36a6a47a68e8e5
# Dockerfile.base sets description="pi-devbox — base image (variant-independent)" # Dockerfile.base sets description="pi-devbox — base image (variant-independent)"
# and every variant INHERITS it, so both published images used to advertise # and every variant INHERITS it, so both published images used to advertise
@@ -462,6 +462,24 @@ every later ask on the same `correlation_id` for good; verified on a live thread
where a `ready` reply at `seq` 16 sits *before* the request at `seq` 17 that it where a `ready` reply at `seq` 16 sits *before* the request at `seq` 17 that it
obviously cannot have answered. obviously cannot have answered.
**On a real mesh, compare `hlc` instead.** `seq` is *replica-local*: it equals
`origin_seq` today only because a single replica authors events for every
machine. Enrol a second replica and a late-syncing peer event gets a late local
`seq`, so two replicas can order the same pair differently and derive different
owed-sets from the same log. Every event already carries `hlc`
(`<millis>-<counter>-<replica_id>`), which is total and causally consistent.
So: compare `seq` while `mempalace_mesh_peers` reports no peers, `hlc` once it
reports any, and `created_at` never. (This is a legitimate use of `mesh_peers` —
choosing an ordering key — not the discredited gate on *whether* to read your
mailbox at all.)
**The failure directions are not symmetric, which is why this is safe to get
slightly wrong.** Local-`seq` skew can make an already-answered item *resurface*
as owed: noise, self-correcting, and visible. A timestamp comparison can
*suppress an unanswered ask forever*: silent and permanent. So if you ever see an
item you know you answered come back, do **not** "fix" it by reaching for
`created_at` — you would be trading the safe failure for the dangerous one.
This also supplies the "taken, not finished" state that looked missing: This also supplies the "taken, not finished" state that looked missing:
`claimed` and `ready` are deliberately **not** terminal, so work you have picked `claimed` and `ready` are deliberately **not** terminal, so work you have picked
up keeps resurfacing until you close it out. No extra convention, no new field. up keeps resurfacing until you close it out. No extra convention, no new field.
+27 -3
View File
@@ -86,7 +86,13 @@ for arg in "$@"; do
case "$arg" in case "$arg" in
--check) MODE="check" ;; --check) MODE="check" ;;
--force) FORCE=1 ;; --force) FORCE=1 ;;
--*) die "unknown option: $arg" ;; # This is the one script whose argument ORDER was itself a landmine, so the
# path that documents the trap must not be the path that errors.
-h|--help)
awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0"
exit 0
;;
--*) die "unknown option: $arg (try --help)" ;;
*) *)
[ -z "$ROOT" ] || die "unexpected extra argument: $arg (root already set to $ROOT)" [ -z "$ROOT" ] || die "unexpected extra argument: $arg (root already set to $ROOT)"
ROOT="$arg" ROOT="$arg"
@@ -110,6 +116,12 @@ fi
[ -d "$ROOT/.git" ] || die "not a git clone: $ROOT" [ -d "$ROOT/.git" ] || die "not a git clone: $ROOT"
[ -f "$ROOT/$REL_PATH" ] || die "no $REL_PATH in $ROOT" [ -f "$ROOT/$REL_PATH" ] || die "no $REL_PATH in $ROOT"
[ -f "$VENDORED" ] || die "vendored snapshot missing: $VENDORED" [ -f "$VENDORED" ] || die "vendored snapshot missing: $VENDORED"
# LOAD-BEARING, DO NOT DELETE AS "REDUNDANT WITH THE EXISTENCE PROBES": -f
# accepts an empty file, and sha256 of an empty file equals sha256 of a failed
# pipeline's empty stdin. Guarding it HERE, before mode dispatch, makes that
# collision unreachable by construction rather than by a probe further down --
# which also means no test below exercises the collision any more. Remove this
# line and the false "OK" for a nonexistent ref returns with nothing failing.
[ -s "$VENDORED" ] || die "vendored snapshot is empty: $VENDORED" [ -s "$VENDORED" ] || die "vendored snapshot is empty: $VENDORED"
head_sha=$(git -C "$ROOT" rev-parse HEAD 2>/dev/null) || die "cannot read HEAD of $ROOT" head_sha=$(git -C "$ROOT" rev-parse HEAD 2>/dev/null) || die "cannot read HEAD of $ROOT"
@@ -200,8 +212,20 @@ if [ "$MODE" = "check" ]; then
# clean and the ref simply moved on — a message that names the wrong cause # clean and the ref simply moved on — a message that names the wrong cause
# is the same defect class as a canary pinned to a deleted phrase. # is the same defect class as a canary pinned to a deleted phrase.
if [ "$recorded" != "$head_sha" ] && [ "$blob_sha" = "$upstream_sha" ]; then if [ "$recorded" != "$head_sha" ] && [ "$blob_sha" = "$upstream_sha" ]; then
printf 'NOTICE: %s has moved to %s; the snapshot describes the older %s (stale, not untruthful)\n' \ # Do not ASSERT which side is newer — test it. Asserting that HEAD is the
"$ROOT" "${head_sha:0:7}" "${recorded:0:7}" >&2 # newer side points the operator at a refresh (which costs a ~67-minute
# base rebuild) when the real remedy may be `git pull` in this clone. The
# refresh path below already uses this primitive; reuse it here.
if git -C "$ROOT" merge-base --is-ancestor "$recorded" "$head_sha" 2>/dev/null; then
printf 'NOTICE: %s has moved on to %s; the snapshot describes the older %s (stale, not untruthful — refresh to catch up)\n' \
"$ROOT" "${head_sha:0:7}" "${recorded:0:7}" >&2
elif git -C "$ROOT" merge-base --is-ancestor "$head_sha" "$recorded" 2>/dev/null; then
printf 'NOTICE: %s is BEHIND at %s; the snapshot describes the newer %s — pull this clone, do NOT refresh the snapshot\n' \
"$ROOT" "${head_sha:0:7}" "${recorded:0:7}" >&2
else
printf 'NOTICE: %s (HEAD %s) and the recorded %s have DIVERGED — neither is an ancestor of the other; reconcile the clone before refreshing\n' \
"$ROOT" "${head_sha:0:7}" "${recorded:0:7}" >&2
fi
else else
printf 'NOTICE: the working tree of %s differs from the snapshot (HEAD %s)\n' \ printf 'NOTICE: the working tree of %s differs from the snapshot (HEAD %s)\n' \
"$ROOT" "${head_sha:0:7}" >&2 "$ROOT" "${head_sha:0:7}" >&2