release: v1.8.9 — the version flag that blamed the wrong component
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 18s
Publish Docker Image / resolve-versions (push) Successful in 9s
Publish Docker Image / base-decide (push) Successful in 9s
Publish Docker Image / build-base (push) Successful in 41m49s
Publish Docker Image / smoke (push) Successful in 4m51s
Publish Docker Image / smoke-studio (push) Successful in 4m59s
Publish Docker Image / build-variant-studio (push) Successful in 16m58s
Publish Docker Image / build-variant (push) Successful in 28m35s
Publish Docker Image / update-description (push) Successful in 7s
Publish Docker Image / promote-base-latest (push) Successful in 17s

Two versions, two flags. `--expected-version` has only ever asserted
`pi --version`, but AGENTS.md step 4 spelled it `X.Y.Z` inside a checklist where
every other X.Y.Z is the pi-devbox tag. Run as documented for v1.8.8 the final
runtime gate of the release printed

    ✗ pi version mismatch: expected 1.8.8, got 0.84.3

and exited 1 — a red accusing the image of being the wrong version. Not one
reader's slip: the v1.8.8 release-readiness handoff from pi@emb-7kj4vr4g
propagated the same wrong spelling twice while correctly calling step 4 "not
ceremonial", so two independent readers converged on it. README.md had it right
all along, which means the two documents disagreed.

- new --expected-image-version asserts the pi-devbox release tag, read from
  release_tag in /etc/pi-devbox/build-manifest.json (no checkout, no network);
  leading `v` optional on either side
- both flags detect being handed the other one's value, and the test is exact
  rather than heuristic: the value is compared against the other quantity the
  image itself reports, so it can only fire on a real mix-up
- neither flag is required now. With none, live `pi --version` is asserted
  against the manifest's pi_version — not a tautology, since a stale pi in the
  ~/.pi/npm-global volume can shadow the baked one, exactly as a stale
  npm:pi-atelier can in packages[]
- the header note replaced was stale and load-bearing: it claimed pi is resolved
  from 'latest' and cannot be self-derived, while Dockerfile.variant pins
  ARG PI_VERSION=0.84.3 and docker-publish.yml reads that ARG as its source of
  truth. The same withdrawn claim also sat in cli_utils' pi-devbox-sanity --help
- argument parsing: a missing value, or a value that is another flag, is a usage
  error instead of silently consuming the next argument; --help works

All fourteen flag combinations exercised by execution, including the two
manifest-absent branches and the shadowing branch a healthy container cannot
reach — mutation-tested with a doctored manifest so each failure branch was
observed firing rather than assumed present.

CHANGELOG also names what no commit here causes: mempalace-toolkit main moved
e70bef2 -> 5b8d78f, so this tag ships the auto-delivered logstream mailbox
because base_tag folds the resolved toolkit SHA. It would have landed either
way; going unnamed is the 553d865 shape that already caused one cross-host
misattribution. Component audit found nothing else to bump — pi, mempalace,
pi-atelier all equal their upstream latest, and every other floating ref
resolves to the commit already baked.
This commit is contained in:
2026-08-26 18:47:03 +02:00
parent 34cf1e3810
commit aac4a1c323
4 changed files with 296 additions and 20 deletions
+17 -3
View File
@@ -98,9 +98,23 @@ re-brand of opencode-devbox's `pi-only` variant.
**post-recreate sanity check** inside the running container to confirm
persisted volumes survived and the pi runtime wiring re-deployed (not just
that the container booted):
`docker compose exec devbox bash scripts/recreate-sanity-check.sh --expected-version X.Y.Z`
(or just `pi-devbox-sanity --expected-version X.Y.Z` if `cli_utils/bin` is
on PATH). This is the runtime peer of the build-time `smoke-test.sh` gate.
`docker compose exec devbox bash scripts/recreate-sanity-check.sh --expected-image-version X.Y.Z`
(or just `pi-devbox-sanity --expected-image-version X.Y.Z` if
`cli_utils/bin` is on PATH). This is the runtime peer of the build-time
`smoke-test.sh` gate.
**`X.Y.Z` here is the pi-devbox release tag** you are shipping (e.g.
`1.8.9`), which is what the rest of this checklist means by `vX.Y.Z`.
`--expected-image-version` is the flag that asserts it. There is also an
`--expected-version`, and it means something else — the **pi coding agent**
version (e.g. `0.84.3`, the `ARG PI_VERSION` pin). Handing the release tag
to that one used to report *"pi version mismatch: expected 1.8.8, got
0.84.3"*, i.e. a red on the final gate of the release accusing the wrong
component; it now tells you to use `--expected-image-version` instead, and
the reverse mix-up is caught too. Both flags are optional — with neither,
the live pi version is asserted against the version recorded in the image's
own build manifest (which catches a stale `pi` in the `~/.pi/npm-global`
volume shadowing the baked one) and the image tag is reported
informationally.
5. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
6. Watch CI: smoke job builds amd64 only and asserts size + extensions +
pi version + new-base-tooling presence. Variant build is multi-arch
+141
View File
@@ -11,6 +11,147 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## v1.8.9 — 2026-08-26
The coordination log gets a reader, and the release checklist's last gate stops
accusing the wrong component.
### The mailbox arrives — named here *because nothing in this repo caused it*
**`mempalace-toolkit` main moves `e70bef2` → `5b8d78f` (exactly one commit, 281
insertions / 9 deletions across `extensions/pi/mempalace.ts` and
`extensions/pi/README.md`), and that is what actually ships the auto-delivered
logstream mailbox.** No pi-devbox commit implements it. `docker-publish.yml`
resolves `MEMPALACE_TOOLKIT_REF=main` to a concrete SHA at build time and folds
that SHA into `base_tag`, so the mailbox would have landed in the next tagged
image **whether or not this section existed** — which is precisely why it exists.
That is the same shipped-undocumented shape as `553d865` in v1.8.7, and that one
caused a cross-host misattribution: an agent on another machine reasoned about
which image contained which behaviour from a CHANGELOG that never mentioned it.
The rule this release adopts: **if a floating ref will pull a behaviour change
into the image, name it in the CHANGELOG before tagging, not after.**
What the mailbox does, from the shipped code rather than from the design
discussion:
- **The bridge was write-only.** It stamped provenance on the way *out* and never
read the log back, so a directed ask reached an agent only if that agent
happened to run `mempalace_event_list` itself. The channel carried real
cross-machine traffic from 2026-08-18 onward with **zero readers** — every
delivery in that window happened because a human said "check your mailbox".
- **Doubly gated, exactly like the provenance stamper:** inert unless *both*
`MEMPALACE_PI_DEVICE` and `MEMPALACE_REMOTE_URL` are set. An unstamped client
has no address to be reached at, so there is nothing for it to read.
- **On by default, opt out with `MEMPALACE_MAILBOX=0`.** Deliberate: an opt-in
fix for a nobody-remembers-to-do-it problem only relocates the forgetting.
Tunables: `MEMPALACE_MAILBOX_POLL_MS` (min gap between mid-session polls,
default 300000) and `MEMPALACE_MAILBOX_RESURFACE_MS` (re-announce a still-owed
ask after, default 3600000).
- **Owed-ness is derived, never read off `status`.** `event_ack` appends and never
mutates, and `status` is written once, so a directed `open` keeps matching the
mailbox query forever — answered or not. A candidate counts as answered only
when one of this device's own events has a **higher `seq`**, joins via
`metadata.ack_of` or a shared `correlation_id`, and carries a terminal status
(`applied`, `superseded`, `failed`, `blocked`). `claimed` and `ready` are
deliberately **not** terminal — that is how "taken, but not finished" keeps
resurfacing.
- **`*` broadcasts are excluded from the owed set.** `to_agent: <me>` also matches
broadcasts per the tool contract, so without this a broadcast written with
`status="open"` would make every machine believe it personally owed the same
answer — and the code would contradict the skill that documents it.
- **The dedup map is in memory on purpose.** A restart forgets, so an already-seen
ask can resurface: visible noise a human corrects in one turn. The opposite
failure — suppressing an unanswered ask — is silent and permanent. Do not
"fix" the noise by persisting it.
- **Delivery queues, it never interrupts.** A sections push at
`before_agent_start` plus a second `agent_settled` handler behind the 300 s
floor, using `steer` and *not* `triggerTurn`: `agent_settled` means idle, so
nothing wakes a model on inbound fleet traffic.
Measured on v1.8.8 (which bakes `e70bef2`, i.e. no mailbox) immediately before
this release: the wake-up mailbox query had to be run by hand, returned **3**
directed asks with `status="open"`, and the derivation above resolved **all
three** as already answered — the third independent confirmation that the raw
`status` filter never shrinks, and the first taken on a fresh container with no
memory of having answered them.
### `--expected-image-version`: two versions, two flags
**`scripts/recreate-sanity-check.sh --expected-version 1.8.8` reported
`✗ pi version mismatch: expected 1.8.8, got 0.84.3` and exit 1** — a red on the
final runtime gate of a release, accusing the image of being the wrong version,
when the flag had only ever asserted `pi --version`. `AGENTS.md` step 4 spelled
it `--expected-version X.Y.Z` inside a checklist where every *other* `X.Y.Z` is
the pi-devbox tag; `README.md` got it right, so the two documents disagreed.
Not hypothetical, and not one reader's slip: the v1.8.8 release-readiness handoff
from `pi@emb-7kj4vr4g` (`evt_20260826T134919_a614ecfc2d4f`) propagated
`--expected-version 1.8.8` twice, in its body and in
`metadata.cannot_check_here`, while correctly calling step 4 "the runtime peer of
the smoke gate, so it is not ceremonial". Two independent readers, one on another
machine, converged on the wrong meaning. Left alone it puts a spurious red on
every release, and the intuitive remedy — re-pull, re-recreate — is pure waste.
- **New `--expected-image-version X.Y.Z`** asserts the pi-devbox release tag,
read from `release_tag` in `/etc/pi-devbox/build-manifest.json` (the image's
own build-time ground truth — no checkout, no network, no Docker socket). A
leading `v` is optional on either side, so `1.8.9` and `v1.8.9` both work.
- **Both flags now detect being handed the other one's value**, and the test is
exact rather than heuristic: the value is compared against the *other*
quantity this image actually reports, so it can only fire when the mix-up is
real. `--expected-version 1.8.9` now says *"is the pi-devbox IMAGE version,
not the pi version — use `--expected-image-version`"*, and the reverse mix-up
is caught the same way.
- **Neither flag is required any more.** With none, the live `pi --version` is
asserted against `pi_version` in the build manifest. That is not a tautology:
`pi` resolves through `PATH`, and a stale install in the `~/.pi/npm-global`
volume can shadow the baked one — the same shadowing this script already
guards against for `npm:pi-atelier` in `packages[]`. Verified by mutating the
manifest to a different version, which made the new check fail as intended.
- **The header note it replaced was stale and load-bearing.** It claimed pi "is
resolved from `latest` at CI build time and is NOT pinned … cannot self-derive
an expected version". `Dockerfile.variant` pins `ARG PI_VERSION=0.84.3`, and
`docker-publish.yml` *reads that ARG* as its source of truth (refusing to
build on a floating value, checking it is published on npm, warning when npm
is ahead). The same withdrawn claim also sat in `cli_utils`'s
`pi-devbox-sanity --help`, the third place this confusion lived; fixed there
too, in that repo.
- Argument parsing hardened while in there: a flag whose value is missing — or
is another flag — is now a usage error (exit 2) instead of silently consuming
the next argument, and `--help` works.
All fourteen flag combinations were exercised by execution, including the two
manifest-absent branches and the shadowing branch, which a healthy container
cannot reach naturally — mutation-tested with a doctored manifest path so that
each failure branch was observed *firing* rather than assumed present.
### Component audit: no bumps, and that is the finding
Checked before tagging, since a base rebuild was already forced:
| Component | In v1.8.8 | Upstream now | Action |
|---|---|---|---|
| pi (npm) | `0.84.3` (pinned) | `0.84.3` is `latest` | none |
| mempalace (PyPI) | `3.8.0` (pinned) | `3.8.0` | none |
| pi-atelier | `v0.8.2` (pinned) | `v0.8.2` highest tag | none |
| pi-toolkit, pi-extensions, pi-fork, pi-observational-memory, pi-studio | floating | **identical to baked** | none |
| skillset snapshot | `6eb20af` | `6eb20af` | none |
| **mempalace-toolkit** | `e70bef2` | **`5b8d78f`** | ships the mailbox |
So the whole ~67-minute base rebuild this tag pays for is attributable to the
toolkit SHA alone — `base_tag` folds it, and it moved. Every other floating ref
resolved to the commit already baked (verified with `git ls-remote` per repo, not
by reading a cached clone).
One claim in this audit came from a fork that had fabricated its findings — six
plausible-looking toolkit commits with five nonexistent SHAs, a pi `0.84.4` that
npm has never published, a pi-studio commit `ls-remote` says does not exist, and
a compatibility floor of `0.8.2` where the code says `0.7.1`. Every row above was
therefore re-measured directly. Recorded because the failure mode is specific:
none of it looked wrong, and `git cat-file -e` is what caught it.
---
## v1.8.8 — 2026-08-26
The vendored `mempalace` skill snapshot stops being anonymous, and the
+13 -2
View File
@@ -1017,10 +1017,21 @@ After `docker compose up -d --force-recreate`, run the **runtime** peer of
persisted volumes survived, and pi runtime wiring is intact:
```bash
./scripts/recreate-sanity-check.sh # auto-detects variant
./scripts/recreate-sanity-check.sh --expected-version 0.79.4 # assert pi version
./scripts/recreate-sanity-check.sh # auto-detects variant
./scripts/recreate-sanity-check.sh --expected-image-version 1.8.9 # assert the pi-devbox release tag
./scripts/recreate-sanity-check.sh --expected-version 0.84.3 # assert the pi coding agent version
```
Those are **two different versions**, and the flags are not interchangeable:
`--expected-image-version` takes the pi-devbox release tag (`v` optional),
`--expected-version` takes `pi --version`. Hand one the other's value and it
says so by name instead of reporting a mismatch against the wrong component.
With neither flag, both values are read from the image's own build manifest
(`/etc/pi-devbox/build-manifest.json`): the live pi version is asserted against
the one recorded at build time — which catches a stale `pi` in the
`~/.pi/npm-global` volume shadowing the baked one — and the release tag is
reported informationally.
If `cli_utils` is on your PATH, the `pi-devbox-sanity` wrapper runs the same
check by short name and locates the repo automatically (override with
`PI_DEVBOX_REPO=/path/to/pi-devbox`). Like `smoke-test.sh`, this script is
+125 -15
View File
@@ -2,8 +2,8 @@
# Runtime post-recreate verification for pi-devbox.
#
# Verifies that after `docker compose up -d --force-recreate`:
# - The new image is actually live (pi version matches, when an expected
# version is supplied — see the version note below)
# - The new image is actually live (both the pi version and — when asked —
# the pi-devbox image release tag; see the two version notes below)
# - Persisted named volumes survived (~/.pi config, shell history, zoxide,
# nvim data, uv cache, ssh-local)
# - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink,
@@ -25,13 +25,33 @@
# the pi-devbox repo (which a maintainer already has for CI builds). A plain
# `docker pull` consumer is not the audience and will not have this file.
#
# Version note: pi's version is resolved from `latest` at CI build time and is
# NOT pinned to a concrete value in Dockerfile.variant (ARG PI_VERSION=latest).
# So unlike opencode-devbox, this script cannot self-derive an expected version
# from the Dockerfile. Pass --expected-version to assert a match; without it the
# live pi version is reported as an informational WARN, not a failure.
# TWO DIFFERENT VERSIONS, TWO DIFFERENT FLAGS. This distinction has already
# cost a release day, so it is spelled out here and in AGENTS.md step 4:
#
# Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z] [--variant studio|plain]
# --expected-version the PI CODING AGENT version, e.g. 0.84.3
# (`pi --version`; pinned as ARG PI_VERSION in
# Dockerfile.variant, which CI reads as the source
# of truth)
# --expected-image-version the PI-DEVBOX IMAGE release tag, e.g. 1.8.9 or
# v1.8.9 (the `release_tag` baked into
# /etc/pi-devbox/build-manifest.json)
#
# Passing a release tag to --expected-version used to report
# "pi version mismatch: expected 1.8.8, got 0.84.3" — an accusation aimed at
# the wrong component, on the last gate of a release. Both flags now detect
# being handed the other one's value and say so instead.
#
# Neither flag is required. Both values are derivable from the image's own
# build manifest, so by default the script asserts the LIVE pi version against
# the version recorded at build time — which is not a tautology: a stale
# `pi` in the ~/.pi/npm-global volume can shadow the baked one, exactly the
# way a stale npm:pi-atelier can (see the packages[] check below). Pass the
# flags when you want an assertion against a value you name yourself, which
# is what a release checklist wants.
#
# Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z]
# [--expected-image-version X.Y.Z]
# [--variant studio|plain]
#
# Exit codes:
# 0 all checks passed
@@ -41,22 +61,61 @@
set -euo pipefail
EXPECTED_VERSION=""
EXPECTED_IMAGE_VERSION=""
VARIANT=""
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST=/etc/pi-devbox/build-manifest.json
# Parse arguments
usage() {
cat >&2 <<'EOF'
usage: recreate-sanity-check.sh [--expected-version X.Y.Z]
[--expected-image-version X.Y.Z]
[--variant studio|plain]
--expected-version pi coding agent version, e.g. 0.84.3 (`pi --version`)
--expected-image-version pi-devbox image release tag, e.g. 1.8.9 or v1.8.9
--variant studio|plain (auto-detected when omitted)
These are two different versions. Both are read from the image's own build
manifest when the corresponding flag is omitted.
EOF
}
# Parse arguments. Every flag takes a value, so reject a missing one rather
# than swallowing the next flag as if it were the value.
need_value() {
case "${2:-}" in
""|-*)
echo "$1 requires a value" >&2
usage
exit 2
;;
esac
}
while [[ $# -gt 0 ]]; do
case "$1" in
--expected-version)
need_value "$@"
EXPECTED_VERSION="$2"
shift 2
;;
--expected-image-version)
need_value "$@"
EXPECTED_IMAGE_VERSION="$2"
shift 2
;;
--variant)
need_value "$@"
VARIANT="$2"
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
echo "usage: $0 [--expected-version X.Y.Z] [--variant studio|plain]" >&2
echo "unknown option: $1" >&2
usage
exit 2
;;
esac
@@ -67,6 +126,19 @@ pass() { echo " ✓ $1"; }
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
warn() { echo " ⚠ $1" >&2; }
# Read one top-level field from the build manifest, or print nothing. The
# manifest is the image's own ground truth (written at `docker build` time by
# Dockerfile.variant), so it needs no checkout and no network. Absent on an
# image built before it existed, hence every caller treats "" as unknown.
manifest_field() {
[ -f "$MANIFEST" ] || return 0
command -v jq >/dev/null 2>&1 || return 0
jq -r --arg k "$1" '.[$k] // empty' "$MANIFEST" 2>/dev/null || true
}
# Release tags are written with a leading v in the manifest and quoted without
# one in checklists; compare on the bare number so both spellings work.
strip_v() { printf '%s' "${1#v}"; }
# Auto-detect variant if not provided. The studio variant vendors pi-studio to
# /opt/pi-studio; the plain variant does not.
if [ -z "$VARIANT" ]; then
@@ -86,21 +158,59 @@ else
fi
echo
echo "-- pi version --"
MANIFEST_PI_VERSION=$(manifest_field pi_version)
MANIFEST_RELEASE_TAG=$(manifest_field release_tag)
echo "-- pi (coding agent) version --"
if ACTUAL_VERSION=$(pi --version 2>&1 | head -1); then
if [ -n "$EXPECTED_VERSION" ]; then
if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then
pass "pi version $ACTUAL_VERSION"
if [ "$(strip_v "$EXPECTED_VERSION")" = "$(strip_v "$ACTUAL_VERSION")" ]; then
pass "pi version $ACTUAL_VERSION (matches --expected-version)"
elif [ -n "$MANIFEST_RELEASE_TAG" ] &&
[ "$(strip_v "$EXPECTED_VERSION")" = "$(strip_v "$MANIFEST_RELEASE_TAG")" ]; then
# Exact, not heuristic: the value handed over IS this image's release
# tag, so it cannot be a pi version anyone meant.
fail "--expected-version $EXPECTED_VERSION is the pi-devbox IMAGE version, not the pi version — use --expected-image-version $EXPECTED_VERSION (live pi is $ACTUAL_VERSION)"
else
fail "pi version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION"
fail "pi version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION (this flag asserts the pi coding agent version; for the image release tag use --expected-image-version)"
fi
elif [ -n "$MANIFEST_PI_VERSION" ]; then
# Not a tautology: the manifest records what pi reported at BUILD time,
# while `pi --version` resolves through PATH, which a stale npm-global
# volume install can shadow.
if [ "$MANIFEST_PI_VERSION" = "$ACTUAL_VERSION" ]; then
pass "pi version $ACTUAL_VERSION (matches this image's build manifest)"
else
fail "live pi $ACTUAL_VERSION != $MANIFEST_PI_VERSION recorded in $MANIFEST — a stale pi in the ~/.pi/npm-global volume is shadowing the baked one"
fi
else
warn "pi version $ACTUAL_VERSION (no --expected-version given; pi is built from 'latest', cannot self-derive — informational only)"
warn "pi version $ACTUAL_VERSION (no --expected-version and no build manifest to compare against — informational only)"
fi
else
fail "pi --version failed"
fi
echo
echo "-- pi-devbox image version --"
if [ -z "$MANIFEST_RELEASE_TAG" ]; then
if [ -n "$EXPECTED_IMAGE_VERSION" ]; then
fail "cannot verify --expected-image-version $EXPECTED_IMAGE_VERSION: no readable release_tag in $MANIFEST (image built before the manifest existed, or jq missing)"
else
warn "image release tag unknown (no readable $MANIFEST) — pi-devbox-version would say the same"
fi
elif [ -n "$EXPECTED_IMAGE_VERSION" ]; then
if [ "$(strip_v "$EXPECTED_IMAGE_VERSION")" = "$(strip_v "$MANIFEST_RELEASE_TAG")" ]; then
pass "image version $MANIFEST_RELEASE_TAG (matches --expected-image-version)"
elif [ -n "$MANIFEST_PI_VERSION" ] &&
[ "$(strip_v "$EXPECTED_IMAGE_VERSION")" = "$MANIFEST_PI_VERSION" ]; then
fail "--expected-image-version $EXPECTED_IMAGE_VERSION is the pi version, not the image release tag — use --expected-version $EXPECTED_IMAGE_VERSION (this image is $MANIFEST_RELEASE_TAG)"
else
fail "image version mismatch: expected $EXPECTED_IMAGE_VERSION, got $MANIFEST_RELEASE_TAG — the recreate did not pick up the intended image"
fi
else
warn "image version $MANIFEST_RELEASE_TAG (no --expected-image-version given — informational only)"
fi
echo
echo "-- Persisted named volumes (must survive --force-recreate) --"