Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a509077c2 | |||
| a55f6369b3 | |||
| ffd54750b9 | |||
| d8b745c164 | |||
| ae13c2264e | |||
| a2f0a4a441 | |||
| 53b41cd76b |
+12
-3
@@ -57,9 +57,18 @@ SSH_KEY_PATH=~/.ssh
|
||||
# the server to mine its own local copy. Without MEMPALACE_PI_SSH_TARGET the
|
||||
# feeder is skipped (a remote palace with no inbox has nothing to mine).
|
||||
# MEMPALACE_PI_SSH_TARGET where to rsync to, as user@host:path
|
||||
# MEMPALACE_PI_REMOTE_PATH what that inbox is called ON THE SERVER — must be
|
||||
# the container path if the server runs in Docker
|
||||
# (see docker-compose.mempalace.yml)
|
||||
# MEMPALACE_PI_REMOTE_PATH what that inbox is called ON THE SERVER — i.e. the
|
||||
# path the SERVER PROCESS can open. If the palace
|
||||
# server runs in Docker, that is the container path
|
||||
# (see docker-compose.mempalace.yml). If it runs
|
||||
# NATIVELY (systemd unit / uv tool / plain
|
||||
# `mempalace serve`), it sees host paths, so this
|
||||
# must equal the path half of
|
||||
# MEMPALACE_PI_SSH_TARGET. Getting this wrong is
|
||||
# quiet: rsync still succeeds and only the mine
|
||||
# fails with "source directory not found", so
|
||||
# transcripts ship and are filed nowhere. The feeder
|
||||
# warns in preflight when the two paths disagree.
|
||||
# MEMPALACE_PI_DEVICE inbox subdirectory for this machine (default: hostname)
|
||||
# MEMPALACE_PI_SSH_TARGET=user@palace-host:/srv/mempalace-feed
|
||||
# MEMPALACE_PI_REMOTE_PATH=/data/feed
|
||||
|
||||
@@ -18,6 +18,14 @@ name: Publish Docker Image
|
||||
# 5. build-variant multi-arch push of latest + vX.Y.Z tags.
|
||||
# 6. promote-base-latest re-tag base-<hash> → base-latest with `crane copy`.
|
||||
# 7. update-description patch Docker Hub description.
|
||||
#
|
||||
# Note the trigger: `push: tags: v*` (plus workflow_dispatch). Nothing here runs
|
||||
# on a push to main, so a smoke assertion added outside a release is UNVALIDATED
|
||||
# until the next tag — which is exactly how v1.8.0 shipped a broken assertion
|
||||
# written three days earlier (it asserted a literal /home/developer stage path,
|
||||
# while `run` executes `docker run --entrypoint=""` as root with HOME=/root).
|
||||
# The `smoke_only` dispatch input exists to close that gap: it runs steps 1-4
|
||||
# against HEAD and stops before anything is published.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -33,6 +41,10 @@ on:
|
||||
description: 'Update latest aliases (default true for tag-push, false for manual test runs)'
|
||||
required: false
|
||||
default: 'false'
|
||||
smoke_only:
|
||||
description: 'Build base + run both smoke jobs against HEAD, then stop. Publishes nothing. Use to validate smoke assertions without cutting a tag.'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -165,6 +177,40 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
# Read a commit SHA from Gitea, surviving a bad build token.
|
||||
#
|
||||
# These repos are public (see the note at the call sites), so auth is
|
||||
# a convenience, not a requirement — but Gitea REJECTS an invalid
|
||||
# token (401) rather than ignoring it, so a revoked or malformed
|
||||
# GITEA_BUILD_TOKEN could fail an entire release on reads that work
|
||||
# fine anonymously. An ABSENT secret was always safe (Gitea ignores an
|
||||
# empty `token ` value and serves the request, 200); a STALE one was
|
||||
# not. So: try authed, and on 401/403 retry anonymously.
|
||||
#
|
||||
# A non-200 after that emits nothing and returns 0 deliberately, so
|
||||
# require_sha raises the loud explicit abort rather than this helper
|
||||
# inventing a fallback ref.
|
||||
#
|
||||
# Messages go to STDERR, not as ::warning:: annotations: this
|
||||
# function's stdout IS the SHA, so anything written there would be
|
||||
# captured into the ref by the command substitution.
|
||||
gitea_sha() { # $1=repo
|
||||
local repo="$1" url resp code
|
||||
url="https://gitea.jordbo.se/api/v1/repos/joakimp/${repo}/commits?limit=1&sha=main"
|
||||
resp=$(curl -s -w '\n%{http_code}' -H "$AUTH_HEADER" "$url" || printf '\n000')
|
||||
code=${resp##*$'\n'}
|
||||
if [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
||||
printf 'WARNING: Gitea rejected the build token for %s (HTTP %s); retrying anonymously. The read should succeed (public repo), but GITEA_BUILD_TOKEN is stale or malformed and should be rotated.\n' "$repo" "$code" >&2
|
||||
resp=$(curl -s -w '\n%{http_code}' "$url" || printf '\n000')
|
||||
code=${resp##*$'\n'}
|
||||
fi
|
||||
if [ "$code" != "200" ]; then
|
||||
printf 'WARNING: Gitea commit lookup for %s returned HTTP %s\n' "$repo" "$code" >&2
|
||||
return 0
|
||||
fi
|
||||
printf '%s' "${resp%$'\n'*}" | jq -r '.[0].sha // empty' 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── pi version: from the PIN, not from npm `latest` ───────────
|
||||
# Until v1.7.0 this followed npm `latest`, which meant every release
|
||||
# silently adopted whatever pi had shipped that morning — unaudited —
|
||||
@@ -226,15 +272,27 @@ jobs:
|
||||
echo "atelier_ref=${ATELIER_REF}" >> "$GITHUB_OUTPUT"
|
||||
echo "atelier_tag=${ATELIER_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. Gitea API
|
||||
# requires auth even for public-repo commit listing.
|
||||
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
|
||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. All three Gitea
|
||||
# repos read in this step are PUBLIC: an unauthenticated GET of these
|
||||
# commit endpoints returns 200 with the IDENTICAL sha (verified
|
||||
# 2026-08-15 for pi-toolkit, pi-extensions and mempalace-toolkit).
|
||||
# The comment that used to sit here claimed the Gitea API "requires
|
||||
# auth even for public-repo commit listing" — it does not. Only
|
||||
# /api/v1/repos/*/actions/* refuses anonymous reads (401), which is
|
||||
# what that claim was almost certainly generalised from.
|
||||
#
|
||||
# The header is still passed on purpose: it keeps working if a repo is
|
||||
# ever flipped private, and an ABSENT secret degrades cleanly, because
|
||||
# Gitea ignores an empty `token ` value and serves the request
|
||||
# anonymously (200). The real hazard is the opposite one — a REVOKED or
|
||||
# malformed token returns 401 where anonymous would have returned 200,
|
||||
# so a stale GITEA_BUILD_TOKEN turns a healthy public read into a
|
||||
# require_sha failure that reads like an API or network fault. If this
|
||||
# step ever fails on a repo you can browse anonymously, suspect the
|
||||
# token before you suspect Gitea.
|
||||
TOOLKIT_REF=$(gitea_sha pi-toolkit)
|
||||
require_sha PI_TOOLKIT_REF "$TOOLKIT_REF"
|
||||
EXTENSIONS_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
|
||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||
EXTENSIONS_REF=$(gitea_sha pi-extensions)
|
||||
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
|
||||
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
||||
@@ -244,9 +302,7 @@ jobs:
|
||||
# into the base-decide hash (see that job) to force a base rebuild
|
||||
# when the toolkit moves — otherwise a toolkit-only fix silently
|
||||
# fails to land unless Dockerfile.base itself changes.
|
||||
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
|
||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||
MEMPALACE_TOOLKIT_REF=$(gitea_sha mempalace-toolkit)
|
||||
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
|
||||
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -482,6 +538,14 @@ jobs:
|
||||
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
||||
build-variant:
|
||||
needs: [base-decide, smoke, resolve-versions]
|
||||
# A `smoke_only` dispatch stops the pipeline here: base is probed/built and
|
||||
# both smoke jobs run, but nothing is published. Deliberately NOT wrapped in
|
||||
# always() — specifying `if:` keeps the implicit "all needs succeeded" gate,
|
||||
# so a failing smoke still blocks the release. On a tag push `inputs` is
|
||||
# unset, and `null != 'true'` is true, so releases are unaffected.
|
||||
# promote-base-latest and update-description need build-variant to have
|
||||
# succeeded, so they skip on their own — no extra guard required.
|
||||
if: inputs.smoke_only != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
@@ -574,6 +638,7 @@ jobs:
|
||||
# or fail independently of the core release.
|
||||
build-variant-studio:
|
||||
needs: [base-decide, smoke-studio, resolve-versions]
|
||||
if: inputs.smoke_only != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
|
||||
+215
@@ -11,6 +11,221 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
---
|
||||
|
||||
## v1.8.3 — 2026-08-16
|
||||
|
||||
Patch release. **Bumps mempalace to `3.7.1`** and closes the gap that made the
|
||||
baked mempalace skill go stale for four commits. `pi` stays `0.84.2` (still the
|
||||
npm latest) and pi-atelier stays `v0.8.1`; every git-ref component
|
||||
(pi-toolkit, pi-extensions, pi-fork, pi-observational-memory, pi-studio,
|
||||
mempalace-toolkit) was checked against its upstream head and is unchanged.
|
||||
|
||||
- **`MEMPALACE_VERSION` `3.6.0` → `3.7.1`.** Verified against the 3.7.1 source
|
||||
rather than its changelog, because the risk is to palaces users cannot
|
||||
reconstruct: legacy drawers lack the new `chunk_total` completion marker and
|
||||
**both** decision sites trust them (`if chunk_total is None: ... trust the
|
||||
match as before`), so there is **no mass re-mine**; `NORMALIZE_VERSION` is `2`
|
||||
in both versions, so the "pre-v2 drawers are stale" gate does not fire either;
|
||||
`chromadb<2,>=1.5.4` keeps the same major, so no index-format migration; there
|
||||
is no auto-migration (the source says *"We do NOT auto-migrate"* twice) and
|
||||
`rebuild_index` has exactly one call site, the explicit `repair rebuild`; the
|
||||
single new palace file (`logstream.sqlite3`) is created lazily on first
|
||||
logstream use. Downgrade stays possible — 3.6.0 has zero references to
|
||||
`chunk_total` and ignores it as unknown metadata.
|
||||
|
||||
Two behaviour changes worth knowing, both turning a silent condition into a
|
||||
hard refusal: `MEMPALACE_MCP_ALLOW_PEER_WRITER` **no longer works on
|
||||
local/chroma palaces** (it is now gated on `backend_requires_single_writer()`,
|
||||
and `_MULTI_PROCESS_WRITER_BACKENDS` is `{pgvector, qdrant}`), and writer-lock
|
||||
*setup* failures now **fail closed** (`refusing this mutating tool`) instead of
|
||||
proceeding with a warning. Neither affects this image's normal
|
||||
MCP-server-plus-CLI-feeder pattern, which already serialised on the same
|
||||
`mine_palace_*.lock` under 3.6.0 — "process-lifetime single-writer ownership"
|
||||
in the upstream changelog describes tightened escape hatches, not a new lease.
|
||||
|
||||
What 3.7.1 buys a **shared central** palace is the real motivation: the stale
|
||||
chromadb `SharedSystemClient` cache is now dropped on reconnect (under 3.6.0 a
|
||||
peer's writes could be overwritten by a stale in-memory HNSW segment, *"index
|
||||
count going backwards"*), the writer lease is released on SIGTERM/SIGHUP
|
||||
instead of leaking a lock naming a dead PID, and an interrupted mine is no
|
||||
longer permanently skipped as though complete.
|
||||
|
||||
**Upgrading a server requires restarting it** — 3.7.1 refuses mutating tools
|
||||
when the served library drifts from what is installed, and `mempalace_reconnect`
|
||||
cannot clear that (it reopens the database but cannot reload Python modules).
|
||||
The fleet primary was upgraded and restarted before this image was tagged.
|
||||
|
||||
Note: opencode-devbox still pins `3.6.0`. The two images are meant to move in
|
||||
lockstep, so that pin diverges until opencode-devbox cuts its own release.
|
||||
|
||||
- **Vendored `mempalace` skill snapshot refreshed** to skillset `936fed8` (was
|
||||
`63f3bf5`). This is the gap worth naming: `~/.agents/skills/mempalace`
|
||||
symlinks to the **image-baked** copy under
|
||||
`/usr/local/share/pi-devbox/skills/`, and `entrypoint-user.sh` creates that
|
||||
link *first* while the skillset deploy never clobbers an existing name — so in
|
||||
a devbox container the vendored snapshot always wins, and editing the skillset
|
||||
repo alone changes nothing a container reads. Two commits' worth of guidance
|
||||
had been invisible here: the multi-machine shared-palace section (device
|
||||
provenance in `source_path`, mined drawers carrying the *mine* date with
|
||||
UUIDv7 recovery, the naive-local vs UTC timestamp mismatch, `agent_name` not
|
||||
being device-scoped, single-writer/no-queue semantics) and the
|
||||
hand-crafted-provenance guard.
|
||||
|
||||
- **`pi-global-AGENTS.append.md`** gains `### If the palace is central, it is
|
||||
shared — three rules`: never run `mempalace sync` against a shared palace (it
|
||||
prunes drawers whose sources look missing, which on a central palace is most
|
||||
of the content, including other machines' — compounded by RFC-001 §7.2, since
|
||||
feeders stage *inside* the palace root); a client-side timeout is not a
|
||||
failure (single writer, one large mine blocks everyone, so
|
||||
`mine timed out after 30000ms` usually means the mine completed — verify
|
||||
before retrying or you file a duplicate); and the `mempalace` CLI is not
|
||||
remote-aware, so it always opens a local-disk palace and can silently
|
||||
disagree with the MCP tools.
|
||||
|
||||
- **`mempalace-census` is now on `PATH`.** It shipped inside the image at
|
||||
`/opt/mempalace-toolkit/bin/` but was never symlinked into `/usr/local/bin`
|
||||
like its three siblings, so RFC-002 Phase A censuses had to be invoked by
|
||||
absolute path. Added to the symlink set, the `chmod +x` set, and the
|
||||
build-time `--help` smoke chain.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.2 — 2026-08-16
|
||||
|
||||
Patch release. **Ships the fix for a silent transcript-feed failure**, plus the
|
||||
smoke assertion that stops it coming back. No image pins changed from v1.8.1
|
||||
(pi `0.84.2`, pi-atelier `v0.8.1`); what moves is the baked `mempalace-toolkit`
|
||||
ref and one new smoke check.
|
||||
|
||||
**The bug this closes** (found on the first boot of the v1.8.1 image, on
|
||||
EMB-7KJ4VR4G, 2026-08-15): the container-start catch-up rsynced seven pi session
|
||||
transcripts to the palace host correctly, then asked the server to mine
|
||||
`/data/feed/<device>` — the feeder's default `MEMPALACE_PI_REMOTE_PATH`, which
|
||||
assumes a *containerized* palace server. That fleet's primary runs **natively**
|
||||
(a systemd user unit + uv tool), so it only ever sees host paths and the mine
|
||||
died with `source directory not found`. rsync had already succeeded, so the
|
||||
inbox looked healthy.
|
||||
|
||||
It stayed invisible because of the second half: the feeder decided success with
|
||||
`'"error"' in body`. MCP answers a hard tool failure with HTTP 200 and a
|
||||
JSON-RPC *result* whose `content[].text` carries the tool's own JSON as an
|
||||
**escaped string** — the bytes are `\"error\"`, so the substring could never
|
||||
match. `~/.pi/agent/mempalace-catchup.log` printed
|
||||
`Done. Wing 'wing_conversations' updated.` directly beneath the error JSON and
|
||||
exited 0. A feeder whose only artifact claims success is worse than one that
|
||||
crashes: nothing in the container disagreed with it.
|
||||
|
||||
Shipped here:
|
||||
|
||||
- **`mempalace-toolkit` ≥ `b609cf5`** baked (CI resolves the ref at build time):
|
||||
`classify()` parses the MCP envelope instead of grepping it (JSON-RPC error,
|
||||
MCP `isError`, inner `success=false`/`error`), and separates "verified ok"
|
||||
from "unverified: no JSON tool payload" rather than assuming the good case.
|
||||
A preflight warning fires when the rsync destination and
|
||||
`MEMPALACE_PI_REMOTE_PATH` disagree — in *preflight*, so `--dry-run` and
|
||||
`--prepare` surface it too. Remote mode also stops previewing NEW/SKIP from
|
||||
the *local* palace, which had been reporting "6 already filed" about a palace
|
||||
it was not feeding; the tags are now `[?]` and the summary names who decides.
|
||||
- **New smoke assertion** — `mempalace-pi-session --self-test` run against the
|
||||
**baked** toolkit. It replays six recorded MCP responses (fixture 1 is the
|
||||
verbatim 2026-08-15 failure body) plus a regression guard asserting the old
|
||||
substring check is blind to it. A stale or reverted `MEMPALACE_TOOLKIT_REF`
|
||||
can therefore no longer ship a feeder that mines nothing while reporting
|
||||
success.
|
||||
- **`.env.example`** now spells out that `MEMPALACE_PI_REMOTE_PATH` is the path
|
||||
the *server process* can open — the container path for a dockerized server,
|
||||
identical to the ssh-target path for a native one — and that a mismatch fails
|
||||
quietly, with rsync succeeding and only the mine failing.
|
||||
|
||||
**The `--self-test` assertion is deliberately bare** (`mempalace-pi-session
|
||||
--self-test`, no `HOME=…` prefix). `run()` invokes
|
||||
`docker run --entrypoint="" $IMAGE sh -c …` and no Dockerfile sets `USER` or
|
||||
`ENV HOME`, so it executes with **no `HOME` at all** — the same condition that
|
||||
made v1.8.0's stage assertion unsatisfiable. The feeder is `set -u` with
|
||||
HOME-anchored defaults, so it used to die with `HOME: unbound variable` there;
|
||||
`b609cf5` derives `HOME` from the passwd database (what python's `expanduser()`
|
||||
falls back to) instead. Keeping the call bare means smoke also proves the feeder
|
||||
runs in a bare container, rather than papering over it with an env prefix.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.1 — 2026-08-15
|
||||
|
||||
Patch release. **Unblocks v1.8.0, which never shipped.** Its `smoke` and
|
||||
`smoke-studio` jobs each failed exactly one assertion (67/68 and 70/71 passed),
|
||||
so `build-variant` and everything downstream skipped: no `v1.8.0` tag reached
|
||||
Docker Hub and `latest` stayed on v1.7.0 from 2026-08-07. Image content is
|
||||
unchanged from what v1.8.0 intended — the pins here are identical (pi `0.84.2`,
|
||||
pi-atelier `v0.8.1`).
|
||||
|
||||
The failing assertion was `pi stage defaults next to the palace (not a cache
|
||||
dir)`, added three days earlier in 7c00dd6. **It was a test bug, not a product
|
||||
regression.** It asserted a literal path:
|
||||
|
||||
```sh
|
||||
echo "$out" | grep -q "stage=/home/developer/.mempalace/pi-stage/"
|
||||
```
|
||||
|
||||
but the `run` helper invokes `docker run --rm --entrypoint="" $IMAGE sh -c …`,
|
||||
and neither `Dockerfile.base` nor `Dockerfile.variant` sets `USER` or `ENV HOME`
|
||||
(the published base image config carries no `HOME` at all — `HOME` is normally
|
||||
set by `entrypoint-user.sh`, which `--entrypoint=""` deliberately skips). So the
|
||||
assertion ran as **root with `HOME=/root`**, `mempalace-pi-session` correctly
|
||||
resolved `stage=/root/.mempalace/pi-stage/…` (it is `$HOME`-relative by design:
|
||||
`$MEMPALACE_PALACE_PATH` → `$MEMPAL_PALACE_PATH` → `~/.mempalace/config.json` →
|
||||
`~/.mempalace/palace`), and the literal grep could never match under any
|
||||
circumstances. The tell was one line below it in the log: the sibling assertion
|
||||
`pi stage follows MEMPALACE_PALACE_PATH` **passed**, because it sets the variable
|
||||
explicitly and so never consults `HOME`. Default fails while explicit passes is
|
||||
the signature of a wrong `HOME`, not of broken staging.
|
||||
|
||||
Fixed by asserting the invariant that was actually meant — the stage sits beside
|
||||
the resolved palace, sharing its lifetime — which is user-independent:
|
||||
|
||||
```sh
|
||||
case "$stage" in
|
||||
"stage=$HOME/.mempalace/pi-stage/"*) exit 0 ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
`$HOME` is expanded by the container's own shell, so this holds as root, as
|
||||
`developer`, or under any future user, while a cache-dir default — the
|
||||
regression the assertion exists to catch — still fails it (verified against all
|
||||
three cases plus a simulated `MEMPALACE_PI_STAGE` cache pin). A second
|
||||
assertion, `pi stage is palace-adjacent for the developer user`, now covers the
|
||||
deployment-specific path properly, by *supplying* `HOME=/home/developer` instead
|
||||
of assuming it.
|
||||
|
||||
### Why it took a release to notice — and the `smoke_only` input
|
||||
|
||||
`docker-publish.yml` triggers on `push: tags: v*` only. 7c00dd6 was a push to
|
||||
**main**, so only `lint.yml` ran; v1.8.0 was the first tag afterwards and
|
||||
therefore the assertion's **first execution ever**. Any smoke assertion written
|
||||
outside a release was unvalidated until the next release consumed it — the
|
||||
worst possible moment to discover it.
|
||||
|
||||
New `workflow_dispatch` input **`smoke_only`** closes that: it probes/builds the
|
||||
base and runs both smoke jobs against HEAD, then stops before publishing
|
||||
anything. Implemented as `if: inputs.smoke_only != 'true'` on `build-variant`
|
||||
and `build-variant-studio`, deliberately *without* `always()` so the implicit
|
||||
"needs succeeded" gate survives and a red smoke still blocks a release;
|
||||
`promote-base-latest` and `update-description` already require `build-variant`
|
||||
success and so skip on their own. On a tag push `inputs` is unset and
|
||||
`null != 'true'` is true, so releases behave exactly as before. This release was
|
||||
validated with a `smoke_only` dispatch before the tag was cut.
|
||||
|
||||
### Smoke failures now explain themselves
|
||||
|
||||
`run` discarded all output (`>/dev/null 2>&1`), so a red ❌ carried zero
|
||||
diagnostic weight — explaining this one-line failure took a CI-log dig plus a
|
||||
registry image-config inspection, when the container had already printed the
|
||||
answer and thrown it away. It now captures output and prints the last few lines
|
||||
under a failed assertion only. Assertions that want a diagnostic echo it to
|
||||
stderr (the stage checks now report the resolved stage and the `HOME` they saw),
|
||||
which stays invisible while they pass.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.0 — 2026-08-15
|
||||
|
||||
Minor release. Headline: **pi sessions now feed MemPalace by themselves.** The
|
||||
|
||||
+17
-2
@@ -384,7 +384,19 @@ ARG INSTALL_MEMPALACE=true
|
||||
# mempalace_checkpoint (#2023/#2034).
|
||||
#
|
||||
# Keep in lockstep with opencode-devbox when bumping.
|
||||
ARG MEMPALACE_VERSION=3.6.0
|
||||
#
|
||||
# 3.7.1 (from 3.6.0) is safe for anyone with an EXISTING LOCAL palace: verified
|
||||
# against the 3.7.1 source, not the changelog. Legacy drawers lack the new
|
||||
# `chunk_total` marker and both decision sites trust them ("trust the match as
|
||||
# before"), NORMALIZE_VERSION is 2 in both, chromadb stays <2 (no index-format
|
||||
# migration), there is no auto-migration ("We do NOT auto-migrate"), and the one
|
||||
# new palace file (logstream.sqlite3) is created lazily on first logstream use.
|
||||
# Two behaviour changes to know: MEMPALACE_MCP_ALLOW_PEER_WRITER no longer works
|
||||
# on local/chroma palaces, and writer-lock setup failures now fail CLOSED
|
||||
# (refuse the write) rather than fail open. Neither affects the container's
|
||||
# normal MCP-server-plus-CLI-feeder pattern, which already serialised on the
|
||||
# same lock under 3.6.0.
|
||||
ARG MEMPALACE_VERSION=3.7.1
|
||||
ENV UV_TOOL_DIR=/opt/uv-tools
|
||||
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
||||
@@ -425,11 +437,14 @@ RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" =
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-session /usr/local/bin/mempalace-session && \
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-docs /usr/local/bin/mempalace-docs && \
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-pi-session /usr/local/bin/mempalace-pi-session && \
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-census /usr/local/bin/mempalace-census && \
|
||||
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs \
|
||||
/opt/mempalace-toolkit/bin/mempalace-pi-session && \
|
||||
/opt/mempalace-toolkit/bin/mempalace-pi-session \
|
||||
/opt/mempalace-toolkit/bin/mempalace-census && \
|
||||
mempalace-session --help >/dev/null && \
|
||||
mempalace-docs --help >/dev/null && \
|
||||
mempalace-pi-session --help >/dev/null && \
|
||||
mempalace-census --help >/dev/null && \
|
||||
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
|
||||
fi
|
||||
|
||||
|
||||
@@ -41,3 +41,32 @@ especially load-bearing here — a pi-devbox container is frequently recreated,
|
||||
the palace is your only memory across recreates. Without the habit it is just
|
||||
storage, not memory. (The skill is the consumer side; feeding the palace is the
|
||||
separate `opencode-mempalace-bridge` skill, if present.)
|
||||
|
||||
### If the palace is central, it is shared — three rules
|
||||
|
||||
If `MEMPALACE_REMOTE_URL` is set, the MCP tools write to a **central palace
|
||||
shared with other machines**, not to a local one. Your drawers are not the only
|
||||
ones in there, and most drawers' `source_file` paths do not exist on this host.
|
||||
The skill covers the orientation side (provenance, chronology, whose diary is
|
||||
whose); these three are here instead because getting them wrong does *damage*
|
||||
rather than merely confusing you:
|
||||
|
||||
- **Never run `mempalace sync` / `mempalace_sync` against a shared palace.** It
|
||||
prunes drawers whose source files look gitignored, deleted, or moved — and on
|
||||
a shared palace that describes most of the content, including every other
|
||||
machine's. Compounding it (RFC-001 §7.2): feeders now stage *inside* the
|
||||
palace root, so a scoped sync can delete the very drawers it just filed.
|
||||
`mempalace_delete_by_source` is exact-match rather than existence-based, but
|
||||
its blast radius is now the whole fleet's palace — leave it on its default
|
||||
`dry_run=true` and confirm the match count before committing.
|
||||
- **A timeout is not a failure.** The palace is single-writer, and one large
|
||||
mine can block every client for minutes, so a write or mine that exceeds the
|
||||
client's deadline has usually *completed* server-side. Verify with
|
||||
`mempalace_get_drawer` or `mempalace_search` before retrying — a blind retry
|
||||
files a duplicate. `[mempalace ext] feed (tick) failed: mine timed out after
|
||||
30000ms` is the common benign instance: the transcript is already in the
|
||||
server's inbox and the mine is idempotent, so nothing is lost either way.
|
||||
- **The `mempalace` CLI is not remote-aware.** It always opens a palace on
|
||||
local disk, so `mempalace search` can return older and different results than
|
||||
the MCP tools while both look correct. Use the MCP tools for the central
|
||||
palace; the CLI only for a local one.
|
||||
|
||||
@@ -50,4 +50,4 @@ also carries a copy, but it is a downstream duplicate and can lag), and
|
||||
`mempalace` from `skillset`. Copying `pi-extensions` from `skillset` would
|
||||
regress the snapshot to whatever that repo last mirrored.
|
||||
|
||||
Snapshot provenance at last refresh: skillset `63f3bf5`, pi-extensions pkg `e73cb9f`.
|
||||
Snapshot provenance at last refresh: skillset `936fed8`, pi-extensions pkg `e73cb9f`.
|
||||
|
||||
@@ -275,18 +275,29 @@ Wings are top-level categories, typically one per project or domain:
|
||||
- Named after the project directory (e.g., `cli_utils`, `opencode_devbox`)
|
||||
- Agent diaries live in `wing_<agent_name>` (e.g., `wing_orchestrator`, `wing_pi`)
|
||||
|
||||
#### Multi-harness palace
|
||||
#### Shared palace: multiple harnesses, and possibly multiple machines
|
||||
|
||||
A single palace can be fed by multiple coding-agent harnesses. On this machine the palace is shared between **opencode** and **pi** (Mario Zechner's pi-coding-agent). Implications:
|
||||
A single palace can be fed by multiple coding-agent harnesses, and — when
|
||||
`MEMPALACE_REMOTE_URL` points at a central palace — by multiple *machines*. On
|
||||
this machine the palace is shared between **opencode** and **pi** (Mario
|
||||
Zechner's pi-coding-agent). Implications:
|
||||
|
||||
- **`wing_conversations` mixes sources.** Both harnesses' session feeders write into the same wing. To tell them apart, look at the `source_file` metadata on each drawer:
|
||||
- `pi_<uuid>.jsonl` → pi session
|
||||
- `<slug>_ses_<id>.jsonl` → opencode session
|
||||
- The first chunk of each session also carries a `| source: opencode` or `| source: pi` marker in the synthetic header line.
|
||||
- **Other wings may belong to other harnesses.** For example `wing_pi` is pi's diary, not opencode's. Don't assume every diary entry was written by you — check `agent_name` on the entry.
|
||||
- **Session feeders run on different schedules.** Pi sessions are fed Tue 03:00, opencode sessions Mon 03:00. Recent sessions from either harness can lag the palace by up to a week, so absence-of-evidence in `wing_conversations` is not evidence-of-absence for recent work.
|
||||
- **Session feeders run on different schedules.** Pi sessions are fed Tue 03:00, opencode sessions Mon 03:00 (launchd `Weekday`: `0`/`7`=Sunday, `1`=Monday, `2`=Tuesday — misreading this by one day is easy). Recent sessions from either harness can lag the palace by up to a week, so absence-of-evidence in `wing_conversations` is not evidence-of-absence for recent work.
|
||||
- **Reading another harness's diary is useful.** When orienting after a gap, `mempalace_diary_read agent_name=pi` (or whichever sibling agent has been active) often gives a fresher picture than waiting for the conversations feeder to catch up.
|
||||
|
||||
When the palace is **central** (shared across machines), five more things apply:
|
||||
|
||||
- **Check which machine a conversation came from.** Transcripts are fed per device, so `source_path` reads `…/mempalace-feed/<device>/pi_<uuid>.jsonl` while the displayed `source_file` is only the basename. One search can legitimately return hits from several machines at once — look at the device segment before attributing a decision to *this* project.
|
||||
- **Mined drawers carry the MINE date, not the session date.** When history is imported, or re-mined on the palace host, `filed_at`/`created_at` is the *import* time — so sorting by them does not give chronological order. Real session time is recoverable from the UUIDv7 in `pi_<uuid>.jsonl`: the first 12 hex digits are milliseconds since the epoch (and UUIDv7 sorts lexicographically in time order, so a plain filename sort is already chronological). Agent-authored drawers and diaries have no such backdoor — for those `filed_at` is the only chronology, which is why it must never be restamped.
|
||||
- **Beware the timezone mismatch when you combine those.** Palace `filed_at`/`created_at` are naive timestamps in the palace host's local time, while a UUIDv7 decodes to UTC. Comparing them directly introduces a silent offset (2 h for a CEST host). Normalise before drawing conclusions about ordering.
|
||||
- **`agent_name` is not device-scoped.** `mempalace_diary_read(agent_name="pi")` returns *every* machine's `pi` diary, interleaved. Read the entry before assuming it is your own history.
|
||||
- **One writer, no queue.** A concurrent mine returns a structured `already-running` error rather than waiting its turn, and one large mine can make the palace unresponsive to every client for minutes. After another client's mine, call `mempalace_reconnect` to see the new drawers. A client-side timeout is not evidence of failure — verify before retrying, or you file a duplicate.
|
||||
|
||||
### Rooms
|
||||
|
||||
Rooms are aspects within a wing:
|
||||
@@ -324,3 +335,4 @@ Entity-relationship triples with temporal validity. Query with `mempalace_kg_que
|
||||
- **Don't mine .git directories or node_modules.** The CLI miner respects .gitignore by default.
|
||||
- **Don't create duplicate drawers.** Use `mempalace_check_duplicate` before adding manually.
|
||||
- **Don't treat the palace as a task list.** It's for knowledge and context, not todos.
|
||||
- **Don't hand-craft provenance.** Leave `added_by` alone (and never put a machine name in a diary's `agent_name` — it becomes the wing name and hides your entries from `diary_read`). Recording *which device* wrote a record is client/server infrastructure, not your job: a hostname or container ID is not a stable identity, and an invented value is worse than none because it silently corrupts any future palace merge. If you find notes in the palace describing an `origin_device` scheme, that is a design for the client to implement — not an instruction for you to start stamping.
|
||||
|
||||
+59
-1
@@ -43,12 +43,23 @@ PASS=0; FAIL=0
|
||||
# catching an unexpected +GB regression.
|
||||
SIZE_THRESHOLD_MB=3800
|
||||
|
||||
# On failure, surface the last few lines the command produced. This used to
|
||||
# discard output entirely (`>/dev/null 2>&1`), which made a red ❌ carry zero
|
||||
# diagnostic weight: explaining the single v1.8.0 stage-default failure took a
|
||||
# full CI-log dig plus a registry-config inspection, when the container had
|
||||
# already printed the answer and thrown it away. Assertions that want a
|
||||
# diagnostic just echo it to stderr — it stays hidden while they pass.
|
||||
run() {
|
||||
local label="$1"; local cmd="$2"
|
||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" >/dev/null 2>&1; then
|
||||
local out
|
||||
if out=$(docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" 2>&1); then
|
||||
printf " ✅ %s\n" "$label"; PASS=$((PASS+1))
|
||||
else
|
||||
printf " ❌ %s\n" "$label"; FAIL=$((FAIL+1))
|
||||
# `if`, not `&&` — a trailing false under `set -e` would abort the script.
|
||||
if [ -n "$out" ]; then
|
||||
printf " └─ %s\n" "$(printf '%s' "$out" | tail -3 | tr '\n' ' ' | cut -c1-300)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -102,8 +113,37 @@ run "mempalace-pi-session on PATH" "mempalace-pi-session --help"
|
||||
# default-staged run at a populated dir would export whatever transcripts it
|
||||
# finds into the real stage, which is how a synthetic test session ends up
|
||||
# staged for mining as if it were a real conversation.
|
||||
#
|
||||
# Asserted $HOME-RELATIVE, not against a literal /home/developer. `run` invokes
|
||||
# `docker run --entrypoint=""`, and neither Dockerfile sets USER or ENV HOME
|
||||
# (HOME is set by entrypoint-user.sh, which --entrypoint="" deliberately skips),
|
||||
# so these assertions execute as root with HOME=/root. The original literal
|
||||
# /home/developer form could therefore never match and failed the v1.8.0
|
||||
# release — a test bug, not a product one: the stage resolution was correct all
|
||||
# along, it just follows $HOME. The invariant under test ("the stage sits beside
|
||||
# the palace, sharing its lifetime") is user-independent, so pinning the user
|
||||
# was never part of it. A cache-dir default still fails the pattern below, which
|
||||
# is the regression this guards.
|
||||
#
|
||||
# It went unnoticed for three days because this workflow only triggers on
|
||||
# `push: tags: v*` — the assertion was added on a main push, so v1.8.0 was its
|
||||
# first execution ever. Use the `smoke_only` workflow_dispatch input to run
|
||||
# smoke against HEAD without cutting a tag.
|
||||
run "pi stage defaults next to the palace (not a cache dir)" '
|
||||
out=$(mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
||||
stage=$(echo "$out" | grep -oE "stage=[^ ]+" | head -1)
|
||||
echo "resolved ${stage:-<no stage= line>} with HOME=$HOME" >&2
|
||||
case "$stage" in
|
||||
"stage=$HOME/.mempalace/pi-stage/"*) exit 0 ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
'
|
||||
# Companion to the above: the deployment-specific case the literal assertion was
|
||||
# reaching for, done properly by supplying the HOME the container actually runs
|
||||
# with instead of assuming it.
|
||||
run "pi stage is palace-adjacent for the developer user" '
|
||||
out=$(HOME=/home/developer mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
||||
echo "$out" | grep -oE "stage=[^ ]+" | head -1 >&2
|
||||
echo "$out" | grep -q "stage=/home/developer/.mempalace/pi-stage/"
|
||||
'
|
||||
run "pi stage follows MEMPALACE_PALACE_PATH" '
|
||||
@@ -155,6 +195,16 @@ run_expect "remote-palace-without-inbox skip is announced, not silent" \
|
||||
"MemPalace catch-up skipped"
|
||||
run "...and the skip notice names the variable that fixes it" \
|
||||
"grep -A6 'MemPalace catch-up skipped' /usr/local/bin/entrypoint-user.sh | grep -q 'MEMPALACE_PI_SSH_TARGET'"
|
||||
# A remote mine that FAILS must not report success. MCP answers a hard tool
|
||||
# failure with HTTP 200 and the tool's own JSON escaped inside
|
||||
# result.content[].text, so the feeder's old `'\"error\"' in body` check could
|
||||
# never see it: on 2026-08-15 a mine that died with "source directory not found:
|
||||
# '/data/feed/...'" logged "Done. Wing updated." and exited 0, and this
|
||||
# container's transcripts were filed nowhere for a whole session. The feeder
|
||||
# carries fixtures for that exact body; run them against the baked toolkit so a
|
||||
# stale/reverted toolkit ref can't reintroduce a silent feed.
|
||||
run "baked feeder detects a failed remote mine (no silent false success)" \
|
||||
"mempalace-pi-session --self-test"
|
||||
# v1.0.0 base additions — verify presence and basic functionality.
|
||||
run "pandoc" "pandoc --version"
|
||||
run "typst" "typst --version"
|
||||
@@ -318,6 +368,14 @@ exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/sett
|
||||
exec_test "pi-devbox-environment skill linked" 'test -L $HOME/.agents/skills/pi-devbox-environment && test -f $HOME/.agents/skills/pi-devbox-environment/SKILL.md && echo ok'
|
||||
exec_test "pi-extensions skill linked (fallback)" 'test -L $HOME/.agents/skills/pi-extensions && test -f $HOME/.agents/skills/pi-extensions/SKILL.md && echo ok'
|
||||
exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills/mempalace && test -f $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
||||
# The vendored mempalace snapshot is refreshed MANUALLY per release (see
|
||||
# rootfs/usr/local/share/pi-devbox/skills/VENDORED.md). It silently shadows the
|
||||
# skillset copy in a devbox container, so a stale snapshot is invisible: assert
|
||||
# the multi-machine shared-palace guidance is actually present, not just the file.
|
||||
exec_test "mempalace skill snapshot is current" 'grep -q "Shared palace: multiple harnesses" $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
||||
# mempalace-census gained a /usr/local/bin symlink in v1.8.3; its three siblings
|
||||
# had one since they were added, so this asserts the set stays complete.
|
||||
exec_test "mempalace-census on PATH" 'command -v mempalace-census >/dev/null && mempalace-census --help >/dev/null && echo ok'
|
||||
|
||||
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
||||
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
||||
|
||||
Reference in New Issue
Block a user