Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aac4a1c323 | |||
| 34cf1e3810 |
@@ -98,9 +98,23 @@ re-brand of opencode-devbox's `pi-only` variant.
|
|||||||
**post-recreate sanity check** inside the running container to confirm
|
**post-recreate sanity check** inside the running container to confirm
|
||||||
persisted volumes survived and the pi runtime wiring re-deployed (not just
|
persisted volumes survived and the pi runtime wiring re-deployed (not just
|
||||||
that the container booted):
|
that the container booted):
|
||||||
`docker compose exec devbox bash scripts/recreate-sanity-check.sh --expected-version X.Y.Z`
|
`docker compose exec devbox bash scripts/recreate-sanity-check.sh --expected-image-version X.Y.Z`
|
||||||
(or just `pi-devbox-sanity --expected-version X.Y.Z` if `cli_utils/bin` is
|
(or just `pi-devbox-sanity --expected-image-version X.Y.Z` if
|
||||||
on PATH). This is the runtime peer of the build-time `smoke-test.sh` gate.
|
`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`.
|
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 +
|
6. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
||||||
pi version + new-base-tooling presence. Variant build is multi-arch
|
pi version + new-base-tooling presence. Variant build is multi-arch
|
||||||
|
|||||||
+197
-19
@@ -11,7 +11,148 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Unreleased
|
## 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
|
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 +186,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 +246,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 +447,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 +465,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
@@ -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
|
||||||
|
|||||||
@@ -1018,9 +1018,20 @@ persisted volumes survived, and pi runtime wiring is intact:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/recreate-sanity-check.sh # auto-detects variant
|
./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 --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
|
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
|
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
|
`PI_DEVBOX_REPO=/path/to/pi-devbox`). Like `smoke-test.sh`, this script is
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# Runtime post-recreate verification for pi-devbox.
|
# Runtime post-recreate verification for pi-devbox.
|
||||||
#
|
#
|
||||||
# Verifies that after `docker compose up -d --force-recreate`:
|
# Verifies that after `docker compose up -d --force-recreate`:
|
||||||
# - The new image is actually live (pi version matches, when an expected
|
# - The new image is actually live (both the pi version and — when asked —
|
||||||
# version is supplied — see the version note below)
|
# the pi-devbox image release tag; see the two version notes below)
|
||||||
# - Persisted named volumes survived (~/.pi config, shell history, zoxide,
|
# - Persisted named volumes survived (~/.pi config, shell history, zoxide,
|
||||||
# nvim data, uv cache, ssh-local)
|
# nvim data, uv cache, ssh-local)
|
||||||
# - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink,
|
# - 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
|
# 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.
|
# `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
|
# TWO DIFFERENT VERSIONS, TWO DIFFERENT FLAGS. This distinction has already
|
||||||
# NOT pinned to a concrete value in Dockerfile.variant (ARG PI_VERSION=latest).
|
# cost a release day, so it is spelled out here and in AGENTS.md step 4:
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# 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:
|
# Exit codes:
|
||||||
# 0 all checks passed
|
# 0 all checks passed
|
||||||
@@ -41,22 +61,61 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
EXPECTED_VERSION=""
|
EXPECTED_VERSION=""
|
||||||
|
EXPECTED_IMAGE_VERSION=""
|
||||||
VARIANT=""
|
VARIANT=""
|
||||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
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
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--expected-version)
|
--expected-version)
|
||||||
|
need_value "$@"
|
||||||
EXPECTED_VERSION="$2"
|
EXPECTED_VERSION="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--expected-image-version)
|
||||||
|
need_value "$@"
|
||||||
|
EXPECTED_IMAGE_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--variant)
|
--variant)
|
||||||
|
need_value "$@"
|
||||||
VARIANT="$2"
|
VARIANT="$2"
|
||||||
shift 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
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -67,6 +126,19 @@ pass() { echo " ✓ $1"; }
|
|||||||
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
|
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
|
||||||
warn() { echo " ⚠ $1" >&2; }
|
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
|
# Auto-detect variant if not provided. The studio variant vendors pi-studio to
|
||||||
# /opt/pi-studio; the plain variant does not.
|
# /opt/pi-studio; the plain variant does not.
|
||||||
if [ -z "$VARIANT" ]; then
|
if [ -z "$VARIANT" ]; then
|
||||||
@@ -86,21 +158,59 @@ else
|
|||||||
fi
|
fi
|
||||||
echo
|
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 ACTUAL_VERSION=$(pi --version 2>&1 | head -1); then
|
||||||
if [ -n "$EXPECTED_VERSION" ]; then
|
if [ -n "$EXPECTED_VERSION" ]; then
|
||||||
if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then
|
if [ "$(strip_v "$EXPECTED_VERSION")" = "$(strip_v "$ACTUAL_VERSION")" ]; then
|
||||||
pass "pi version $ACTUAL_VERSION"
|
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
|
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
|
fi
|
||||||
else
|
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
|
fi
|
||||||
else
|
else
|
||||||
fail "pi --version failed"
|
fail "pi --version failed"
|
||||||
fi
|
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
|
||||||
echo "-- Persisted named volumes (must survive --force-recreate) --"
|
echo "-- Persisted named volumes (must survive --force-recreate) --"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
# 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
|
"$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
|
||||||
|
|||||||
Reference in New Issue
Block a user