Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a55f6369b3 | |||
| ffd54750b9 | |||
| d8b745c164 | |||
| ae13c2264e | |||
| a2f0a4a441 |
+12
-3
@@ -57,9 +57,18 @@ SSH_KEY_PATH=~/.ssh
|
|||||||
# the server to mine its own local copy. Without MEMPALACE_PI_SSH_TARGET the
|
# 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).
|
# 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_SSH_TARGET where to rsync to, as user@host:path
|
||||||
# MEMPALACE_PI_REMOTE_PATH what that inbox is called ON THE SERVER — must be
|
# MEMPALACE_PI_REMOTE_PATH what that inbox is called ON THE SERVER — i.e. the
|
||||||
# the container path if the server runs in Docker
|
# path the SERVER PROCESS can open. If the palace
|
||||||
# (see docker-compose.mempalace.yml)
|
# 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_DEVICE inbox subdirectory for this machine (default: hostname)
|
||||||
# MEMPALACE_PI_SSH_TARGET=user@palace-host:/srv/mempalace-feed
|
# MEMPALACE_PI_SSH_TARGET=user@palace-host:/srv/mempalace-feed
|
||||||
# MEMPALACE_PI_REMOTE_PATH=/data/feed
|
# MEMPALACE_PI_REMOTE_PATH=/data/feed
|
||||||
|
|||||||
@@ -177,6 +177,40 @@ jobs:
|
|||||||
fi
|
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` ───────────
|
# ── pi version: from the PIN, not from npm `latest` ───────────
|
||||||
# Until v1.7.0 this followed npm `latest`, which meant every release
|
# Until v1.7.0 this followed npm `latest`, which meant every release
|
||||||
# silently adopted whatever pi had shipped that morning — unaudited —
|
# silently adopted whatever pi had shipped that morning — unaudited —
|
||||||
@@ -238,15 +272,27 @@ jobs:
|
|||||||
echo "atelier_ref=${ATELIER_REF}" >> "$GITHUB_OUTPUT"
|
echo "atelier_ref=${ATELIER_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "atelier_tag=${ATELIER_TAG}" >> "$GITHUB_OUTPUT"
|
echo "atelier_tag=${ATELIER_TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. Gitea API
|
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. All three Gitea
|
||||||
# requires auth even for public-repo commit listing.
|
# repos read in this step are PUBLIC: an unauthenticated GET of these
|
||||||
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
# commit endpoints returns 200 with the IDENTICAL sha (verified
|
||||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
|
# 2026-08-15 for pi-toolkit, pi-extensions and mempalace-toolkit).
|
||||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
# 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"
|
require_sha PI_TOOLKIT_REF "$TOOLKIT_REF"
|
||||||
EXTENSIONS_REF=$(curl -sf -H "$AUTH_HEADER" \
|
EXTENSIONS_REF=$(gitea_sha pi-extensions)
|
||||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
|
|
||||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
|
||||||
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
|
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
|
||||||
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
||||||
@@ -256,9 +302,7 @@ jobs:
|
|||||||
# into the base-decide hash (see that job) to force a base rebuild
|
# into the base-decide hash (see that job) to force a base rebuild
|
||||||
# when the toolkit moves — otherwise a toolkit-only fix silently
|
# when the toolkit moves — otherwise a toolkit-only fix silently
|
||||||
# fails to land unless Dockerfile.base itself changes.
|
# fails to land unless Dockerfile.base itself changes.
|
||||||
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
MEMPALACE_TOOLKIT_REF=$(gitea_sha mempalace-toolkit)
|
||||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
|
|
||||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
|
||||||
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
|
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
|
||||||
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,65 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## v1.8.1 — 2026-08-15
|
||||||
|
|
||||||
Patch release. **Unblocks v1.8.0, which never shipped.** Its `smoke` and
|
Patch release. **Unblocks v1.8.0, which never shipped.** Its `smoke` and
|
||||||
|
|||||||
@@ -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
|
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
|
storage, not memory. (The skill is the consumer side; feeding the palace is the
|
||||||
separate `opencode-mempalace-bridge` skill, if present.)
|
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.
|
||||||
|
|||||||
@@ -195,6 +195,16 @@ run_expect "remote-palace-without-inbox skip is announced, not silent" \
|
|||||||
"MemPalace catch-up skipped"
|
"MemPalace catch-up skipped"
|
||||||
run "...and the skip notice names the variable that fixes it" \
|
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'"
|
"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.
|
# v1.0.0 base additions — verify presence and basic functionality.
|
||||||
run "pandoc" "pandoc --version"
|
run "pandoc" "pandoc --version"
|
||||||
run "typst" "typst --version"
|
run "typst" "typst --version"
|
||||||
|
|||||||
Reference in New Issue
Block a user