7 Commits

Author SHA1 Message Date
pi 314c3767a8 release: v1.1.1 — pi 0.79.3 + mempalace-mcp hang fix
Publish Docker Image / base-decide (push) Successful in 9s
Publish Docker Image / resolve-versions (push) Successful in 12s
Publish Docker Image / build-base (push) Successful in 33m29s
Publish Docker Image / smoke (push) Successful in 3m20s
Publish Docker Image / smoke-studio (push) Successful in 3m38s
Publish Docker Image / build-variant (push) Successful in 15m27s
Publish Docker Image / promote-base-latest (push) Successful in 8s
Publish Docker Image / update-description (push) Successful in 11s
Publish Docker Image / build-variant-studio (push) Successful in 16m51s
2026-06-13 23:59:25 +02:00
pi 05e88c5c75 fix: mempalace-mcp uninterruptible hang resolved via toolkit ext timeout
The per-request timeout + stall-kill landed in mempalace-toolkit's
mempalace.ts pi extension (commit a3b8829), which the base clones at
build via MEMPALACE_TOOLKIT_REF=main. A base rebuild picks it up.

- CHANGELOG: move from 'Known issues' to 'Fixed'; document the env knobs
  (MEMPALACE_MCP_TIMEOUT_MS / MEMPALACE_MCP_INIT_TIMEOUT_MS) and why the
  standalone stdio-watchdog shim was dropped.
- Dockerfile.base: replace the TODO with a note pointing at the fix.
2026-06-13 23:49:36 +02:00
pi 7f67c36a1c docs: capture mempalace-mcp uninterruptible-hang diagnosis (2026-06-13)
Symptom: pi TUI blocks on a mempalace tool call, ESC does not abort.
Initial WAL-contention hypothesis ruled out (no other writer running).
Likely cause: virtiofs cold open of chroma.sqlite3 stalls the JSON-RPC
initialize handshake; pi has no per-call MCP timeout.

Recovery today: docker exec <ctr> pkill -9 -f mempalace-mcp, restart pi.

Planned fix (deferred until after opencode-devbox pi removal): stdio
watchdog shim with per-REQUEST timeout. A naive process-lifetime
timeout wrapper is wrong because mempalace-mcp is long-lived.

Sharing the palace across harnesses remains the goal.
2026-06-13 16:18:45 +02:00
pi ab5ff8ec56 feat: bundle dot-watch helper for live graphviz .dot -> PNG re-render in Studio
pi-studio renders Mermaid natively but has no DOT renderer. Its markdown
preview displays local PNG/JPG/GIF/WEBP images, so dot-watch closes the
loop for Graphviz: edit .dot -> auto-render <name>.png -> Studio
refresh-from-disk shows the update. Uses mtime polling (no inotify dep).

- rootfs/usr/local/bin/dot-watch: the helper (executable)
- Dockerfile.base: COPY + chmod, following the studio-expose pattern
- README.md: 'Graphviz diagrams in Studio' subsection
- CHANGELOG.md: Unreleased entry

graphviz was already in the base image; no new package.
2026-06-11 16:25:27 +02:00
pi 421558477d docs(studio): add commented studio ports + STUDIO_EXPOSE to basic-shape compose 2026-06-11 13:20:44 +02:00
pi b655faab9f docs(studio): render network-hop figure as mermaid flowchart 2026-06-11 11:25:23 +02:00
pi 3b0335f34e docs(studio): clarify studio-expose foreground + token, add remote/mosh end-to-end recipe 2026-06-11 11:23:15 +02:00
4 changed files with 208 additions and 2 deletions
+58 -1
View File
@@ -11,7 +11,64 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
--- ---
## Unreleased ## v1.1.1 — 2026-06-13
Patch release: pi `0.79.1``0.79.3` (auto-resolved at build) plus the
mempalace-mcp hang fix below.
### Fixed
- **`mempalace-mcp` no longer hangs the pi TUI uninterruptibly.** When
the palace is bind-mounted from the macOS host (OrbStack virtiofs) and
the container opened a large `chroma.sqlite3` for the first time, a
cold storage open / HNSW load could stall the server before it emitted
its JSON-RPC response. The awaiting promise then hung forever and the
TUI froze — ESC cancels the LLM stream, not a pending MCP tool call, so
there was no way out short of `docker exec <container> pkill -9 -f
mempalace-mcp` and restarting pi.
The fix lives in the `mempalace.ts` pi extension shipped by
**mempalace-toolkit** (cloned into the base at build time via
`MEMPALACE_TOOLKIT_REF`, default `main`): the JSON-RPC client now arms
a **per-request** timeout. On expiry it rejects the request *and* kills
the stalled child (SIGTERM→SIGKILL), so pi surfaces an error instead of
hanging; the bridge then marks itself unavailable so subsequent calls
fail fast (restart pi to retry). This is deliberately per-REQUEST, not
a process-lifetime `timeout 60 mempalace-mcp` wrapper — the long-lived
server is only killed when a request genuinely stalls.
Tunables (env): `MEMPALACE_MCP_TIMEOUT_MS` (tool-call timeout, default
`60000`), `MEMPALACE_MCP_INIT_TIMEOUT_MS` (initialize/tools-list
handshake, default `120000`); set either to `0` to disable. Requires a
base rebuild to pull the updated extension. The earlier plan of a
standalone Python stdio-watchdog shim was dropped: the extension
already owns request/response correlation, so a separate
framing-reparsing shim is unnecessary.
Still open (out of scope here): sharing one palace across harnesses
ideally wants a single host-side `mempalace-mcp` daemon multiplexing
stdio over a UNIX socket, so all clients share one writer on native
APFS rather than each cold-opening over virtiofs.
`mempalace-mcp` that applies a per-request timeout and kills the child
on stall, **without** killing the long-lived server itself (a naive
`timeout 60 mempalace-mcp` wrapper is wrong — it kills the server
mid-session). Sharing the palace across harnesses (native pi, container
pi, opencode) remains the goal — isolated palaces defeat the point.
Longer term: run a single mempalace-mcp daemon on the host and
multiplex stdio over a UNIX socket so all clients share one writer on
native APFS.
### Added
- **`dot-watch` helper** (`/usr/local/bin/dot-watch`) — auto-rerenders a
Graphviz `.dot` file to PNG on every save via mtime polling (no
`inotify` dependency). pi-studio renders Mermaid natively but has no
DOT renderer; since its markdown preview displays local PNG/JPG/GIF/WEBP
images, this closes the loop for Graphviz: edit `.dot``dot-watch`
regenerates `<name>.png` → Studio *refresh-from-disk* shows the update.
`graphviz` was already in the base image, so no new package. Baked into
`Dockerfile.base` following the `studio-expose` pattern; documented in
the README Studio section.
## v1.1.0 — 2026-06-10 ## v1.1.0 — 2026-06-10
+14
View File
@@ -48,6 +48,8 @@ ENV DEBIAN_FRONTEND=noninteractive
# preview/export pipelines and broadly useful for any # preview/export pipelines and broadly useful for any
# agent-driven document workflow. ~200 MB. # agent-driven document workflow. ~200 MB.
# graphviz — `dot` rendering for many diagram tools. ~10 MB. # graphviz — `dot` rendering for many diagram tools. ~10 MB.
# See the bundled `dot-watch` helper for live .dot -> PNG
# re-render (handy with pi-studio's image preview).
# imagemagick — image conversion / resizing for thumbnails, etc. ~50 MB. # imagemagick — image conversion / resizing for thumbnails, etc. ~50 MB.
# yq — YAML-aware companion to jq. # yq — YAML-aware companion to jq.
# socat — TCP relay. Powers `studio-expose`, which bridges # socat — TCP relay. Powers `studio-expose`, which bridges
@@ -277,6 +279,16 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
# Provides semantic search over conversation history via 29 MCP tools. # Provides semantic search over conversation history via 29 MCP tools.
# Always installed in the base. Set INSTALL_MEMPALACE=false at base-build # Always installed in the base. Set INSTALL_MEMPALACE=false at base-build
# time to shave ~300 MB. # time to shave ~300 MB.
#
# Stall protection (fixed 2026-06-13): mempalace-mcp is launched by the
# `mempalace.ts` pi extension from mempalace-toolkit (cloned below). That
# extension now applies a per-REQUEST timeout in its JSON-RPC client and
# kills the child on stall, so a virtiofs cold-open of chroma.sqlite3 /
# HNSW load can no longer hang the pi TUI uninterruptibly. Tunables:
# MEMPALACE_MCP_TIMEOUT_MS (default 60000), MEMPALACE_MCP_INIT_TIMEOUT_MS
# (default 120000); 0 disables. A standalone stdio-watchdog shim is NOT
# needed — the extension already owns request/response correlation. See
# CHANGELOG.md "Unreleased > Fixed".
ARG INSTALL_MEMPALACE=true ARG INSTALL_MEMPALACE=true
# Pin to a known-good version. Bump deliberately, not implicitly: an # Pin to a known-good version. Bump deliberately, not implicitly: an
# unpinned install silently swept in mempalace 3.3.x/3.4.0 with a broken # unpinned install silently swept in mempalace 3.3.x/3.4.0 with a broken
@@ -436,10 +448,12 @@ COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
# ── Entrypoint ──────────────────────────────────────────────────────── # ── Entrypoint ────────────────────────────────────────────────────────
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/ COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/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 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/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
+77 -1
View File
@@ -199,9 +199,15 @@ With `STUDIO_EXPOSE=1`, the entrypoint starts the bridge for you; just run
(leave `STUDIO_EXPOSE` unset), run `studio-expose` in a container shell: (leave `STUDIO_EXPOSE` unset), run `studio-expose` in a container shell:
```bash ```bash
studio-expose # bridges $STUDIO_PORT (default 8765); --help for details studio-expose & # bridges $STUDIO_PORT (default 8765); --help for details
``` ```
> **`studio-expose` runs in the foreground** (it's a `socat` relay) — it
> blocks the shell until Ctrl-C. Background it with `&` or run it in its
> own tmux pane. It only relays traffic; it does **not** print a token.
> The lines it prints ending in `...token=...` are literal help text, not
> a truncated URL — the real token comes from `/studio` (see below).
> **Security:** the bridge intentionally exposes Studio beyond loopback; > **Security:** the bridge intentionally exposes Studio beyond loopback;
> its tokenized URL is the only auth. Keep the host-side publish on > its tokenized URL is the only auth. Keep the host-side publish on
> `127.0.0.1:` and use `ssh -L` for remote access. Default is **off**. > `127.0.0.1:` and use `ssh -L` for remote access. Default is **off**.
@@ -221,10 +227,75 @@ tunnel alongside mosh (mosh for the shell, ssh for the port), or reach the
host's published port directly over a trusted network (LAN / Tailscale / host's published port directly over a trusted network (LAN / Tailscale /
WireGuard). WireGuard).
#### End-to-end recipe: remote host, mosh shell, `studio-expose` bridge
The full path has four network hops, each added by one step:
```mermaid
flowchart LR
browser["laptop browser"]
host["host :8765"]
eth0["container eth0 :8765"]
loop["container 127.0.0.1 :8765"]
studio["pi-studio"]
browser -->|"ssh -L"| host
host -->|"docker -p"| eth0
eth0 -->|"studio-expose (socat)"| loop
studio -->|"binds"| loop
```
Assuming the compose file publishes `127.0.0.1:8765:8765` (see method B):
1. **In a container shell** — start the bridge (skip if `STUDIO_EXPOSE=1`
is set in compose, which auto-starts it):
```bash
studio-expose &
```
2. **In your pi session** (the pi TUI in the container) — start Studio and
print the tokenized URL. `/studio` is a slash command you type in the
TUI, not a shell command:
```
/studio --no-browser --port 8765
/studio --status # reprint the URL anytime
```
Copy the `http://…:8765/?token=<token>` it prints. **This** is where
the real token comes from — not `studio-expose`.
3. **On your laptop** — open the ssh port-forward alongside mosh:
```bash
ssh -L 8765:127.0.0.1:8765 user@docker-host
```
4. **In your laptop browser** — open `http://127.0.0.1:8765/?token=<token>`
(keep the port and token verbatim; only the host part is `127.0.0.1`).
> **Order check:** nothing listens on the container's `127.0.0.1:8765`
> until step 2 runs. If the browser can't connect, verify Studio is up
> (`/studio --status`) and the bridge is running (`ps aux | grep socat`).
> PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine, > PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine,
> which is **not** in `-studio` (only the planned `-studio-tex`). HTML > which is **not** in `-studio` (only the planned `-studio-tex`). HTML
> export, KaTeX, Mermaid, and all REPL features work without it. > export, KaTeX, Mermaid, and all REPL features work without it.
### Graphviz diagrams in Studio: `dot-watch`
pi-studio renders **Mermaid** natively but has **no Graphviz/DOT renderer**.
Its markdown preview *does* render local image links (`.png`/`.jpg`/`.gif`/
`.webp`), so the workflow for Graphviz is: write a `.dot` file, render it to
PNG with `dot`, and preview the PNG (directly, or embedded in a markdown
file). The bundled **`dot-watch`** helper automates the re-render so edits
show up on Studio's *refresh-from-disk*:
```bash
dot-watch graph.dot # dot engine, 150 dpi -> graph.png
dot-watch graph.dot neato 200 # pick layout engine + dpi
```
It polls the file's mtime (no `inotify` dependency) and regenerates
`<name>.png` on every save, printing timestamped status and indenting any
DOT syntax errors instead of crashing. Then in Studio: open the PNG (or a
`.md` that embeds it) and hit **refresh-from-disk** after each edit.
Note: SVG is **not** in Studio's local-image-link allowlist — use PNG.
## docker-compose.yml — basic shape ## docker-compose.yml — basic shape
```yaml ```yaml
@@ -236,10 +307,15 @@ services:
container_name: pi-devbox container_name: pi-devbox
stdin_open: true stdin_open: true
tty: true tty: true
# pi-studio (only on `-studio` images): publish loopback + enable the
# socat bridge so the browser UI is reachable. See "Using pi-studio".
# ports:
# - "127.0.0.1:8765:8765" # host-localhost only; use ssh -L for remote
env_file: env_file:
- .env - .env
environment: environment:
- TERM=xterm-256color - TERM=xterm-256color
# - STUDIO_EXPOSE=1 # -studio only: auto-start the socat bridge on boot
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-} - GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
- GITEA_HOST=${GITEA_HOST:-} - GITEA_HOST=${GITEA_HOST:-}
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-} - GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# dot-watch — auto-rerender a graphviz .dot file to PNG on every save.
#
# WHY THIS EXISTS
# pi-studio renders mermaid natively but has no graphviz/DOT renderer.
# Its markdown preview DOES render local image links (.png/.jpg/.gif/.webp),
# and the editor offers "refresh from disk". This helper closes the loop:
# edit a .dot file -> dot-watch regenerates <name>.png -> hit refresh in
# Studio to see the update. Uses mtime polling (no inotify dependency,
# which isn't in the trixie-slim base).
#
# USAGE
# dot-watch <file.dot> [layout] [dpi]
# layout: dot|neato|fdp|circo|twopi (default: dot)
# dpi: output resolution (default: 150)
# env: DOT_WATCH_INTERVAL=<seconds> poll interval (default: 1)
#
# EXAMPLES
# dot-watch /workspace/graph.dot
# dot-watch graph.dot neato 200
set -euo pipefail
SRC="${1:?usage: dot-watch <file.dot> [layout] [dpi]}"
LAYOUT="${2:-dot}"
DPI="${3:-150}"
[[ -f "$SRC" ]] || { echo "error: no such file: $SRC" >&2; exit 1; }
command -v "$LAYOUT" >/dev/null || { echo "error: layout engine '$LAYOUT' not found" >&2; exit 1; }
OUT="${SRC%.dot}.png"
INTERVAL="${DOT_WATCH_INTERVAL:-1}" # seconds between polls
ERRLOG="$(mktemp -t dot-watch.XXXXXX.err)"
trap 'rm -f "$ERRLOG"' EXIT
render() {
if "$LAYOUT" -Tpng -Gdpi="$DPI" "$SRC" -o "$OUT" 2> "$ERRLOG"; then
printf '[%s] rendered -> %s\n' "$(date +%H:%M:%S)" "$OUT"
else
printf '[%s] DOT error:\n' "$(date +%H:%M:%S)"
sed 's/^/ /' "$ERRLOG"
fi
}
# portable mtime (GNU stat, fallback to BSD stat)
mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
echo "watching $SRC ($LAYOUT, ${DPI}dpi) -> $OUT [Ctrl-C to stop]"
render
last="$(mtime "$SRC")"
while true; do
sleep "$INTERVAL"
[[ -f "$SRC" ]] || continue
now="$(mtime "$SRC")"
if [[ "$now" != "$last" ]]; then
last="$now"
render
fi
done