Files
pi-devbox/rootfs/usr/local/share/pi-devbox/skills/mempalace/SKILL.md
T
pi dbb78798fb
Lint / hadolint (push) Successful in 9s
Lint / actionlint (push) Successful in 40s
vendor: resync mempalace skill snapshot to skillset c04cd15
c04cd15 ('the withdrawal only holds where the bridge is live') landed after the
v1.8.7 snapshot was taken, so the baked fallback was already 4 lines behind the
skillset within hours of publishing. It adds the caveat this fleet is currently
living in: the bridge is baked at image build, so a container on an image older
than the stamping commit satisfies both env gates while stamping nothing, and
hand-stamping is still the only signal a hand-filed drawer gets there. It also
gives the one-line test —
  grep -c MEMPALACE_PI_DEVICE "$(readlink -f ~/.pi/agent/extensions/mempalace.ts)"
which returns 0 on this v1.8.6 container, confirming the gap empirically.

Note what this instance proves about the canary fixed one commit ago: it still
PASSES on the refreshed copy, because both pinned phrases survived the edit. A
phrase canary cannot detect 'older than skillset main' — only a diff can. This is
the second drift in 24h and is the argument for the Still-open item (a CI job
diffing this file against the skillset repo, blocked on a clone credential for a
private repo). No re-pin was needed here.
2026-08-26 09:41:02 +02:00

26 KiB
Raw Blame History

name, description
name description
mempalace MemPalace agent memory protocol. Use on every session to maintain continuity across conversations — search before answering about past work, write diary entries before session ends, and mine new projects into the palace. Load this skill at session start.

MemPalace Agent Memory Protocol

Overview

MemPalace gives you persistent memory across sessions via an MCP server. It stores project knowledge (mined from files), conversation summaries (diary entries), and entity relationships (knowledge graph). Without this protocol, you have tools but no habits — and memory without habits is just storage.

Core principle: Storage is not memory. Storage + protocol = memory.

When to Load This Skill

  • At the start of every session (proactively, before the user asks)
  • When the user mentions past conversations, decisions, or work
  • When working on a new project or repository for the first time
  • When the user asks about people, projects, or relationships

Session Lifecycle

Phase 1: Wake Up (session start)

Run these immediately when a session begins, before responding to the user:

  1. Load palace overview:

    mempalace_status
    

    This returns wing/room counts, the AAAK spec, and the memory protocol reminder.

  2. Read your recent diary:

    mempalace_diary_read(agent_name="<your_agent_name>", last_n=5)
    

    Scan for context about recent sessions — what was worked on, what matters, what's pending.

  3. Check the knowledge graph for the user or active project if relevant:

    mempalace_kg_query(entity="<project_or_person>")
    

Do NOT announce this to the user. Just do it silently to orient yourself.

Temporal grounding — compute time deltas, don't guess

Diary entries and drawers carry real timestamps (timestamp, created_at). Before describing when something happened — "yesterday", "earlier today", "last week", "a while back" — establish the current date/time first and compute the delta against the actual timestamp. Get "now" from the injected session date or by running date in a shell; never infer it.

A container recreate or a fresh session is NOT a day boundary. A devbox container (pi-devbox or opencode-devbox) is frequently restarted — often several times within the same day — and each restart begins a new session with a fresh wake-up. Do not reason "new session ⇒ last session was yesterday": two diary entries 90 minutes apart can straddle a container recreate. The only authoritative clock is the timestamp on the memory, not the session/container boundary.

Practical rule: prefer explicit, checkable phrasing — e.g. "earlier today, ~8h ago (both 2026-06-25)" — over a vague relative term. If you catch yourself about to write "yesterday" / "last week", subtract now − entry.timestamp and state the computed result. (Remember timestamps may be UTC while the wall clock is local — reconcile the offset before computing the delta.) Note too that session feeders can lag up to a week (see Multi-harness palace), so a recent absence in wing_conversations is not proof nothing happened.

Phase 2: Active Session (during work)

Search Before You Speak

Before answering questions about past work, decisions, people, or projects:

mempalace_search(query="<keywords>", wing="<project>")

Never guess about facts that might be in the palace. Wrong is worse than slow. Say "let me check" and query.

Search Before You Probe

The rule above covers questions. This one covers actions — and it is the one that actually gets skipped, because mid-task the impulse is to go and look rather than to remember. The palace is a fleet record: another machine's agent has usually already paid the cost of discovering how this environment is wired, and its notes include the corrections that came afterwards, which a fresh probe cannot show you.

Before you SSH somewhere to find out how it is set up, enumerate infrastructure, or derive a deployment — search. Concrete triggers, all meaning search first:

  • about to run ssh <host> …, docker ps, systemctl list-units, ip addr to discover how something is deployed or connected
  • about to establish topology: which hosts/runners/services exist, where they live, which of them can reach which
  • about to conclude "this isn't documented anywhere" or "there's no way to know"
  • about to assert an environment fact you learned earlier in this same session

That last trigger is the sharp edge. A compacted session summary is lossy by design, and a belief you formed 40 turns ago may already be retracted in the palace by another machine. Trusting your own context over the shared record is how a withdrawn claim gets re-published as fact.

Search broadly before narrowing — fleet knowledge often sits in another machine's wing, or inside a mined conversation, not where you would file it yourself:

mempalace_search(query="<topic> <host> <mechanism>")     # no wing filter first
mempalace_search(query="…", wing="<likely-wing>")        # then narrow

Two or three searches cost seconds. Re-deriving infrastructure costs minutes and can be wrong: a probe shows one host's present state, while the palace records intent, history, and what was already disproved.

Worked example (real, 2026-08-25). An agent evaluating whether to add an ARM CI runner probed hosts directly instead of searching. It concluded "the runner lives on synlig" — there are four — and that "synlig is on the home LAN" — it is an OpenStack VM with a public floating IP that cannot reach the home LAN at all. Both facts were already in the palace, the second one as an explicit retraction of the very same mistake made weeks earlier. The palace also held the runner labels and the deliberate capacity: 1 setting, which the probe never revealed. Cost: a wrong recommendation written into the palace twice, then corrected twice.

A search that comes back empty is not an answer — least of all about recent work. Semantic search is weakest exactly where the fleet record is freshest: a drawer filed minutes ago is unranked against a keyword-shaped query, and the drawer you most need is by construction the newest one, because the other machine files its release, handoff and correction drawers at the end of its session. So a single miss proves nothing. If the work is 0-2 days old and the first search looks stale or empty, enumerate before concluding:

mempalace_list_drawers(wing="<wing>", since="<today>")   # or room=, or no filter
mempalace_diary_read(agent_name="<you>", wing="<wing>")  # the other machine's handoff

Enumeration is exact where embeddings are probabilistic. Treat "I searched and found nothing" as a hypothesis you have not yet tested, and never as licence to go probing.

Worked example (real, 2026-08-25, same fleet as above). An agent asked to orient on an in-flight release did search first — "v1.8.6 release run 579 Docker Hub verification" — and got back only v1.6.4 / v0.78.0 era hits, because the release drawer it needed was 58 seconds old. It accepted the miss and went off to probe Docker Hub and the Gitea API. The user had to prompt "maybe there is a note in mempalace"; list_drawers(wing="pi-devbox", since=<today>) then returned the drawer immediately, along with the diary entry naming the exact open item. The rule above was present and correct in this very file at the time — the failure was not knowing to retry differently after a bad first hit.

Mine New Projects

When working on a new codebase for the first time:

  1. Check if it's already mined:

    mempalace_list_wings
    
  2. Decide what to mine — docs first, code never (by default).

    The palace is for context and intent, not code recall. Code is better read from the working tree via Read/Grep/glob — always authoritative, never stale. Embedding source code produces thousands of low-signal drawers (e.g. def __init__(self, ...) across every class) that pollute search for years.

    Mine by default:

    • *.md, *.rst, *.txt — docs, READMEs, CHANGELOGs, architecture notes
    • AGENTS.md, CLAUDE.md, CONTRIBUTING.md, design/decision docs — highest signal per byte
    • *.sh, Dockerfile, Makefile, entrypoints — small, intent-bearing
    • *.yml, *.yaml, *.toml, selective *.json (docker-compose, pyproject, mkdocs.yml, CI workflows) — skip lockfiles

    Do NOT mine by default:

    • *.py, *.ts, *.tsx, *.js, *.go, *.rs, *.java, *.cpp, *.c, *.rb — raw source code
    • Test files, fixtures, generated code
    • node_modules/, .venv/, __pycache__/, .mypy_cache/, .pytest_cache/, .ruff_cache/ (the miner respects .gitignore but double-check)

    Exception: if a code file is the documentation (e.g. a heavily-commented reference script, or a protocol definition), file it manually via mempalace_add_drawer.

  3. Before mining, inspect the repo to estimate drawer count:

    # Quick audit — what will actually get mined?
    find <dir> -type f \
      -not -path '*/.git/*' -not -path '*/node_modules/*' \
      -not -path '*/.venv/*' -not -path '*/__pycache__/*' \
      \( -name '*.md' -o -name '*.sh' -o -name '*.yml' -o -name '*.yaml' \
         -o -name '*.toml' -o -name 'Dockerfile*' -o -name 'Makefile' \) | wc -l
    

    A docs-heavy repo should produce ~5–10 drawers per file. If a mine produces >15 drawers/file on average, code leaked in — investigate.

  4. Run the mine:

    mempalace init --yes <directory>
    mempalace mine <directory> --agent <your_agent_name>
    

    The miner currently lacks a --docs-only or --exclude-ext flag (as of v3.3.3). Until it does, either:

    • (a) Add a mempalace.yaml at the repo root with explicit include globs, OR
    • (b) Mine everything, then surgically remove code-sourced drawers via SQL on ~/.mempalace/palace/chroma.sqlite3 (delete by embedding_metadata.source_file LIKE '%.py'), followed by mempalace repair --yes.
  5. If the CLI miner misses a file you do want (e.g., .zsh, an undocumented extension), file it manually:

    mempalace_add_drawer(wing="<project>", room="<aspect>", content="<verbatim content>", source_file="<path>")
    
  6. After mining, reconnect to pick up the new embeddings:

    mempalace_reconnect
    

    If search errors occur after mining ("Error finding id"), repair the index:

    mempalace repair --yes
    

Track Facts in the Knowledge Graph

When you learn new facts about people, projects, or relationships:

mempalace_kg_add(subject="ProjectX", predicate="uses", object="PostgreSQL")
mempalace_kg_add(subject="Alice", predicate="owns", object="ProjectX", valid_from="2026-01-15")

When facts change (ended, no longer true):

mempalace_kg_invalidate(subject="Alice", predicate="works_at", object="OldCorp", ended="2026-03-01")

Cross-Reference with Tunnels

When content in one project relates to another, create a tunnel:

mempalace_create_tunnel(
  source_wing="project_api", source_room="endpoints",
  target_wing="project_db", target_room="schema",
  label="API endpoints map to these DB tables"
)

Feeding opencode session history (opencode + mempalace-toolkit only)

MemPalace has no upstream integration with opencode as of v3.3.3 — hooks_cli.py only supports claude-code and codex harnesses. Opencode persists every turn in a local SQLite DB at ~/.local/share/opencode/opencode.db, but nothing moves that data into the palace automatically.

On a machine with opencode + the mempalace-toolkit installed, session history is fed into wing_conversations via mempalace-session — either manually, or on a weekly systemd user timer / cron schedule shipped in mempalace-toolkit/contrib/. If this is missing, opencode conversations exist only in the local SQLite DB and are invisible to mempalace_search.

How to tell if it's set up:

mempalace_list_wings

If wing_conversations exists and has a drawer count comparable to the user's opencode session count, session feeding is working. If it's empty or suspiciously small, suggest:

  1. Check if the toolkit is installed: which mempalace-session.
  2. If installed, suggest running mempalace-session --dry-run to preview and mempalace-session to file.
  3. If not installed, point the user at gitea.jordbo.se/joakimp/mempalace-toolkit for setup.

Don't try to paper over the gap by dumping turn-level content into the palace manually via mempalace_add_drawer — that reinvents what mempalace-session does with normalization and dedup. Use the tool.

Full routine (triggers, cadence, automation) is in the opencode-mempalace-bridge skill and the toolkit's ARCHITECTURE.md §5. The two skills pair: this one (mempalace) covers using the palace; that one (opencode-mempalace-bridge) covers feeding it from opencode.

Phase 3: Wind Down (session end)

Always write a diary entry before the session ends. This is the most important habit.

mempalace_diary_write(
  agent_name="<your_agent_name>",
  entry="<AAAK compressed summary>",
  topic="session-summary"
)

Why still write diaries when sessions may be mined automatically?

On machines running opencode + mempalace-toolkit, every session is mined into wing_conversations on a weekly (or user-defined) schedule. A common and incorrect conclusion: "since every turn is captured automatically, writing a diary entry is redundant." It isn't.

Session mining captures what was said (every turn, verbatim). A diary captures what the session meant — editorial judgment by the agent who lived it:

  • Lessons learned, patterns noticed, pending items rolled forward
  • Meta-observations that were never said aloud during the session
  • Aggregate counts (commits shipped, bugs fixed, hours spent)
  • A compressed, recency-scannable summary for the next agent's wake-up

Mining raw turns cannot surface these because the words don't exist verbatim — they're the agent's reflection at wind-down. Think of the split as release notes (diary) vs. git log with diffs (session mine): a repo keeps both because they answer different questions. So does the palace.

Practical rule: automated mining does not replace Phase 3. Both systems cover each other's failure modes — a skipped diary is recovered from the raw turns; a missed mine is recovered from the diary summary. For the full treatment (comparison table, retrieval patterns, token economics), see mempalace-toolkit/ARCHITECTURE.md §5 → "Diary vs session mine: why keep both?".

AAAK Diary Format

Write diary entries in compressed AAAK format for efficiency. Structure:

SESSION:<date>|<what.you.worked.on>|
TASKS:
1.<task.description>→<outcome>|
2.<task.description>→<outcome>|
DISCOVERED:<unexpected.findings>|
ENTITIES:<people.or.projects.encountered>|
<importance: one to five stars>

Example:

SESSION:2026-04-28|api.refactor+db.migration|
TASKS:
1.refactored.auth.endpoints→split.into.3.modules|
2.added.user.roles.migration→postgres.enum.type|
DISCOVERED:legacy.session.table.unused.since.v2|
ENTITIES:ProjectX;Alice(reviewer)|
***

Rules:

  • Use dots instead of spaces within phrases
  • Use pipes as field separators
  • Use arrows for cause/effect or transitions
  • Stars indicate session importance (one to five)
  • Keep it tight — a future agent should get the gist in seconds

What to Capture

Prioritize recording:

  • Decisions made and their rationale
  • Discoveries — things that surprised you or that a future session needs to know
  • Unfinished work — what's pending, what was deferred
  • User preferences observed during the session
  • Entities encountered — people, projects, tools, services

Phase 4: Fact Updates

If facts changed during the session, update the knowledge graph before writing the diary:

mempalace_kg_invalidate(subject="...", predicate="...", object="...", ended="<today>")
mempalace_kg_add(subject="...", predicate="...", object="...", valid_from="<today>")

Palace Structure

Wings

Wings are top-level categories, typically one per project or domain:

  • Named after the project directory (e.g., cli_utils, opencode_devbox)
  • Agent diaries live in wing_<agent_name> (e.g., wing_orchestrator, wing_pi)

Shared palace: multiple harnesses, and possibly multiple machines

A single palace can be fed by multiple coding-agent harnesses, and — when MEMPALACE_REMOTE_URL points at a central palace — by multiple machines. On this machine the palace is shared between opencode and pi (Mario Zechner's pi-coding-agent). Implications:

  • wing_conversations mixes sources. Both harnesses' session feeders write into the same wing. To tell them apart, look at the source_file metadata on each drawer:
    • pi_<uuid>.jsonl → pi session
    • <slug>_ses_<id>.jsonl → opencode session
    • The first chunk of each session also carries a | source: opencode or | source: pi marker in the synthetic header line.
  • Other wings may belong to other harnesses. For example wing_pi is pi's diary, not opencode's. Don't assume every diary entry was written by you — check agent_name on the entry.
  • Session feeders run on different schedules. Pi sessions are fed Tue 03:00, opencode sessions Mon 03:00 (launchd Weekday: 0/7=Sunday, 1=Monday, 2=Tuesday — misreading this by one day is easy). Recent sessions from either harness can lag the palace by up to a week, so absence-of-evidence in wing_conversations is not evidence-of-absence for recent work.
  • Reading another harness's diary is useful. When orienting after a gap, mempalace_diary_read agent_name=pi (or whichever sibling agent has been active) often gives a fresher picture than waiting for the conversations feeder to catch up.

When the palace is central (shared across machines), these further 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.
  • Provenance is stamped for you — leave it alone. Drawers 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). You do not set these, and you no longer set added_by either: the pi bridge defaults the writer field to <harness>@<device> on add_drawer/checkpoint/mine/event_append/artifact_put, and prefixes diary entries with HOST:<device>|, from host-supplied $MEMPALACE_PI_DEVICE. RFC 001 §7.3.2 ranks "agent stamps it via a skill instruction" as the worst possible place for exactly the reason you would expect — it is per-call boilerplate that gets forgotten, and it did: the agent who wrote the previous version of this bullet then filed its own provenance drawer as added_by=checkpoint. Confirm the bridge in your image actually stamps before trusting it: the extension is baked at image build time, so a container on an image older than the stamping commit (pi-devbox < v1.8.7) stamps nothing while still satisfying both gates — the env vars are set and the code is simply absent. Check with grep -c MEMPALACE_PI_DEVICE "$(readlink -f ~/.pi/agent/extensions/mempalace.ts)"; zero means keep passing added_by="<harness>@<device>" and a manual HOST:<device>| diary prefix until the container is recreated on a newer image. Two things remain yours: pass source_drawer_id on kg_add (triples have no provenance field, so that pointer is the only path back to a device), and pass an explicit added_by only when deliberately filing on behalf of another device. Never invent values for device/agent_kind/origin_device — a fabricated value is worse than a blank, because it silently corrupts a future merge.
  • Metadata is invisible to search — so check the text, not the fields. search results are built from a fixed key list and diary_read returns content, so neither ever shows device/added_by. Only mempalace_get_drawer reveals them. This is why diary entries carry an in-text HOST:<device> marker: it is the only attribution a reader actually sees. A diary entry with no HOST: marker predates the convention and may be from any machine — do not assume it is this one's history.
  • 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 — and note that a container cannot tell you which machine it is on (hostname is a docker hash, $DEVBOX_HOST_ALIAS is generic). $MEMPALACE_PI_DEVICE is the cheap answer; ssh -F ~/.ssh-local/config host hostname is the independent one.
  • One writer, no queue. A concurrent mine returns a structured already-running error rather than waiting its turn, and one large mine can make the palace unresponsive to every client for minutes. After another client's mine, call mempalace_reconnect to see the new drawers. A client-side timeout is not evidence of failure — verify before retrying, or you file a duplicate.

Rooms

Rooms are aspects within a wing:

  • fzf, scripts, configuration, general — whatever the miner detects
  • Diary entries go into rooms by topic tag

Drawers

Drawers hold verbatim content — never summarized, always searchable.

Tunnels

Cross-wing connections linking related content across projects.

Knowledge Graph

Entity-relationship triples with temporal validity. Query with mempalace_kg_query, browse with mempalace_kg_timeline.

Troubleshooting

Problem Fix
"No palace found" Run mempalace init <dir> then mempalace mine <dir>
"Error finding id" after mining Run mempalace repair --yes then mempalace_reconnect
Search returns irrelevant results Use max_distance=1.0 for stricter matching; add wing filter
Miner skips file types File manually with mempalace_add_drawer or use --no-gitignore
Stale results after external changes Call mempalace_reconnect

Anti-Patterns

  • Don't guess when you can search. If a question touches past work, search first.
  • Don't probe what the fleet already knows. Before SSH-ing into a host, enumerating infrastructure, or deriving how something is deployed, search the palace. A probe reveals one host's present state; the palace holds intent, history and prior corrections — including the ones that contradict what you are about to conclude.
  • Don't trust this session's context over the palace. A compacted summary is lossy, and another machine may have corrected the fact since. Verify load-bearing environment claims against the shared record before acting on them.
  • Don't take one empty search as proof the palace is silent. Fresh drawers rank worst, and the drawer that matters is usually the newest one. For anything 0-2 days old, enumerate with mempalace_list_drawers(since=…) and read the other machine's diary before you go and probe.
  • Don't infer elapsed time from session or container boundaries. A restart isn't a new day. Compare the actual timestamp (timestamp / created_at) against the current date/time before saying "yesterday", "last week", etc.
  • Don't skip the diary. A session without a diary entry is a session forgotten.
  • Don't summarize drawer content. File verbatim — the embedding model needs the original words.
  • Don't mine .git directories or node_modules. The CLI miner respects .gitignore by default.
  • Don't create duplicate drawers. Use mempalace_check_duplicate before adding manually.
  • Don't treat the palace as a task list. It's for knowledge and context, not todos.
  • Don't invent provenance metadata, and don't hand-stamp it either. An earlier version of this list told you to set added_by="<harness>@<device>" by hand; that instruction has been withdrawn, because RFC 001 §7.3.2 places provenance at the client/server boundary and the pi bridge now does it uniformly (see Provenance is stamped for you above) — but the withdrawal only holds where the bridge is live, so run the one-line check in that bullet first; on an older image hand-stamping is still the only signal a hand-filed drawer gets. DO NOT invent values for the palace's own metadata fields (device, agent_kind, origin_device): those are stamped by infrastructure that also records how each was determined, and a fabricated value is worse than none because it silently corrupts a future merge. DO pass source_drawer_id on kg_add. And never put a machine name in a diary's agent_name — it becomes the wing name and hides your entries from diary_read.