feat(entrypoint): non-destructively merge new template keys into settings.json

The settings.json bootstrap only fires when the file is ABSENT, so a
settings.json on a preserved named volume never picks up config added in a
later image (e.g. the observational-memory / pi-fork blocks, a newly-enabled
model). Users had to hand-merge after every upgrade.

On start, when settings.json already exists, deep-merge the template into it
with 'jq -s ".[0] * .[1]"' (template first, live second) so the user's values
always win and only MISSING keys are filled from the template. Arrays are
leaves (a model the user removed is not re-added). Rewrites only when the
merge changes something, backs up the original first, and skips safely (no
clobber) if either file is invalid JSON. Opt out with PI_SETTINGS_MERGE=0.

Add a recreate-sanity-check assertion that settings.json carries the
observational-memory + pi-fork blocks after recreate.
This commit is contained in:
Joakim Persson
2026-06-17 20:49:41 +02:00
parent 5c08bfc8a8
commit 41c2c2b716
2 changed files with 41 additions and 3 deletions
+29 -3
View File
@@ -86,9 +86,35 @@ if command -v pi &>/dev/null; then
# Bootstrap settings.json from template if absent (pi rewrites this # Bootstrap settings.json from template if absent (pi rewrites this
# file at runtime — lastChangelogVersion, etc — so we can't symlink it). # file at runtime — lastChangelogVersion, etc — so we can't symlink it).
if [ ! -f "$HOME/.pi/agent/settings.json" ] && \ _pi_settings="$HOME/.pi/agent/settings.json"
[ -f /opt/pi-toolkit/settings.example.json ]; then _pi_template=/opt/pi-toolkit/settings.example.json
cp /opt/pi-toolkit/settings.example.json "$HOME/.pi/agent/settings.json" if [ ! -f "$_pi_settings" ] && [ -f "$_pi_template" ]; then
cp "$_pi_template" "$_pi_settings"
echo "pi settings.json bootstrapped from template"
elif [ -f "$_pi_settings" ] && [ -f "$_pi_template" ] && \
[ "${PI_SETTINGS_MERGE:-1}" != "0" ] && command -v jq >/dev/null 2>&1; then
# Non-destructive merge: a settings.json on a PRESERVED volume never
# otherwise sees new template keys (the bootstrap above only fires when
# the file is absent), so config added in an image upgrade — e.g. the
# observational-memory / pi-fork blocks or a newly-enabled model — never
# reaches existing users. Deep-merge with the template FIRST and the
# live file SECOND ('.[0] * .[1]') so the user's values always win and
# only keys MISSING from the live file are filled in from the template.
# Arrays are treated as leaves (the user's array is kept verbatim, so a
# model they deliberately removed is not re-added). Only rewrite when the
# merge actually changes something, and back up the original first.
# Set PI_SETTINGS_MERGE=0 to disable. Invalid JSON on either side → skip,
# never clobber.
if _pi_merged=$(jq -s '.[0] * .[1]' "$_pi_template" "$_pi_settings" 2>/dev/null); then
if [ -n "$_pi_merged" ] && \
! printf '%s' "$_pi_merged" | jq -e --slurpfile cur "$_pi_settings" '. == $cur[0]' >/dev/null 2>&1; then
cp "$_pi_settings" "${_pi_settings}.bak.$(date +%Y%m%d-%H%M%S)"
printf '%s\n' "$_pi_merged" > "$_pi_settings"
echo "pi settings.json: merged new template keys from settings.example.json (backup saved)"
fi
else
echo "WARN: pi settings.json merge skipped (jq could not parse template or live file; left untouched)"
fi
fi fi
# pi↔mempalace MCP bridge — single extension symlink. # pi↔mempalace MCP bridge — single extension symlink.
+12
View File
@@ -187,6 +187,18 @@ else
fail "~/.pi/agent/settings.json missing" fail "~/.pi/agent/settings.json missing"
fi fi
# settings.json merge: the entrypoint deep-merges new template keys into a
# preserved settings.json on every start, so config added in an image upgrade
# (e.g. the observational-memory / pi-fork blocks) reaches existing volumes.
# Assert those blocks are present and that the file is still valid JSON.
if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.pi/agent/settings.json" ]; then
if jq -e 'has("observational-memory") and has("pi-fork")' "$HOME/.pi/agent/settings.json" >/dev/null 2>&1; then
pass "settings.json has observational-memory + pi-fork blocks (template merge)"
else
fail "settings.json missing observational-memory and/or pi-fork blocks (template merge did not land)"
fi
fi
# pi package registrations (pi install <local-path> → recorded in settings.json) # pi package registrations (pi install <local-path> → recorded in settings.json)
if [ -f "$HOME/.pi/agent/settings.json" ]; then if [ -f "$HOME/.pi/agent/settings.json" ]; then
for pkg in pi-fork pi-observational-memory; do for pkg in pi-fork pi-observational-memory; do