Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01abda3456 | |||
| b5810654f6 | |||
| 4f6f470518 | |||
| fbc1f86612 | |||
| 2ebf00d6d4 | |||
| c3b6d36778 | |||
| 3a509077c2 | |||
| a55f6369b3 |
@@ -76,10 +76,15 @@ re-brand of opencode-devbox's `pi-only` variant.
|
|||||||
4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
||||||
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
5. 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
|
||||||
(amd64 + arm64) only after smoke passes. **A tag push produces two runs, not
|
(amd64 + arm64) only after smoke passes. A tag push fires **only**
|
||||||
one** — `lint.yml` fires on every push (including tag refs) and
|
`docker-publish.yml` — `lint.yml` is scoped to `branches: ['**']`, which
|
||||||
`docker-publish.yml` fires on `v*` tags. Watch the **publish** run; see
|
excludes tag refs on purpose (the tagged tree was already linted when the
|
||||||
*Gitea API access* below for how to find it without picking lint by mistake.
|
commit hit `main`, and a fast lint run sorting above the slow publish run
|
||||||
|
made releases look finished before anything shipped). Verified on v1.8.4:
|
||||||
|
`refs/tags/v1.8.4` produced run 571 (publish) and nothing else. Still filter
|
||||||
|
discovery on `head_sha` **and** the workflow `path` — see *Gitea API access*
|
||||||
|
below — because that guard costs nothing and a future workflow added on `v*`
|
||||||
|
would silently reintroduce the ambiguity.
|
||||||
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
||||||
base-latest if the base was rebuilt this run).
|
base-latest if the base was rebuilt this run).
|
||||||
7. **Revoke any short-lived Gitea PAT** used during the release at
|
7. **Revoke any short-lived Gitea PAT** used during the release at
|
||||||
@@ -87,6 +92,34 @@ re-brand of opencode-devbox's `pi-only` variant.
|
|||||||
`GITEA_ACCESS_TOKEN` env var instead (see *Gitea API access* below) —
|
`GITEA_ACCESS_TOKEN` env var instead (see *Gitea API access* below) —
|
||||||
its lifecycle is managed host-side, nothing to revoke.
|
its lifecycle is managed host-side, nothing to revoke.
|
||||||
|
|
||||||
|
## Verifying this repo's reality from inside a container
|
||||||
|
|
||||||
|
Most work on this repo happens **inside** a pi-devbox container, inspecting a
|
||||||
|
host or a peer over SSH. That setup manufactures convincing false negatives, so
|
||||||
|
when you are about to report that something is **absent, unreachable, or not
|
||||||
|
running**, suspect your own command first. Recurring instances:
|
||||||
|
|
||||||
|
- **`docker` is not on the host's non-interactive SSH `PATH`.** `ssh mac 'docker
|
||||||
|
ps'` says *command not found* on a host that plainly runs Docker; use
|
||||||
|
`/usr/local/bin/docker` (or `command -v docker` first). Every step in the
|
||||||
|
*Release-day checklist* that inspects a running container hits this.
|
||||||
|
- **Don't `| head -N` a search whose answer you don't already know.** The host's
|
||||||
|
`~/.ssh/config` is ~500 lines; a `head -20` "proved" a peer absent that was
|
||||||
|
defined at line 454.
|
||||||
|
- **The deployment compose file is not this repo's.** `docker-compose.yml` here
|
||||||
|
is a template pinning `:latest`; a real host runs its own per-machine file
|
||||||
|
(find it with `docker inspect <container> --format '{{ index .Config.Labels
|
||||||
|
"com.docker.compose.project.config_files" }}'`). Recreating from the repo copy
|
||||||
|
can silently move a host off `:latest-studio` onto `:latest`.
|
||||||
|
- **A live SSH ControlMaster hides remote auth changes** — after editing a
|
||||||
|
peer's `authorized_keys`, prove access with `-o ControlPath=none -o
|
||||||
|
ControlMaster=no`, or the breakage surfaces in a later session instead.
|
||||||
|
|
||||||
|
Depth and further mechanisms: the repo-authored `pi-devbox-environment` skill
|
||||||
|
(`rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md`) §2
|
||||||
|
and §3 — that file is the one an agent actually loads mid-session, whereas this
|
||||||
|
`AGENTS.md` is only auto-read when the cwd *is* this repo.
|
||||||
|
|
||||||
## Gitea API access (env token)
|
## Gitea API access (env token)
|
||||||
|
|
||||||
`GITEA_ACCESS_TOKEN` + `GITEA_HOST` are passed into the container from the
|
`GITEA_ACCESS_TOKEN` + `GITEA_HOST` are passed into the container from the
|
||||||
|
|||||||
+370
@@ -11,6 +11,376 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.8.5 — 2026-08-23
|
||||||
|
|
||||||
|
Patch release with two fixes in the container's skill wiring — one behavioural,
|
||||||
|
one a latent crash found while reviewing the first — plus the `mempalace-toolkit`
|
||||||
|
change that makes palace writes carry provenance. **No component pin moved.**
|
||||||
|
Every ref was re-resolved at tag time and is byte-identical to what v1.8.4
|
||||||
|
shipped: `pi` `0.84.2` (still npm latest), `pi-atelier` `v0.8.2` → `159f34cf`
|
||||||
|
(newest tag; the `≥0.7.1` floor for `pi ≥ 0.84` holds), `pi-studio` `v0.9.48` →
|
||||||
|
`c3b83680`, `pi-fork` `f1ff8087`, `pi-observational-memory` `ce9fc982`,
|
||||||
|
`pi-toolkit` `0e1369e6`, `pi-extensions` `20228878`, `MEMPALACE_VERSION` `3.7.1`
|
||||||
|
(still PyPI latest, and the version the central palace serves — no client/server
|
||||||
|
skew). The single moving part is `mempalace-toolkit` `fd8b15f5` → `0fe64c4`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Vendored skills no longer silently shadow their live skillset
|
||||||
|
counterparts.** `~/.agents/skills` was *asymmetric*: `mempalace`,
|
||||||
|
`pi-devbox-environment` and `pi-extensions` resolved to the baked
|
||||||
|
`/usr/local/share/pi-devbox/skills/…`, while every other skill resolved to the
|
||||||
|
live `/workspace/skillset/skills/…`. Root cause was precedence-by-ordering in
|
||||||
|
`entrypoint-user.sh`: the baked links are created **early** (line 65 in
|
||||||
|
v1.8.4; the loop moved down as this fix added comments) — deliberately so, to
|
||||||
|
close a smoke-test readiness race — with `[ ! -e … ]` so they are
|
||||||
|
"created only when absent", and the skillset deploy runs **last**, where it
|
||||||
|
classifies the existing links as foreign and leaves them alone. The comment at
|
||||||
|
line 61 claimed the goal was "a same-named skillset skill … is never
|
||||||
|
clobbered" — but with baked-first plus create-when-absent, the *skillset* skill
|
||||||
|
was precisely the one that lost. Comment now describes the actual behaviour.
|
||||||
|
|
||||||
|
Observed cost, on two hosts independently: an edit to
|
||||||
|
`skillset/skills/mempalace/SKILL.md` (adding a drawer-attribution rule) was
|
||||||
|
pushed and present in the live clone (`md5 129bcc4752`), yet both the
|
||||||
|
EMB-7KJ4VR4G and tor-ms22 containers kept loading the baked copy
|
||||||
|
(`md5 5236024fef`) with zero occurrences of the new rule. The tor-ms22 agent
|
||||||
|
had to fetch the rule from the Gitea API to read it at all. Editing a skillset
|
||||||
|
skill therefore *appeared* to work and silently did nothing until an image
|
||||||
|
rebuild — for exactly the three skills most likely to be iterated on.
|
||||||
|
|
||||||
|
**The fix is not "the skillset always wins"**, because ownership is per-skill
|
||||||
|
(`rootfs/usr/local/share/pi-devbox/skills/VENDORED.md`): `pi-extensions`'
|
||||||
|
authoritative source is the *package* repo, copied over the snapshot at build
|
||||||
|
time, and `skillset` carries a downstream copy that can lag — handing that one
|
||||||
|
to the clone would regress the skill. So a new helper
|
||||||
|
`devbox-skill-reconcile` runs immediately after the skillset deploy and
|
||||||
|
repoints only the skills named in `skills/skillset-owned.txt` (today:
|
||||||
|
`mempalace`). Precedence is now user override → live skillset clone (owned
|
||||||
|
names only) → baked snapshot, with the early links untouched as the fallback,
|
||||||
|
so the readiness race stays closed. It only ever replaces a symlink that
|
||||||
|
points into the baked tree, so a real directory or a link pointing elsewhere
|
||||||
|
is never disturbed. Verify with `readlink -f ~/.agents/skills/mempalace`, not
|
||||||
|
by reading the entrypoint.
|
||||||
|
|
||||||
|
- **A latent boot-abort in the baked-link block, found while reviewing the fix
|
||||||
|
above and fixed with it.** `[ ! -e "$link" ]` is TRUE for a *dangling* symlink
|
||||||
|
(`-e` follows the link), so once a link may point into `/workspace/skillset`
|
||||||
|
— which the fix above makes possible — a vanished mount turns the guard into
|
||||||
|
"create over a broken link", and plain `ln -s` then fails with `File exists`.
|
||||||
|
Under the entrypoint's `set -euo pipefail` that **aborts container start**
|
||||||
|
before `exec "$@"`, with a cryptic `ln` error and no pi. Reachable on a
|
||||||
|
`docker restart` or a host reboot under `restart: unless-stopped` (the writable
|
||||||
|
layer survives and `~/.agents` is not a volume on any host), though not on a
|
||||||
|
`compose up -d` recreate. Now `ln -sfn`, which heals the broken link back to
|
||||||
|
the baked fallback; the reconciler re-points it in the same boot if the clone
|
||||||
|
is back. A comment at the call site records why the `-f` must stay.
|
||||||
|
|
||||||
|
- **README's skill-precedence documentation was wrong** in the same way the
|
||||||
|
entrypoint comment was: it claimed baked skills are "created only when absent
|
||||||
|
so a same-named skillset skill … is never clobbered" and that "a mounted
|
||||||
|
skillset always overrides them". Rewritten to state the real, per-skill
|
||||||
|
precedence and to name `skillset-owned.txt` and `devbox-skill-reconcile`.
|
||||||
|
|
||||||
|
- **The smoke canary for a stale `mempalace` snapshot could not detect
|
||||||
|
staleness.** It grepped `"Shared palace: multiple harnesses"` — a phrase
|
||||||
|
present in *both* the stale and the fresh copy, so it passed throughout the
|
||||||
|
shadowing bug above. It now pins the newest section
|
||||||
|
(`"Attribute what you file yourself"`), and `VENDORED.md` records that
|
||||||
|
updating this string is part of refreshing the snapshot. Three further
|
||||||
|
assertions close the gaps that let the bug ship: skill link **targets** are
|
||||||
|
asserted (not merely `test -L`), the `skillset-owned.txt` list is asserted to
|
||||||
|
contain `mempalace` and *not* `pi-extensions`, and the reconciler's replace
|
||||||
|
path — which CI never exercises, since no smoke container mounts a skillset —
|
||||||
|
is covered by fabricating a skillset and asserting all three outcomes (owned
|
||||||
|
skill repointed, unowned skill left baked, user override untouched) — plus a
|
||||||
|
second case that a mutation test proved necessary: with the reconciler's
|
||||||
|
"is this link ours?" guard deleted, all three of those assertions still
|
||||||
|
passed, so the discriminating case is an *owned* name whose link is a user
|
||||||
|
override pointing outside the baked tree.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Vendored `mempalace` skill snapshot refreshed** from `skillset` `936fed8` →
|
||||||
|
`670f7f1` (`md5 5236024fef` → `129bcc4752`), which adds the "Attribute what
|
||||||
|
you file yourself" rule: hand-filed drawers should carry
|
||||||
|
`added_by="<harness>@<device>"`. Without this refresh the symlink fix above
|
||||||
|
would only help hosts that mount `skillset`; a bare container would still ship
|
||||||
|
the pre-attribution-rule skill.
|
||||||
|
|
||||||
|
- **Component audit for this release — no pin edits needed.** Every component
|
||||||
|
except `pi`/`pi-atelier` is pinned to a moving ref that CI resolves at build
|
||||||
|
time, and each was checked against upstream on 2026-08-23: `pi` `0.84.2`
|
||||||
|
(still npm latest, published 2026-08-14), `pi-atelier` `v0.8.2` (newest tag;
|
||||||
|
the `≥0.7.1` floor for `pi ≥ 0.84` is satisfied), `pi-fork` `f1ff8087`,
|
||||||
|
`pi-observational-memory` `ce9fc982`, `pi-toolkit` `0e1369e6`,
|
||||||
|
`pi-extensions` `20228878`, `pi-studio` `v0.9.48` → `c3b83680` — all
|
||||||
|
**byte-identical to what v1.8.4 shipped**. `MEMPALACE_VERSION` stays `3.7.1`
|
||||||
|
(still PyPI latest, and the version the central palace serves, so no
|
||||||
|
client/server skew). The one component that moved is `mempalace-toolkit`
|
||||||
|
`fd8b15f5` → `0fe64c4`, which is this release's other payload: the feeder now
|
||||||
|
defaults `--agent` to `pi@$MEMPALACE_PI_DEVICE` so palace writes carry
|
||||||
|
provenance, with `$USER` still the fallback when the variable is unset
|
||||||
|
(`AGENT="${MEMPALACE_PI_DEVICE:+pi@${MEMPALACE_PI_DEVICE}}"`), so un-enrolled
|
||||||
|
hosts are unaffected. Nothing landed upstream after the
|
||||||
|
`pi-observational-memory` merge `ce9fc982`, so the eight-week-bug fix in
|
||||||
|
v1.8.4 is not destabilised. All of the above was re-resolved immediately
|
||||||
|
before the tag and was unchanged — worth repeating for any future release,
|
||||||
|
because six of nine components are moving refs that CI resolves at build
|
||||||
|
time, so the build, not the Dockerfile, decides what ships.
|
||||||
|
|
||||||
|
Two notes for whoever runs the build. This release changes
|
||||||
|
`entrypoint-user.sh`, `rootfs/**` and the resolved toolkit SHA — all three feed
|
||||||
|
the base-image hash — so expect a **full multi-arch base rebuild** (~95 min,
|
||||||
|
as on v1.8.3/CI 562), not a fast variant-only publish. And that rebuild
|
||||||
|
re-resolves the ~14 base-tooling `ARG *_VERSION=latest` pins; measured drift
|
||||||
|
on 2026-08-23 was one patch (`nvim v0.12.4 → v0.12.5`), so the window is
|
||||||
|
favourable, but it is not covered by version assertions.
|
||||||
|
|
||||||
|
- **`pi-devbox-environment` skill — new §2 subsection "A negative result is
|
||||||
|
usually your own filter", plus ControlMaster masking in §3.** This is baked
|
||||||
|
(`rootfs/usr/local/share/pi-devbox/skills/`, symlinked to
|
||||||
|
`~/.agents/skills/`), so it is an image-behaviour change even though no
|
||||||
|
package moved. Motivated by three false negatives an agent produced in a
|
||||||
|
single session, each from its own filter rather than from the world: a
|
||||||
|
`| head -20` "proved" an SSH peer absent that was defined at **line 454** of a
|
||||||
|
~500-line config; `ssh mac 'docker ps'` "proved" the host had no Docker, when
|
||||||
|
the non-interactive SSH `PATH` simply lacks `/usr/local/bin`; and a `grep 'ssh
|
||||||
|
'` "proved" no ControlMaster was running, when master processes **rename
|
||||||
|
themselves** to `ssh: <controlpath> [mux]`. The rule now stated: a positive
|
||||||
|
result carries its own evidence, absence has to be *earned*. §3 additionally
|
||||||
|
documents that a live master socket makes later commands authenticate **not at
|
||||||
|
all**, so "it still works" proves nothing after editing a peer's
|
||||||
|
`authorized_keys` — verify with `-o ControlPath=none -o ControlMaster=no`, or
|
||||||
|
the breakage surfaces in a future session with no memory of the edit.
|
||||||
|
- **`AGENTS.md`: a stale CI claim corrected.** It said "a tag push produces two
|
||||||
|
runs, not one — `lint.yml` fires on every push (including tag refs)". That
|
||||||
|
stopped being true when lint was scoped to `branches: ['**']`, which excludes
|
||||||
|
tag refs by design; `refs/tags/v1.8.4` produced run 571 (publish) and nothing
|
||||||
|
else. The `head_sha` + workflow-`path` filter advice stays, because it costs
|
||||||
|
nothing and any future `v*`-triggered workflow would reintroduce the
|
||||||
|
ambiguity. Also adds a short "Verifying this repo's reality from inside a
|
||||||
|
container" section, including the trap that **this repo's `docker-compose.yml`
|
||||||
|
is a template pinning `:latest`** while a real host runs its own per-machine
|
||||||
|
file — so recreating from the repo copy can silently move a host off
|
||||||
|
`:latest-studio`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Still open
|
||||||
|
|
||||||
|
- `build-manifest.json` records the `mempalace-toolkit` SHA but **not** the
|
||||||
|
mempalace **core** version, so a palace bug cannot be correlated with an
|
||||||
|
image. `mempalace --version` prints it; adding it is a one-line change to the
|
||||||
|
manifest `RUN` in `Dockerfile.variant` plus one smoke assertion, and is
|
||||||
|
variant-only (no base rebuild cost).
|
||||||
|
- Smoke asserts the `pi-observational-memory` clone **exists** but not that it
|
||||||
|
contains the ambient-auth fix. npm still ships pre-fix `3.0.4`, so an
|
||||||
|
accidental switch from the `/opt` clone to an npm install would be a silent
|
||||||
|
regression. Cheap guard: `grep -rl availability_recheck` must be ≥1.
|
||||||
|
- The feeder's new `pi@<device>` default has no behavioural test hook
|
||||||
|
(`--dry-run` never prints the agent; `--self-test` only covers the remote-mine
|
||||||
|
response classifier). Cheapest available check is a source-shape grep for
|
||||||
|
`MEMPALACE_PI_DEVICE:+pi@`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.8.4 — 2026-08-22
|
||||||
|
|
||||||
|
Patch release, and the one that ends an eight-week bug: **the baked
|
||||||
|
`pi-observational-memory` finally records observations on a Bedrock host that
|
||||||
|
uses ambient AWS credentials.** The fix is ours, but it is no longer a patch —
|
||||||
|
upstream merged it, so this release picks it up through the ordinary
|
||||||
|
`PI_OBSMEM_REF=master` path with no local carry. Also **bumps pi-atelier
|
||||||
|
`v0.8.1` → `v0.8.2`** (audited below) and bakes the `todo` extension's new
|
||||||
|
`edit` action. `pi` stays `0.84.2` (still the npm latest, published
|
||||||
|
2026-08-14) and `MEMPALACE_VERSION` stays `3.7.1` (still the PyPI latest).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **om consolidation on request-time-signed providers — upstream, not patched
|
||||||
|
(`pi-observational-memory` `37986b6` → `ce9fc98`).** Under `37986b6`, om's
|
||||||
|
pre-flight gate treated "pi exposes no `apiKey` and no auth header" as
|
||||||
|
*unauthenticated* and skipped every consolidation. On Bedrock with ambient
|
||||||
|
AWS credentials that is the normal case — pi signs SigV4 at request time —
|
||||||
|
so om recorded **nothing for eight weeks** with no error, no cost and no log
|
||||||
|
line. Every pi-devbox image up to and including v1.8.3 has that behaviour.
|
||||||
|
|
||||||
|
Two commits, both authored here and now upstream verbatim: `6f694e6` fixes
|
||||||
|
the gate itself (it must not require a credential *payload*), and `699ccc7`
|
||||||
|
adds the second half of pi's own rule — `hasConfiguredAuth` reads an
|
||||||
|
availability snapshot that stays empty when the startup availability pass was
|
||||||
|
skipped/aborted/failed, so on the otherwise-fatal path om now asks pi to
|
||||||
|
re-check the credential live (refresh scoped to the provider, network-free),
|
||||||
|
rate-limited 60 s per provider, bounded by a raced timeout, logged as
|
||||||
|
`resolve.availability_recheck`. Filed as upstream issue #51, merged as PR #52
|
||||||
|
(`ce9fc98`, 2026-08-22T04:46:18Z), which also carries PR #49's `env`/`baseUrl`
|
||||||
|
forwarding merged four minutes earlier; the maintainer resolved the textual
|
||||||
|
conflict between them keeping both behaviours. Verified on `ce9fc98` here:
|
||||||
|
`tsc --noEmit` clean, `vitest` 257 tests / 27 files green.
|
||||||
|
|
||||||
|
**Note for anyone carrying the local workaround:** the interim fix was a
|
||||||
|
`packages[]` override in `~/.pi/agent/settings.json` pointing pi at a patched
|
||||||
|
clone outside the image. From this release on, delete the override — the
|
||||||
|
baked `/opt/pi-observational-memory` has the fix. Confirm with
|
||||||
|
`/etc/pi-devbox/build-manifest.json` → `components.pi-observational-memory`
|
||||||
|
before removing it. The npm-published `pi-observational-memory` is **still
|
||||||
|
`3.0.4` and still broken**; the image does not use npm for this component, so
|
||||||
|
the release cadence there is irrelevant to us.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`PI_ATELIER_REF` / `PI_ATELIER_VERSION` `v0.8.1` → `v0.8.2`, audited per the
|
||||||
|
floor note above the ARG.** Only one version sits between old and new and its
|
||||||
|
changelog is two lines, both Workspace-Pulse-internal: inspection requests are
|
||||||
|
now coalesced and serialized so short Turns avoid duplicate Git work and
|
||||||
|
overlapping inspections cannot run concurrently, and live tool-driven Pulse
|
||||||
|
updates are preserved while a fresh inspection is guaranteed at Turn end and
|
||||||
|
retired sessions can no longer publish stale results. **Nothing touches pi's
|
||||||
|
private TUI renderer**, which is the coupling that produced the
|
||||||
|
0.6.0/0.7.0-under-pi-0.84 startup hang, and `pi` is unchanged at `0.84.2`, so
|
||||||
|
this bump does not re-enter that risk class. Both the seam and the pin floor
|
||||||
|
(never pair pi-atelier < 0.7.1 with pi >= 0.84) are unaffected.
|
||||||
|
- **`pi-studio` `65995fe` (0.9.44) → `v0.9.48`** — 14 commits, four releases.
|
||||||
|
Studio-side only (`INSTALL_STUDIO=false` by default, so this lands in the
|
||||||
|
studio variant): open Studio in Muxy's browser, local PDF preview actions,
|
||||||
|
previews survive Pandoc probe failures, legacy LaTeX styles tolerated in
|
||||||
|
Pandoc previews, native dialogs replaced in embedded browsers, and file-copy
|
||||||
|
import fixes with an explicit fallback. CI resolves the highest semver tag,
|
||||||
|
not `main`, so this is `v0.9.48` exactly.
|
||||||
|
- **`pi-fork` `4a09af4` → `f1ff808`** — one commit, "Add fork runtime
|
||||||
|
awareness" (2026-08-19).
|
||||||
|
- **`mempalace-toolkit` `b609cf5` → `fd8b15f`** — two commits, **docs only**
|
||||||
|
(backup/recovery + units; the convos-miner mtime correction finished). The
|
||||||
|
`fix(pi-session)` false-success guard was already baked in v1.8.3 — checked
|
||||||
|
by ancestry (`git merge-base --is-ancestor 6e1f4f3 b609cf5`), not by reading
|
||||||
|
the log, because a commit's *date* does not tell you which side of a pin it
|
||||||
|
fell on.
|
||||||
|
- **`aws-cli` 2.36.24 → 2.36.29** and the other `*_VERSION=latest` tools
|
||||||
|
(bat/eza/fzf/gitleaks/nvim/micro/zoxide/yq/typst/tealdeer/agent-browser/
|
||||||
|
playwright/gosu/git-lfs/uv) refresh implicitly, as designed.
|
||||||
|
- `pi-toolkit` unchanged (`0e1369e`, local `main` == baked).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`todo` extension: an `edit` action** (`pi-extensions` `98eb07b` →
|
||||||
|
`2022887`). The tool is a verbatim vendored copy of pi's own
|
||||||
|
`examples/extensions/todo.ts`, which offers list/add/toggle/clear and no way
|
||||||
|
to change an item's text. On a long-lived list that forces either a "patch"
|
||||||
|
item describing a *different* item, or clear-and-re-add of everything — both
|
||||||
|
hit for real on 2026-08-17 while tracking a 17-item fleet plan, which ended up
|
||||||
|
with `#18` correcting `#17`. `edit` takes `id` + `text` and keeps the id and
|
||||||
|
the done status; `nextId` is untouched. Id stability is the point, because ids
|
||||||
|
are the only handle a palace snapshot of a plan can refer to.
|
||||||
|
- Verified live in-container before committing, by repointing
|
||||||
|
`~/.pi/agent/extensions/todo.ts` at a working copy for one session (unknown
|
||||||
|
id and missing text both error as intended; editing a completed item kept its
|
||||||
|
id and its `done` state), then reverting the symlink to the image copy.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- **No pi-atelier change was needed for the `todo` action.** Its `tool_result`
|
||||||
|
hook only checks that `details.todos` is a well-shaped array and ignores the
|
||||||
|
action string, so the new action flows through its normalizer and sidebar
|
||||||
|
untouched. Worth knowing while reading agent transcripts: that hook
|
||||||
|
*replaces* todo tool output with `N/M done · see sidebar` whenever the sidebar
|
||||||
|
todo panel is visible (`showSidebarTodos`), so an agent sees only the counter
|
||||||
|
and not the item text — upstream's `list` otherwise returns every item. That
|
||||||
|
is a deliberate context saving, not a tool limitation.
|
||||||
|
- The vendored copy now carries a numbered **LOCAL DELTAS** list in its header
|
||||||
|
(the earlier `ctx.mode !== "tui"` → `!ctx.hasUI` API fix, and this action), so
|
||||||
|
reconciling a future upstream version stays mechanical rather than
|
||||||
|
archaeological.
|
||||||
|
- **A second om fix is NOT in this image and will not be.** Upstream PR #24
|
||||||
|
("advance coverage watermark when observer records nothing", head
|
||||||
|
`joakimp:fix/observer-empty-coverage-watermark` `b577b29`) is open but
|
||||||
|
design-rejected by the maintainer on 2026-07-03: an empty observer verdict is
|
||||||
|
usually a *technical* failure, so advancing the watermark would leave a gap in
|
||||||
|
the observed session, and in the genuinely-nothing-to-observe case the next
|
||||||
|
observer simply gets more context. So the observer can still re-fire on a
|
||||||
|
growing span after an empty verdict — that is upstream's intended behaviour,
|
||||||
|
not an image defect. Do not "fix" it by rebasing that branch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
## v1.8.2 — 2026-08-16
|
||||||
|
|
||||||
Patch release. **Ships the fix for a silent transcript-feed failure**, plus the
|
Patch release. **Ships the fix for a silent transcript-feed failure**, plus the
|
||||||
|
|||||||
+19
-2
@@ -384,7 +384,19 @@ ARG INSTALL_MEMPALACE=true
|
|||||||
# mempalace_checkpoint (#2023/#2034).
|
# mempalace_checkpoint (#2023/#2034).
|
||||||
#
|
#
|
||||||
# Keep in lockstep with opencode-devbox when bumping.
|
# 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_DIR=/opt/uv-tools
|
||||||
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
||||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
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-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-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-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 \
|
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-session --help >/dev/null && \
|
||||||
mempalace-docs --help >/dev/null && \
|
mempalace-docs --help >/dev/null && \
|
||||||
mempalace-pi-session --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)" ; \
|
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -671,12 +686,14 @@ COPY rootfs/usr/local/share/pi-devbox/ /usr/local/share/pi-devbox/
|
|||||||
COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose
|
COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose
|
||||||
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
||||||
COPY rootfs/usr/local/bin/pi-devbox-version /usr/local/bin/pi-devbox-version
|
COPY rootfs/usr/local/bin/pi-devbox-version /usr/local/bin/pi-devbox-version
|
||||||
|
COPY rootfs/usr/local/bin/devbox-skill-reconcile /usr/local/bin/devbox-skill-reconcile
|
||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
||||||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
||||||
/usr/local/bin/studio-expose \
|
/usr/local/bin/studio-expose \
|
||||||
/usr/local/bin/dot-watch \
|
/usr/local/bin/dot-watch \
|
||||||
/usr/local/bin/pi-devbox-version \
|
/usr/local/bin/pi-devbox-version \
|
||||||
|
/usr/local/bin/devbox-skill-reconcile \
|
||||||
/usr/local/lib/pi-devbox/*.sh 2>/dev/null || true
|
/usr/local/lib/pi-devbox/*.sh 2>/dev/null || true
|
||||||
|
|
||||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
||||||
|
|||||||
+2
-2
@@ -92,9 +92,9 @@ ARG PI_OBSMEM_REF=master
|
|||||||
# the /opt checkout. Adding an install here would be a no-op that only costs
|
# the /opt checkout. Adding an install here would be a no-op that only costs
|
||||||
# build time.
|
# build time.
|
||||||
ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
|
ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
|
||||||
ARG PI_ATELIER_REF=v0.8.1
|
ARG PI_ATELIER_REF=v0.8.2
|
||||||
# Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label.
|
# Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label.
|
||||||
ARG PI_ATELIER_VERSION=v0.8.1
|
ARG PI_ATELIER_VERSION=v0.8.2
|
||||||
|
|
||||||
RUN set -e && \
|
RUN set -e && \
|
||||||
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
|
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
|
||||||
|
|||||||
@@ -547,7 +547,8 @@ directory, and they compose:
|
|||||||
`~/.agents/skills/` by `entrypoint-user.sh` on every start. They need no
|
`~/.agents/skills/` by `entrypoint-user.sh` on every start. They need no
|
||||||
external mount, survive volume recreate (the source is an image path, not a
|
external mount, survive volume recreate (the source is an image path, not a
|
||||||
home dir a named volume would shadow), and are created only when absent so a
|
home dir a named volume would shadow), and are created only when absent so a
|
||||||
same-named skillset skill or user override is never clobbered. The bundled
|
user override is never clobbered. Precedence against a mounted `skillset` repo
|
||||||
|
is per-skill, not blanket — see *Skillset repo* below. The bundled
|
||||||
**`pi-devbox-environment`** skill is delivered this way — it teaches agents
|
**`pi-devbox-environment`** skill is delivered this way — it teaches agents
|
||||||
the container's persistence model, host/LAN SSH reachability, split-DNS
|
the container's persistence model, host/LAN SSH reachability, split-DNS
|
||||||
mechanisms, the interactive-vs-tool-shell alias gotcha (`dssh`/`dscp`),
|
mechanisms, the interactive-vs-tool-shell alias gotcha (`dssh`/`dscp`),
|
||||||
@@ -558,8 +559,11 @@ directory, and they compose:
|
|||||||
pi session to read `~/.agents/skills/pi-extensions/SKILL.md` at start (to fix
|
pi session to read `~/.agents/skills/pi-extensions/SKILL.md` at start (to fix
|
||||||
fork/recall under-utilisation). That pointer would dangle in a container
|
fork/recall under-utilisation). That pointer would dangle in a container
|
||||||
started *without* the private `skillset` repo, so the image also bakes
|
started *without* the private `skillset` repo, so the image also bakes
|
||||||
fallback copies of **`pi-extensions`** and **`mempalace`**. They are
|
fallback copies of **`pi-extensions`** and **`mempalace`**. Whether a mounted
|
||||||
symlinked only when absent, so a mounted skillset always overrides them. The
|
skillset overrides them depends on who *owns* the skill (see *Skillset repo*):
|
||||||
|
`mempalace` is skillset-owned, so the live clone wins; `pi-extensions` is
|
||||||
|
owned by its package repo, so the baked copy keeps winning — the skillset's
|
||||||
|
copy of it is a downstream duplicate that can lag. The
|
||||||
`pi-extensions` skill is *layered*: a committed snapshot in `rootfs/` is the
|
`pi-extensions` skill is *layered*: a committed snapshot in `rootfs/` is the
|
||||||
floor, and `Dockerfile.variant` copies the canonical, package-owned copy from
|
floor, and `Dockerfile.variant` copies the canonical, package-owned copy from
|
||||||
the pinned `pi-extensions` clone (`/opt/pi-extensions/skill/`) over it at
|
the pinned `pi-extensions` clone (`/opt/pi-extensions/skill/`) over it at
|
||||||
@@ -573,7 +577,16 @@ directory, and they compose:
|
|||||||
- **Skillset repo (optional).** If a `skillset` repo is mounted (at
|
- **Skillset repo (optional).** If a `skillset` repo is mounted (at
|
||||||
`$HOME/skillset` or `/workspace/skillset`, or via `SKILLSET_CONTAINER_PATH`),
|
`$HOME/skillset` or `/workspace/skillset`, or via `SKILLSET_CONTAINER_PATH`),
|
||||||
`deploy-skills.sh` symlinks its skills in too. Image-baked skills are
|
`deploy-skills.sh` symlinks its skills in too. Image-baked skills are
|
||||||
classified as foreign-links by its `--prune-stale` pass and left untouched.
|
classified as foreign-links by its `--prune-stale` pass and left untouched —
|
||||||
|
which through v1.8.4 meant the baked copy *always* won, so an edit pushed to a
|
||||||
|
skillset-owned skill was invisible until the next image build. Since v1.8.5
|
||||||
|
`devbox-skill-reconcile` runs right after the deploy and repoints the links for
|
||||||
|
skills the skillset owns, listed in
|
||||||
|
`/usr/local/share/pi-devbox/skills/skillset-owned.txt` (today: `mempalace`).
|
||||||
|
Effective precedence, highest first: **user override** (a real directory, or a
|
||||||
|
symlink pointing outside the baked tree) → **live skillset clone** (owned names
|
||||||
|
only) → **baked snapshot** (everything else, and every skill when no skillset
|
||||||
|
is mounted). Check with `readlink -f ~/.agents/skills/<skill>`.
|
||||||
|
|
||||||
To make agents *proactively* load a baked skill at session start (rather than
|
To make agents *proactively* load a baked skill at session start (rather than
|
||||||
only on description match), the image appends a short, gated pointer to the
|
only on description match), the image appends a short, gated pointer to the
|
||||||
@@ -990,8 +1003,8 @@ resolved to `latest` at build time:
|
|||||||
| Component | Pin | Where |
|
| Component | Pin | Where |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| pi | `0.84.2` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
| pi | `0.84.2` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
||||||
| pi-atelier | `v0.8.1` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
| pi-atelier | `v0.8.2` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
||||||
| mempalace | `3.6.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
| mempalace | `3.7.1` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
||||||
|
|
||||||
The objective is **not** to freeze versions. Bumping is routine — usually one
|
The objective is **not** to freeze versions. Bumping is routine — usually one
|
||||||
line plus a changelog note. The objective is that adopting a new upstream
|
line plus a changelog note. The objective is that adopting a new upstream
|
||||||
|
|||||||
+28
-5
@@ -58,10 +58,17 @@ fi
|
|||||||
# the runtime skill-link assertion. Pointing at the image path (/usr/local/...)
|
# the runtime skill-link assertion. Pointing at the image path (/usr/local/...)
|
||||||
# keeps the skill fresh from the image and surviving volume recreate (unlike
|
# keeps the skill fresh from the image and surviving volume recreate (unlike
|
||||||
# anything baked under a home dir, which a named volume would shadow). Created
|
# anything baked under a home dir, which a named volume would shadow). Created
|
||||||
# only when absent, so a same-named skillset skill (deployed later, at the end
|
# only when absent, so a user override is never clobbered.
|
||||||
# of this script) or a user override is never clobbered; the skillset deploy
|
#
|
||||||
# classifies these as foreign-links and its --prune-stale pass leaves them
|
# NB: "created only when absent" does NOT hand a same-named skillset skill
|
||||||
# alone (only dangling symlinks are pruned).
|
# priority — the opposite. The skillset deploy runs at the end of this script
|
||||||
|
# and classifies these links as foreign, so through v1.8.4 the BAKED copy
|
||||||
|
# always won and an edit pushed to a skillset-owned skill was invisible until
|
||||||
|
# the next image build. The links below are therefore the FALLBACK only;
|
||||||
|
# devbox-skill-reconcile (invoked right after the skillset deploy) hands the
|
||||||
|
# skillset-OWNED skills back to the live clone. Ownership is per-skill, listed
|
||||||
|
# in skills/skillset-owned.txt — see VENDORED.md for why pi-extensions must
|
||||||
|
# keep losing to the baked copy.
|
||||||
DEVBOX_SKILLS_SRC=/usr/local/share/pi-devbox/skills
|
DEVBOX_SKILLS_SRC=/usr/local/share/pi-devbox/skills
|
||||||
if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
||||||
mkdir -p "$HOME/.agents/skills"
|
mkdir -p "$HOME/.agents/skills"
|
||||||
@@ -69,7 +76,16 @@ if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
|||||||
[ -d "$_sk" ] || continue
|
[ -d "$_sk" ] || continue
|
||||||
_skname=$(basename "$_sk")
|
_skname=$(basename "$_sk")
|
||||||
if [ ! -e "$HOME/.agents/skills/$_skname" ]; then
|
if [ ! -e "$HOME/.agents/skills/$_skname" ]; then
|
||||||
ln -s "${_sk%/}" "$HOME/.agents/skills/$_skname"
|
# -sfn, not -s: `[ ! -e ]` is TRUE for a DANGLING symlink (-e follows the
|
||||||
|
# link), and since v1.8.5 these links can point into /workspace/skillset
|
||||||
|
# (see devbox-skill-reconcile, invoked after the skillset deploy). If that
|
||||||
|
# mount vanishes while the writable layer survives — a `docker restart` or
|
||||||
|
# a host reboot under restart: unless-stopped, as opposed to a recreate —
|
||||||
|
# plain `ln -s` fails with "File exists" and, under `set -e`, aborts
|
||||||
|
# container start before `exec "$@"`. With -f the broken link heals back to
|
||||||
|
# the baked fallback, and the reconciler re-points it in the same boot if
|
||||||
|
# the clone is back.
|
||||||
|
ln -sfn "${_sk%/}" "$HOME/.agents/skills/$_skname"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -385,6 +401,13 @@ elif [ -x /workspace/skillset/deploy-skills.sh ]; then
|
|||||||
fi
|
fi
|
||||||
if [ -n "$SKILLSET_DEPLOY" ]; then
|
if [ -n "$SKILLSET_DEPLOY" ]; then
|
||||||
"$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true
|
"$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true
|
||||||
|
# The deploy leaves the early baked links (above) in place as foreign links,
|
||||||
|
# which silently shadows the live clone for skills the skillset OWNS. Repoint
|
||||||
|
# just those; baked stays the fallback, user overrides still win. `|| true`:
|
||||||
|
# a skill-link refinement must never break container start.
|
||||||
|
if command -v devbox-skill-reconcile >/dev/null 2>&1; then
|
||||||
|
devbox-skill-reconcile "$(dirname "$SKILLSET_DEPLOY")" || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Execute command ──────────────────────────────────────────────────
|
# ── Execute command ──────────────────────────────────────────────────
|
||||||
|
|||||||
Executable
+91
@@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# devbox-skill-reconcile — hand skillset-OWNED skills back to the live clone.
|
||||||
|
#
|
||||||
|
# WHY THIS EXISTS
|
||||||
|
# ---------------
|
||||||
|
# entrypoint-user.sh links the image-baked skills into ~/.agents/skills/ EARLY
|
||||||
|
# (before pi-deploy), because the smoke readiness probe gates on markers that
|
||||||
|
# only land later, and a link created after that gate produced a flaky
|
||||||
|
# assertion. Those links are created with a `[ ! -e ]` guard — "only when
|
||||||
|
# absent" — and the skillset deploy runs LAST, treating already-present links
|
||||||
|
# as foreign and leaving them alone. Net effect through v1.8.4: the baked copy
|
||||||
|
# always won, so an edit pushed to a skillset-owned skill was invisible in
|
||||||
|
# every container until the next image build (measured on two hosts: live
|
||||||
|
# skillset md5 129bcc4752 vs baked 5236024fef, the new section absent).
|
||||||
|
#
|
||||||
|
# The fix is NOT "the skillset always wins". Ownership is per-skill (see
|
||||||
|
# rootfs/usr/local/share/pi-devbox/skills/VENDORED.md):
|
||||||
|
#
|
||||||
|
# pi-devbox-environment authored in pi-devbox → baked IS canonical
|
||||||
|
# pi-extensions owned by the package repo, copied over the snapshot
|
||||||
|
# at build time; skillset carries a DOWNSTREAM copy
|
||||||
|
# that can lag → baked must keep winning
|
||||||
|
# mempalace owned by the skillset repo; baked is a snapshot
|
||||||
|
# fallback for containers with no skillset mounted
|
||||||
|
# → the live clone must win when it is present
|
||||||
|
#
|
||||||
|
# So only skills listed in skills/skillset-owned.txt are handed over. Baked
|
||||||
|
# links stay as the fallback (the early-link race fix is untouched), and a user
|
||||||
|
# override always beats both: a real directory is never replaced, and neither is
|
||||||
|
# a symlink that already points somewhere other than the baked tree.
|
||||||
|
#
|
||||||
|
# Usage: devbox-skill-reconcile <skillset-root> [skills-dir] [baked-src]
|
||||||
|
# skillset-root the mounted skillset repo (contains skills/<name>/)
|
||||||
|
# skills-dir default $HOME/.agents/skills
|
||||||
|
# baked-src default /usr/local/share/pi-devbox/skills
|
||||||
|
#
|
||||||
|
# Idempotent, and silent unless it changes something. Exits 0 when there is
|
||||||
|
# nothing to do (no skillset, no list) so the entrypoint never fails on it.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SKILLSET_ROOT="${1:-}"
|
||||||
|
SKILLS_DIR="${2:-$HOME/.agents/skills}"
|
||||||
|
BAKED_SRC="${3:-/usr/local/share/pi-devbox/skills}"
|
||||||
|
BAKED_SRC="${BAKED_SRC%/}" # a trailing slash would make the prefix
|
||||||
|
# match below ("$BAKED_SRC"/*) match nothing
|
||||||
|
|
||||||
|
[ -n "$SKILLSET_ROOT" ] || exit 0
|
||||||
|
[ -d "$SKILLSET_ROOT/skills" ] || exit 0
|
||||||
|
[ -d "$SKILLS_DIR" ] || exit 0
|
||||||
|
|
||||||
|
# Absolutise BOTH roots before they are used, because each has its own way of
|
||||||
|
# failing silently when relative: a relative symlink TARGET is resolved against
|
||||||
|
# the link's directory (~/.agents/skills), not $PWD, so it would dangle on
|
||||||
|
# creation; and a relative BAKED_SRC would never prefix-match the absolute
|
||||||
|
# target that `readlink` reports, so every skill would be skipped and the fix
|
||||||
|
# would look like it had simply done nothing.
|
||||||
|
SKILLSET_ROOT=$(CDPATH= cd -- "$SKILLSET_ROOT" 2>/dev/null && pwd) || exit 0
|
||||||
|
BAKED_SRC=$(CDPATH= cd -- "$BAKED_SRC" 2>/dev/null && pwd) || exit 0
|
||||||
|
OWNED_LIST="$BAKED_SRC/skillset-owned.txt"
|
||||||
|
[ -f "$OWNED_LIST" ] || exit 0
|
||||||
|
|
||||||
|
while IFS= read -r _line || [ -n "$_line" ]; do
|
||||||
|
# strip comments and surrounding whitespace; skip blanks
|
||||||
|
_name=$(printf '%s\n' "$_line" | sed -e 's/#.*$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||||
|
[ -n "$_name" ] || continue
|
||||||
|
# defensive: a list entry must be a plain skill name, never a path
|
||||||
|
case "$_name" in */*|.*) continue ;; esac
|
||||||
|
|
||||||
|
_live="$SKILLSET_ROOT/skills/$_name"
|
||||||
|
_link="$SKILLS_DIR/$_name"
|
||||||
|
|
||||||
|
# the skillset does not ship it → the baked fallback is all there is
|
||||||
|
[ -d "$_live" ] || continue
|
||||||
|
# a real directory is a user override → never touch
|
||||||
|
[ -L "$_link" ] || continue
|
||||||
|
|
||||||
|
# only ever replace OUR OWN link. readlink is deliberate: `readlink -f`
|
||||||
|
# would resolve a link that already points into the skillset clone and,
|
||||||
|
# since both trees hold a same-named skill, could not tell them apart.
|
||||||
|
_target=$(readlink "$_link" 2>/dev/null || true)
|
||||||
|
case "$_target" in
|
||||||
|
"$BAKED_SRC"/*|"$BAKED_SRC") ;; # baked link → ours to replace
|
||||||
|
*) continue ;; # user/foreign target → leave alone
|
||||||
|
esac
|
||||||
|
|
||||||
|
# -n so an existing symlink-to-directory is replaced rather than followed
|
||||||
|
# (without it, ln would create $_link/$_name inside the baked tree).
|
||||||
|
if ln -sfn "$_live" "$_link" 2>/dev/null; then
|
||||||
|
printf 'skill %s: baked snapshot -> live skillset (%s)\n' "$_name" "$_live"
|
||||||
|
fi
|
||||||
|
done < "$OWNED_LIST"
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Vendored fallback skills
|
# Vendored fallback skills
|
||||||
|
|
||||||
Most directories here are **image-baked skills** that `entrypoint-user.sh`
|
Most directories here are **image-baked skills** that `entrypoint-user.sh`
|
||||||
symlinks into `~/.agents/skills/` on container start (only when a skill of the
|
symlinks into `~/.agents/skills/` on container start. They are the **fallback**
|
||||||
same name is not already present, so a mounted `skillset` repo or a user
|
layer: see *Runtime precedence* below for which copy actually wins when a
|
||||||
override always wins).
|
`skillset` repo is mounted (through v1.8.4 the answer was "always the baked
|
||||||
|
one", which was a bug).
|
||||||
|
|
||||||
| skill | owner | how it gets here |
|
| skill | owner | how it gets here |
|
||||||
|-------|-------|------------------|
|
|-------|-------|------------------|
|
||||||
@@ -38,6 +39,35 @@ its skill file needed baking.
|
|||||||
*different* skill, `opencode-mempalace-bridge`), so there is no public
|
*different* skill, `opencode-mempalace-bridge`), so there is no public
|
||||||
package source to copy from. This snapshot is refreshed manually per release.
|
package source to copy from. This snapshot is refreshed manually per release.
|
||||||
|
|
||||||
|
## Runtime precedence (v1.8.5+)
|
||||||
|
|
||||||
|
The baked links are created **early** in `entrypoint-user.sh` (before pi-deploy,
|
||||||
|
to close a smoke readiness race) with a create-only-when-absent guard, and the
|
||||||
|
skillset deploy runs **last** and treats them as foreign links. Through v1.8.4
|
||||||
|
that combination meant the baked snapshot always won: an edit pushed to
|
||||||
|
`skillset/skills/mempalace/SKILL.md` was invisible in every container until the
|
||||||
|
next image build (measured on two hosts — live `md5 129bcc4752` vs baked
|
||||||
|
`5236024fef`, new section absent). Editing those skills *appeared* to work.
|
||||||
|
|
||||||
|
`devbox-skill-reconcile` now runs immediately after the skillset deploy and
|
||||||
|
repoints the links for skills the **skillset owns**, listed one per line in
|
||||||
|
`skillset-owned.txt`. Precedence, highest first:
|
||||||
|
|
||||||
|
1. **user override** — a real directory, or a symlink pointing outside the baked
|
||||||
|
tree; never touched by anything
|
||||||
|
2. **live skillset clone** — but only for names in `skillset-owned.txt`
|
||||||
|
3. **baked snapshot** — everything else, and every skill when no skillset is
|
||||||
|
mounted
|
||||||
|
|
||||||
|
Ownership is per-skill on purpose: `pi-extensions`' authoritative source is the
|
||||||
|
package repo (copied over the snapshot at build), and `skillset` carries a
|
||||||
|
downstream copy that can lag, so handing it to the clone would *regress* the
|
||||||
|
skill. Only `mempalace` is skillset-owned today.
|
||||||
|
|
||||||
|
Verify with `readlink -f ~/.agents/skills/<skill>` — not by reading the
|
||||||
|
entrypoint. Smoke covers both directions (baked resolution with no skillset
|
||||||
|
mounted, plus a fabricated-skillset run of the reconciler).
|
||||||
|
|
||||||
## Refreshing the snapshots
|
## Refreshing the snapshots
|
||||||
|
|
||||||
cp <pi-extensions-pkg>/skill/SKILL.md pi-extensions/SKILL.md
|
cp <pi-extensions-pkg>/skill/SKILL.md pi-extensions/SKILL.md
|
||||||
@@ -50,4 +80,9 @@ also carries a copy, but it is a downstream duplicate and can lag), and
|
|||||||
`mempalace` from `skillset`. Copying `pi-extensions` from `skillset` would
|
`mempalace` from `skillset`. Copying `pi-extensions` from `skillset` would
|
||||||
regress the snapshot to whatever that repo last mirrored.
|
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 `670f7f1`, pi-extensions pkg `e73cb9f`.
|
||||||
|
|
||||||
|
When you refresh the `mempalace` snapshot, also update the phrase asserted by
|
||||||
|
the "mempalace skill snapshot is current" smoke test — it deliberately pins the
|
||||||
|
**newest** section, because the previous canary grepped a phrase that survived
|
||||||
|
the very edit that made the snapshot stale, and so passed on stale content.
|
||||||
|
|||||||
@@ -275,18 +275,30 @@ Wings are top-level categories, typically one per project or domain:
|
|||||||
- Named after the project directory (e.g., `cli_utils`, `opencode_devbox`)
|
- Named after the project directory (e.g., `cli_utils`, `opencode_devbox`)
|
||||||
- Agent diaries live in `wing_<agent_name>` (e.g., `wing_orchestrator`, `wing_pi`)
|
- 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:
|
- **`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
|
- `pi_<uuid>.jsonl` → pi session
|
||||||
- `<slug>_ses_<id>.jsonl` → opencode 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.
|
- 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.
|
- **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.
|
- **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.
|
||||||
|
- **Attribute what you file yourself.** Drawers now carry `device` and `agent_kind` metadata (plus `device_source`/`agent_kind_source` recording *how* each was determined, so an inference is never mistaken for a fact). Mined content gets these for free — the inbox path gives the device, the filename shape gives the harness — and a timer on the palace host re-stamps hourly, because live re-mining replaces metadata rows and silently drops earlier stamps. But for anything **you** file by hand, the only signal is what you pass: set `added_by="<harness>@<device>"` (e.g. `pi@emb-7kj4vr4g`, from `$MEMPALACE_PI_DEVICE`) on `add_drawer`/`checkpoint`/`mine`. Skip it and your drawer joins the ~16k historic `/workspace` project mines that are permanently unattributable, because `/workspace` exists identically on every devbox. Note the palace preserves `source_file` in full (see `source_path`) but *displays* only the basename — so a device prefix there survives storage even though it looks stripped.
|
||||||
|
- **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
|
||||||
|
|
||||||
Rooms are aspects within a wing:
|
Rooms are aspects within a wing:
|
||||||
@@ -324,3 +336,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 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 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 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.
|
||||||
|
|||||||
@@ -130,6 +130,36 @@ are "command not found" there — you must spell out the underlying command.
|
|||||||
If a command "works in my terminal but not when the agent runs it," this alias
|
If a command "works in my terminal but not when the agent runs it," this alias
|
||||||
gap is the first thing to suspect.
|
gap is the first thing to suspect.
|
||||||
|
|
||||||
|
### A negative result is usually your own filter
|
||||||
|
|
||||||
|
**When you are about to report that something is absent, unreachable, or not
|
||||||
|
running, the filter you wrote is the prime suspect — not the thing.** This
|
||||||
|
environment produces false negatives cheaply, and they are convincing because
|
||||||
|
the command "succeeded". Three real instances from one session, all wrong, all
|
||||||
|
mine:
|
||||||
|
|
||||||
|
| Claim I made | Why it was false |
|
||||||
|
|---|---|
|
||||||
|
| "`tor-ms22` is not in the SSH config" | `grep … \| head -20` — the entry was at **line 454**. `~/.ssh/config` here is ~500 lines. |
|
||||||
|
| "the Docker host has no `docker`" | non-interactive SSH `PATH` lacks `/usr/local/bin` (§2, §3). It was at `/usr/local/bin/docker`. |
|
||||||
|
| "no ControlMaster is running" | pattern `ssh ` (trailing space) cannot match a master: those processes **rename themselves** to `ssh: <controlpath> [mux]`. |
|
||||||
|
|
||||||
|
Habits that would have caught all three:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# don't cap the output of a search whose answer you don't already know
|
||||||
|
grep -n -i -A6 'tor-ms22' ~/.ssh/config # not | head -20
|
||||||
|
|
||||||
|
# on the host, resolve the binary instead of trusting PATH
|
||||||
|
ssh -F "$HOME/.ssh-local/config" mac 'command -v docker || ls /usr/local/bin/docker'
|
||||||
|
|
||||||
|
# match a process's ACTUAL argv, not the name you imagine
|
||||||
|
ps -eo pid,etime,args | grep -Ei 'mux|mosh|ssh'
|
||||||
|
```
|
||||||
|
|
||||||
|
A positive result needs no such scepticism — it carries its own evidence. Only
|
||||||
|
absence has to be *earned*, so spend the extra command there.
|
||||||
|
|
||||||
**`dscp`/`scp` with accented filenames on a macOS host.** macOS stores filenames
|
**`dscp`/`scp` with accented filenames on a macOS host.** macOS stores filenames
|
||||||
in Unicode **NFD** (decomposed — e.g. `ä` is `a` + combining U+0308), while the
|
in Unicode **NFD** (decomposed — e.g. `ä` is `a` + combining U+0308), while the
|
||||||
string you type or paste is usually **NFC** (precomposed `ä`, U+00E4). The bytes
|
string you type or paste is usually **NFC** (precomposed `ä`, U+00E4). The bytes
|
||||||
@@ -175,6 +205,23 @@ Two related mechanisms (don't reinvent them):
|
|||||||
CGNAT per-destination flow caps on residential ISPs. If `~/.ssh/config` pins
|
CGNAT per-destination flow caps on residential ISPs. If `~/.ssh/config` pins
|
||||||
a `ControlPath` under the read-only `~/.ssh`, override with
|
a `ControlPath` under the read-only `~/.ssh`, override with
|
||||||
`-o ControlPath=none` (or use the sidecar, which already redirects it).
|
`-o ControlPath=none` (or use the sidecar, which already redirects it).
|
||||||
|
- **A live master socket MASKS auth and config changes on the far end.** Once
|
||||||
|
`~/.ssh-local/cm/<user>@<host>:22` exists, later commands ride it and
|
||||||
|
authenticate **not at all** — so after editing remote `authorized_keys`,
|
||||||
|
`sshd_config`, host keys, or firewall rules, "it still works" proves nothing.
|
||||||
|
A corrupted `authorized_keys` then bites on the next *cold* connect, likely in
|
||||||
|
a future session with no memory of the edit. Prove it immediately instead:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh -F "$HOME/.ssh-local/config" -O check <host> # 'Master running (pid=N)'
|
||||||
|
ssh -F "$HOME/.ssh-local/config" -o ControlPath=none -o ControlMaster=no \
|
||||||
|
-o BatchMode=yes <host> 'echo COLD AUTH OK'
|
||||||
|
```
|
||||||
|
|
||||||
|
To attribute a socket rather than guess whose it is: `ps -p <pid> -o
|
||||||
|
pid,ppid,lstart,etime,args`. A `mosh` the *user* started on the host
|
||||||
|
bootstraps with the **host's** `~/.ssh/cm/` and is invisible from in here;
|
||||||
|
only a mosh started *inside* the container shares `~/.ssh-local/cm/`.
|
||||||
- **`pi --ssh <host>`** rewires pi's own read/write/edit/bash tools to run on a
|
- **`pi --ssh <host>`** rewires pi's own read/write/edit/bash tools to run on a
|
||||||
remote host; it has its own writable-socket fallback. See the `pi-extensions`
|
remote host; it has its own writable-socket fallback. See the `pi-extensions`
|
||||||
skill for that path.
|
skill for that path.
|
||||||
@@ -257,6 +304,10 @@ hardcode. Details are in the `mempalace` skill.
|
|||||||
- [ ] Writing durable output? → `/workspace`, not the ephemeral layer.
|
- [ ] Writing durable output? → `/workspace`, not the ephemeral layer.
|
||||||
- [ ] Using `dssh`/`dscp`/`ll` in the bash tool? → spell out the real command.
|
- [ ] Using `dssh`/`dscp`/`ll` in the bash tool? → spell out the real command.
|
||||||
- [ ] Assuming a hostname / domain / nameserver / host OS? → stop, detect it.
|
- [ ] Assuming a hostname / domain / nameserver / host OS? → stop, detect it.
|
||||||
|
- [ ] About to report something **absent / unreachable / not running**? → re-run
|
||||||
|
without your own `head`/pattern/`PATH` assumptions first (§2).
|
||||||
|
- [ ] Changed remote `authorized_keys` / `sshd_config`? → prove it with a **cold**
|
||||||
|
connect; a live master socket hides breakage (§3).
|
||||||
- [ ] "Resolves but won't connect"? → check route *and* DNS (§3 + §4).
|
- [ ] "Resolves but won't connect"? → check route *and* DNS (§3 + §4).
|
||||||
- [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged.
|
- [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged.
|
||||||
- [ ] Editing a skill? → `readlink -f ~/.agents/skills/<name>` first (§1).
|
- [ ] Editing a skill? → `readlink -f ~/.agents/skills/<name>` first (§1).
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Skills in this directory whose OWNER is the skillset repo.
|
||||||
|
#
|
||||||
|
# Read by devbox-skill-reconcile, which runs after the skillset deploy in
|
||||||
|
# entrypoint-user.sh: for each name below, if the mounted skillset ships a
|
||||||
|
# skill of that name, the baked link in ~/.agents/skills/ is repointed at the
|
||||||
|
# live clone. The baked copy remains the fallback for containers started
|
||||||
|
# WITHOUT a skillset mount, and a user override always wins over both.
|
||||||
|
#
|
||||||
|
# Add a name here ONLY if the skillset repo is the authoritative source (see
|
||||||
|
# the ownership table in VENDORED.md). Do NOT add:
|
||||||
|
# pi-devbox-environment — authored in this repo; baked IS canonical
|
||||||
|
# pi-extensions — owned by the pi-extensions package repo and copied
|
||||||
|
# over the snapshot at build time; the skillset copy
|
||||||
|
# is a downstream duplicate that can lag, so letting
|
||||||
|
# it win would regress the skill.
|
||||||
|
mempalace
|
||||||
@@ -235,6 +235,16 @@ run "image-baked mempalace fallback skill" \
|
|||||||
# baked copy must be the fresh package copy (Option 1), not the stale snapshot.
|
# baked copy must be the fresh package copy (Option 1), not the stale snapshot.
|
||||||
run "pi-extensions skill refreshed from package when present" \
|
run "pi-extensions skill refreshed from package when present" \
|
||||||
"if [ -f /opt/pi-extensions/skill/SKILL.md ]; then cmp -s /opt/pi-extensions/skill/SKILL.md /usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md; else true; fi"
|
"if [ -f /opt/pi-extensions/skill/SKILL.md ]; then cmp -s /opt/pi-extensions/skill/SKILL.md /usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md; else true; fi"
|
||||||
|
# Runtime ownership handover (v1.8.5): the baked links are a FALLBACK, and
|
||||||
|
# skillset-OWNED skills must be repointed at the live clone when one is mounted.
|
||||||
|
# The list is data, so assert its content, not just its presence: mempalace in,
|
||||||
|
# pi-extensions deliberately out (its skillset copy is a lagging duplicate).
|
||||||
|
run "devbox-skill-reconcile helper present + executable" \
|
||||||
|
"test -x /usr/local/bin/devbox-skill-reconcile"
|
||||||
|
run "skillset-owned list ships and names mempalace" \
|
||||||
|
"grep -qx 'mempalace' /usr/local/share/pi-devbox/skills/skillset-owned.txt"
|
||||||
|
run "skillset-owned list excludes pi-extensions (ownership)" \
|
||||||
|
"! grep -qx 'pi-extensions' /usr/local/share/pi-devbox/skills/skillset-owned.txt"
|
||||||
|
|
||||||
# ── tmux 0-indexing (required for pi-studio variants) ─────────────────
|
# ── tmux 0-indexing (required for pi-studio variants) ─────────────────
|
||||||
echo ""
|
echo ""
|
||||||
@@ -368,6 +378,54 @@ 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-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 "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'
|
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). Through v1.8.4 it also
|
||||||
|
# silently SHADOWED the live skillset copy, so staleness was invisible — and the
|
||||||
|
# canary that was supposed to catch it could not: it grepped "Shared palace:
|
||||||
|
# multiple harnesses", a phrase present in BOTH the stale and the fresh copy.
|
||||||
|
# A snapshot canary must pin the NEWEST section, so update this string whenever
|
||||||
|
# the snapshot is refreshed — that is the point of it.
|
||||||
|
exec_test "mempalace skill snapshot is current" 'grep -q "Attribute what you file yourself" $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
||||||
|
# Link TARGETS, not just link existence: with no skillset mounted (as here) the
|
||||||
|
# baked tree must be what resolves, for all three vendored skills.
|
||||||
|
exec_test "vendored skills resolve to the baked tree (no skillset mounted)" \
|
||||||
|
'for s in mempalace pi-extensions pi-devbox-environment; do
|
||||||
|
case "$(readlink -f $HOME/.agents/skills/$s)" in
|
||||||
|
/usr/local/share/pi-devbox/skills/$s) ;;
|
||||||
|
*) echo "$s resolves to $(readlink -f $HOME/.agents/skills/$s)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done; echo ok'
|
||||||
|
# The handover path itself. CI never mounts a skillset, so without this the
|
||||||
|
# v1.8.5 fix would ship untested: fabricate a skillset + a skills dir holding
|
||||||
|
# baked-style links, run the reconciler, and assert all three outcomes —
|
||||||
|
# owned skill repointed, unowned skill left baked, user override untouched.
|
||||||
|
exec_test "reconciler: owned skill handed to live clone, others untouched" \
|
||||||
|
'set -e; t=$(mktemp -d); mkdir -p $t/ss/skills/mempalace $t/ss/skills/pi-extensions $t/skills
|
||||||
|
echo LIVE > $t/ss/skills/mempalace/SKILL.md; echo LIVE > $t/ss/skills/pi-extensions/SKILL.md
|
||||||
|
ln -s /usr/local/share/pi-devbox/skills/mempalace $t/skills/mempalace
|
||||||
|
ln -s /usr/local/share/pi-devbox/skills/pi-extensions $t/skills/pi-extensions
|
||||||
|
mkdir -p $t/skills/mine; echo MINE > $t/skills/mine/SKILL.md
|
||||||
|
devbox-skill-reconcile $t/ss $t/skills >/dev/null
|
||||||
|
devbox-skill-reconcile $t/ss $t/skills >/dev/null # idempotent
|
||||||
|
[ "$(readlink $t/skills/mempalace)" = "$t/ss/skills/mempalace" ] || { echo "owned skill NOT repointed" >&2; exit 1; }
|
||||||
|
[ "$(readlink $t/skills/pi-extensions)" = /usr/local/share/pi-devbox/skills/pi-extensions ] || { echo "unowned skill was repointed" >&2; exit 1; }
|
||||||
|
[ "$(cat $t/skills/mine/SKILL.md)" = MINE ] || { echo "user override clobbered" >&2; exit 1; }
|
||||||
|
rm -rf $t; echo ok'
|
||||||
|
# The case above cannot fail if the reconciler stops checking WHERE a link
|
||||||
|
# points — a mutation test showed all three of its assertions still passing with
|
||||||
|
# that guard deleted, which is the same false-green shape as the old snapshot
|
||||||
|
# canary. This one discriminates: an OWNED name (so it is considered) whose link
|
||||||
|
# is a user override pointing outside the baked tree (so it must be left alone).
|
||||||
|
exec_test "reconciler: user override on an owned name is left alone" \
|
||||||
|
'set -e; t=$(mktemp -d); mkdir -p $t/ss/skills/mempalace $t/skills $t/mine-skill
|
||||||
|
echo LIVE > $t/ss/skills/mempalace/SKILL.md; echo USERLINK > $t/mine-skill/SKILL.md
|
||||||
|
ln -sfn $t/mine-skill $t/skills/mempalace
|
||||||
|
devbox-skill-reconcile $t/ss $t/skills >/dev/null
|
||||||
|
[ "$(cat $t/skills/mempalace/SKILL.md)" = USERLINK ] || { echo "user symlink override clobbered" >&2; exit 1; }
|
||||||
|
rm -rf $t; 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-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
||||||
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
||||||
|
|||||||
Reference in New Issue
Block a user