diff --git a/AGENTS.md b/AGENTS.md index 5c1614e..2d66608 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,8 @@ install.sh # Idempotent installer — see "What install.sh does" be ARCHITECTURE.md # Canonical spec: diagrams, setup recipe, ops notes, upstream roadmap README.md # Human-facing quickstart + per-tool usage reference SKILL.md # Agent skill (symlinked into ~/.agents/skills/ on install) +docs/ + rfc-001-global-palace.md # Design: one primary palace + per-machine local fallback (mempalace-edge) bin/ mempalace-docs # Docs-only MemPalace miner (bash wrapper) mempalace-session # Opencode session → MemPalace bridge (bash + inline Python) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 86bda02..c359fa3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -357,10 +357,13 @@ These gaps should ideally close upstream, making the wrappers thinner or obsolet When #1 merges, retire `mempalace-docs` to a thin shim. When #2 + #3 land together, `mempalace-session` becomes a manual-only fallback (cron / backfill) while hooks handle live saves. +Separately tracked in [`docs/rfc-001-global-palace.md`](docs/rfc-001-global-palace.md): moving from one palace *per machine* to a **single primary palace with per-machine local fallback** (`mempalace-edge`). That RFC also records upstream items of its own — `origin_host` provenance metadata, per-wing ACLs, a `mempalace_kg_supersede` tool-classification fix, and a guard against running `mempalace sync` on a shared palace. + --- ## 7. See also +- [`docs/rfc-001-global-palace.md`](docs/rfc-001-global-palace.md) — design for a fleet-wide primary palace with offline-capable local fallback. **Read before attempting to "centralize" or "sync" palaces** — it documents two silently destructive footguns (`--palace` vs `MEMPALACE_PALACE_PATH`, and `mempalace sync`). - [`README.md`](README.md) — human-facing quickstart + per-tool usage reference. - [`AGENTS.md`](AGENTS.md) — repo conventions for AI agents modifying this codebase. - [`SKILL.md`](SKILL.md) — agent skill (producer side), symlinked into `~/.agents/skills/opencode-mempalace-bridge/` by `install.sh`. diff --git a/docs/rfc-001-global-palace.md b/docs/rfc-001-global-palace.md new file mode 100644 index 0000000..0d518cb --- /dev/null +++ b/docs/rfc-001-global-palace.md @@ -0,0 +1,378 @@ +# RFC 001 — Global palace with local fallback (`mempalace-edge`) + +| | | +| --- | --- | +| **Status** | Draft — design agreed, not implemented | +| **Created** | 2026-08-08 | +| **Applies to** | mempalace 3.6.0, mempalace-toolkit @ `96699f2`, pi-devbox ≥ v1.3.0 | +| **Decision** | Phases 0–2 + 4 in scope. **Phase 3 (full pull replication) explicitly deferred** — "a laptop that can reach its own stuff plus whatever it can reach" is good enough. | + +**Read this first if you are asked to "centralize MemPalace" / "sync palaces between machines".** Most of the +hard-won facts below are non-obvious and two of them are actively destructive if you guess wrong +(§7.1, §7.2). The evidence index in §10 lets you re-verify any claim without re-reading 45k lines. + +--- + +## 1. The problem + +One palace per machine per harness. Today: a pi-devbox container on `EMB-7KJ4VR4G`, another on +`tor-ms22`, opencode-devbox containers, `MBP-M1-2020`, plus native hosts — each with a private +`~/.mempalace`. Consequences: + +1. **Memory is sharded by accident of where you happened to be working.** A decision recorded on the + laptop is invisible to the agent on the desktop. +2. **Container recreates are amnesia events** unless the palace happens to be host-bind-mounted. +3. **The KG is the worst hit** — `kg_query`/`kg_timeline` answers depend on which machine you ask. + +**Target state:** one primary palace holds the fleet's memory. Every client keeps working when the +primary is unreachable (writes buffer locally, reads degrade to local), and reconciles when it +returns. Access is authenticated per device, with a sane authorization policy. + +--- + +## 2. Verified starting point (as of 2026-08-08) + +Two of the three pieces already exist. This is not greenfield. + +| Piece | State | Evidence | +| --- | --- | --- | +| **Primary server** | ✅ **Exists.** `mempalace serve --host --port --token --tls-cert --tls-key --read-only --allow-insecure`. Bearer token compared with `hmac.compare_digest`, **mandatory** on non-loopback binds (unless `--allow-insecure`), TLS 1.2+ resolved *before* bind, Host-header pinning + `Origin` allowlist (anti-DNS-rebinding), 16 MiB body cap, token-free `/healthz`. | `cli.py:cmd_serve` (~1448); `mcp_server.py:5205-5215`, `5284-5289` | +| **Remote client** | ⚠️ **Exists for pi only, and it is either/or.** `createClient()` picks stdio *or* HTTP once at process start. | `extensions/pi/mempalace.ts:629-641` | +| **Fallback + resync** | ❌ **Absent everywhere.** On remote failure the pi bridge re-handshakes the same URL, then **de-registers all palace tools** and runs blind. | `extensions/pi/mempalace.ts:665-673` | +| **opencode client** | ❌ **stdio only.** `{"type":"local","command":["mempalace-mcp"]}` in `~/.config/opencode/opencode.json`. No remote entry exists anywhere in `myconfigs`. | `myconfigs/tor-ms22.home.arpa/.config/opencode/opencode.json` | +| **Server compose** | ✅ Exists: `pi-devbox/docker-compose.mempalace.yml` (canonical) + a tor-ms22 derivative (`docker-compose-repo/tor-ms22/pi-devbox/`, `df2c2ae`, port 8766, uid 1000, binds the real `~/.mempalace`). Not enabled. | pi-devbox CHANGELOG v1.3.0 (2026-07-02) | + +> **Stale docs warning.** `docker-compose.mempalace.yml`, `.env.example` and CHANGELOG v1.3.0 all say the +> HTTP transport is unauthenticated and should be fronted by a reverse proxy. That was true for +> `mempalace-mcp --transport http` in the v1.3.0 era. **mempalace 3.6.0's `serve` has token + TLS +> built in** (upstream #1877). Fix those comments during Phase 1. + +### Two things that sound like the feature and are not + +- **`sync.py` is not replication.** It is gitignore-aware drawer *deletion*: "*Removes drawers whose + source files are now gitignored, deleted, or moved out of the project*" (`sync.py:1-12`). See §7.2 — + running it against a shared palace is a fleet-wide memory wipe. +- **`wal/write_log.jsonl` is not a replayable WAL.** `_WAL_REDACT_KEYS` strips + `content`/`content_preview`/`document`/`entry`/`entry_preview`/`query`/`text` and replaces them with + `"[REDACTED N chars]"`; all 12 references are writers, there is **no reader** and no `replay` + function (`wal.py:74`). Reconstructing memory from it is information-theoretically impossible. + +--- + +## 3. Why we sync operations, not databases + +Row-level / file-level replication of a palace is a trap in this codebase. Four findings, each +independently disqualifying: + +1. **Vectors are not portable.** Embeddings are computed **client-side** — `backends/pgvector.py:6-8`: + "*Embeddings are still produced locally by MemPalace through the core embedding wrapper before + vectors are written to Postgres.*" Cross-architecture ONNX determinism (arm64 macOS vs x86-64 + Linux, different execution providers) is not guaranteed. → **Ship text + metadata, always re-embed + on receipt.** Cheap and safe: the raw text is always stored as the chroma document. +2. **KG rows cannot be copied.** `triples` has **no** `UNIQUE(subject,predicate,object,valid_from)` — + only `id` is unique, and `make_triple_id` embeds `datetime.now()`. A row copy therefore duplicates + every fact. But `add_triple()` guards at the application level (`SELECT id … WHERE subject=? AND + predicate=? AND object=? AND valid_to IS NULL` → returns the existing id), so **replaying `kg_add` + is idempotent for open facts** (`knowledge_graph.py:163-178`, `305-313`). +3. **Only one write path has deterministic IDs.** + + | Write path | ID recipe | Same content on 2 hosts → same ID? | + | --- | --- | --- | + | `add_drawer` / `checkpoint` | `drawer_{wing}_{room}_{sha256(wing\|room\|content)[:24]}` | **YES** — content-addressed, merges for free | + | project/format miner | `…sha256(source_file\|chunk_index)` | **NO** — absolute path (`/Users/joakim/x` vs `/workspace/x`) | + | convo miner | `…sha256(source_file\|extract_mode\|chunk_index)` | **NO** — same reason | + | diary | `diary_{wing}_{YYYYmmdd_HHMMSSffffff}_{sha256(entry)[:12]}` | **NO** (µs timestamp) — but the 12-hex suffix is a usable content dedup key | + | KG triple | `t_{s}_{p}_{o}_{sha256(valid_from\|recorded_at)[:12]}` | **NO** (`recorded_at = now()`) | + + (`ids.py:56,71` — `ID_RECIPE = "v3"`, length-prefixed delimited hashing.) +4. **Batch dedup will not save a naive merge.** `dedup.py` groups by `source_file` and only compares + *within* a group (cosine < 0.15) → cross-host duplicates with differing paths are never compared. + The mechanism that *does* work is write-time: `tool_check_duplicate(content, threshold=0.9)` queries + the whole collection, and `mempalace_checkpoint` already runs it per item. + +**Conclusion:** the MCP tool surface is already a small, coarse-grained, mostly-idempotent operation +vocabulary. Log the *intent*, replay the *intent*. That sidesteps chroma internals, chunking, +embedder drift and vector portability in one move. + +--- + +## 4. Design: `mempalace-edge`, a local MCP proxy + +### 4.1 Shape + +Do **not** put fallback logic in `extensions/pi/mempalace.ts` — opencode would need it again, and it +may not support remote MCP at all (§9.1). Instead: a sidecar that *is* an MCP server. + +``` +pi (mempalace.ts, stdio) ─┐ +opencode (mcp: type=local) ─┼──► mempalace-edge ──HTTPS MCP──► PRIMARY +mempalace CLI (local palace) ─┘ │ (stdio MCP server) mempalace serve + ├─ child: mempalace-mcp --token --tls-cert + │ (local palace, always writable) + └─ outbox.sqlite (durable op queue) +``` + +**`mempalace-edge` needs zero mempalace internals** — it is an MCP-to-MCP proxy. It speaks stdio down +to a child `mempalace-mcp` and HTTPS up to the primary, reusing the `RemoteMcpClient` already written +in `96699f2` (`extensions/pi/mempalace.ts:390`, vendored from `pi-extensions/mcp-loader.ts` — mind the +`MCP-STREAMABLE-HTTP-CLIENT-SYNC: v1` drift token if you copy it again). + +Why this shape wins: + +- **One implementation for every harness.** opencode's change is one line: + `"command": ["mempalace-edge"]`. pi's is one line. The CLI is untouched. +- **Tools never vanish** from the tool list, so wake-up context injection keeps working offline — + unlike today's fail-soft de-registration. +- **Survives mempalace upgrades**: coupled to the tool schema, not to chroma/HNSW. +- The remote transport is **sessionless JSON-RPC** today (per `96699f2`'s own note), so reconnect + after an outage is cheap — there is no session to re-establish. + +### 4.2 Routing policy + +`service.py:29,54,72` already ships the exact three-way split we need — `READ_TOOLS`, +`WRITE_TOOLS`, `MAINTENANCE_TOOLS` (`{mine, sync, reconnect}`) — plus `classify_tool()`. + +| Class | Primary up | Primary down | +| --- | --- | --- | +| **read** (`search`, `kg_query`, `diary_read`, `list_*`, `traverse`…) | query **both**, merge ranked lists, dedupe by drawer id | local only, response flagged `degraded: true` | +| **write** (`add_drawer`, `checkpoint`, `diary_write`, `kg_add`…) | apply local **and** enqueue → primary | apply local, enqueue, keep working | +| **maintenance** (`mine`, `sync`, `reconnect`) | **never proxied** — local only (§5, §7.2) | local only | +| **destructive** (`delete_drawer`, `delete_by_source`, `delete_tunnel`…) | admin-scoped only; tombstone, never hard-delete remotely | local only | + +> ⚠️ **`classify_tool()` must be treated as fail-closed.** `mempalace_kg_supersede` is a real MCP tool +> (3 references in `mcp_server.py`) but is **absent from `WRITE_TOOLS`** → `classify_tool()` returns +> `"unknown"` for it. An edge proxy that routed "unknown" as read would silently drop supersede +> operations. Maintain our own tool→class table, default unknown ⇒ **write**, and file the upstream fix. + +**The merged-read trick is what makes Phase 3 optional.** Both palaces return ranked results with +distances; if the embedder identity matches (§7.4) the distances are comparable, so a union + +re-sort + dedupe-by-id gives a correct combined result set. You get fleet-wide recall *without* +replicating anything. Offline you simply see less. + +KG reads are the exception: unioning triples is unsafe because `invalidate`/`supersede` are +order-dependent. Use **primary-first, local-fallback** (no union) for `kg_*` reads, and accept that an +offline KG answer is incomplete. + +### 4.3 Outbox + +Model it on `daemon.py`, which already implements a durable local job queue and is the closest +existing prior art (token auth via `ensure_token`, 0600, per-palace state dir, `MAX_ATTEMPTS = 3`, +`recover_running`, `JOB_RETENTION_DAYS = 7`). Its key primitive is worth copying verbatim: + +```sql +CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_dedupe_active + ON jobs(dedupe_key) WHERE state IN ('queued', 'running'); +``` + +A TOCTOU-safe, cross-process "at most one active job per key". Our outbox rows: + +| Column | Purpose | +| --- | --- | +| `op_id` | `sha256(origin_device, local_seq, tool_name, canonical_payload)` — stable across retries, the primary's idempotency key | +| `local_seq` | Per-device monotonic counter → **causal order** for `kg_invalidate`/`supersede` replay | +| `origin_device`, `origin_host` | Provenance (§7.3) | +| `tool_name`, `payload_json` | The MCP call to replay | +| `state`, `attempts`, `created_at`, `flushed_at`, `error_json` | Retry/audit | + +Flush = drain in `local_seq` order, stop on first hard failure (preserve ordering), exponential +backoff, resume on reconnect. The primary de-dupes on `op_id`; `add_drawer`'s own pre-write probe and +`add_triple`'s open-fact guard make replay safe even if `op_id` tracking is lost. + +`write_routing.py` is the upstream-shaped seam for this if we ever want it in core: its +`WriteRoutingPolicy{DIRECT,PREFER,REQUIRE}` → `WriteRoutingTarget{DIRECT,DAEMON,BLOCKED}` enums would +gain a `REMOTE` target. The module says it "*changes no caller defaults by itself*" — i.e. it exists +precisely to be extended. + +--- + +## 5. Not everything should be global + +Per-wing replication policy, declared in edge config: + +| Policy | Wings | Rationale | +| --- | --- | --- | +| `replicated` | diaries, `wing_pi`, checkpoints, KG, hand-authored notes | Small, curated, content-addressed → **already merge-safe** | +| `local` | mined code/docs (e.g. the 13,597-drawer `workspace` wing on this host) | Derived data, re-mineable from git, and carries exactly the path-dependent IDs that break merging (§3.3) | + +This shrinks the hard problem to the layer that is already safe, and it is why **`mine` must never be +remote**: the primary serializes *every* request behind one lock (§7.5), so a bulk remote mine would +stall every other agent in the fleet. + +--- + +## 6. Security model + +The transport is largely solved; **the gap is authorization, not cryptography.** + +### 6.1 Threats, in priority order + +1. **Memory poisoning / persistent cross-machine prompt injection — the underrated one.** Palace + content is injected into agent context at wake-up. A shared palace means one careless or + compromised container can plant instructions that *every other agent in the fleet reads as trusted + memory*. Mitigation: provenance on every record (§7.3), don't auto-inject wake-up content authored + by devices outside a trusted set, keep an admin-only wing for anything instruction-shaped. +2. **Aggregation raises exfiltration impact.** One primary holds every machine's diaries — which + already quote internal hostnames, paths and token prefixes. Mitigation: per-wing ACL; don't put + secrets in memory (the WAL redaction list exists for a reason); private-network-only exposure. +3. **Accidental mass deletion** by a client (`sync`, `delete_by_source`) — see §7.2. +4. **Availability**: the server is single-writer by design; one long operation blocks everyone. + +### 6.2 Policy + +| Control | Decision | +| --- | --- | +| **Network posture** | Primary **never** internet-exposed. Bind loopback in the container; publish only onto the private overlay / existing tunnel (Pangolin/newt). | +| **Transport** | TLS via `serve --tls-cert/--tls-key`, or terminate TLS **+ mTLS** in the reverse proxy (cheaper than patching Python's TLS surface). | +| **Authentication** | **Per-device bearer tokens**, not the one shared token. Server-side registry `token → {device_id, scopes}`. Store in the existing `.env.age` flow, 0600 on disk. Enables revoking one laptop and rotating without a fleet outage. | +| **Authorization** | Per-device read/write **wing globs**. Server-side refusal of `mine`/`sync`/`delete_*` except for an admin device. `--read-only` gives a free observer tier. | +| **Provenance/audit** | `origin_host` + `origin_device` + `op_id` on every record (§7.3); server-side append-only op log. | +| **Recovery** | Deletes as tombstones; server-side backups with `backups.py` retention (`MEMPALACE_MAX_BACKUPS`). | +| **DoS** | Keep the 16 MiB cap; add rate limiting; no remote `mine`. | + +--- + +## 7. Landmines — Phase 0 runbook + +Do these **before** any cutover. 7.1 and 7.2 cause silent data loss. + +### 7.1 `MEMPALACE_PALACE_PATH` ≠ `--palace` (silent empty KG) + +```python +# mcp_server.py:708-711 (_palace_flag_given = bool(_args.palace), line 325) +def _resolve_kg_path() -> str: + if _palace_flag_given: + return os.path.join(_config.palace_path, "knowledge_graph.sqlite3") + return DEFAULT_KG_PATH # knowledge_graph.py:49 → ~/.mempalace/knowledge_graph.sqlite3 +``` + +`cmd_serve` **always** passes `--palace`. So the moment you start the server, the KG becomes +`/knowledge_graph.sqlite3` — a *different file* from the live `~/.mempalace/knowledge_graph.sqlite3`. +The vector store looks fine and the knowledge graph is silently empty. + +Four stores, **three** location rules: + +| Store | Location rule | +| --- | --- | +| drawers | backend-abstracted (`BaseCollection`) | +| `knowledge_graph.sqlite3` | HOME-anchored **unless `--palace` flag** (`knowledge_graph.py:49`) | +| `hallways.json` | derived from `palace_path`, with legacy HOME fallback (`hallways.py:73,83`) | +| `known_entities.json` | HOME-anchored (`miner.py:701`) | + +**Action:** `mv` all three files into the palace directory before first `serve`, and verify +`kg_stats` is non-empty afterwards. Corollary: **a shared pgvector/qdrant backend shares drawers and +nothing else** — it would leave the KG as fragmented as it is today. That is why the shared-backend +option is *not* the answer. + +### 7.2 Never run `mempalace sync` against a shared palace + +It classifies drawers whose source files are absent **on the running host** as orphans and deletes +them; `_auto_detect_project_roots` guesses roots from drawer metadata, so the blast radius is +data-dependent rather than obvious. From a laptop that lacks the repos, it is a fleet-wide wipe. +**Action:** edge blocks `mempalace_sync` from ever reaching the primary; document it; consider an +upstream `--refuse-shared` guard. + +### 7.3 Add provenance metadata now + +Today every drawer carries exactly `{wing, room, source_file, added_by, filed_at, id_recipe}` +(+`chunk_index`, `parent_drawer_id`); diaries add `{hall, topic, type, agent, date}`. `added_by`/`agent` +is the *agent* name (`pi`, `mcp`, `checkpoint`) — **never the machine**. So after any merge you cannot +tell which host wrote a record, cannot audit, and cannot compute per-device high-water marks. +Metadata is free-form, so this is cheap — but it is **impossible to backfill**. Do it first. + +### 7.4 Embedder identity is client-local and currently toothless + +`check_embedder_identity()` raises `DimensionMismatchError` / `EmbedderIdentityMismatchError`, but the +sidecar on this host reads `{"mempalace_drawers": {"model_name": "minilm", "dimension": 0}}` — and +`dimension: 0` means *unknown* and is **skipped**, so only the model *name* is compared. The sidecar +lives in the client's palace dir (`_sidecar.py`), so with a remote backend the check is per-client and +never centrally enforced. **Action:** the primary must reject writes from a client whose embedder +identity does not match, and merged reads (§4.2) must be disabled on mismatch — comparing distances +across models silently degrades recall. + +### 7.5 Concurrency expectations + +- `_HTTP_REQUEST_LOCK = threading.Lock()` wraps **every** dispatch (`mcp_server.py:5115`, held at + `5346`/`5513`), commented: "*HTTP gives us a safer transport, not concurrent Chroma/HNSW mutation.*" + The primary is a **single-writer, one-request-at-a-time** service. Size for a handful of agents. +- `mine_palace_lock` uses `fcntl.flock(LOCK_EX|LOCK_NB)` and exists because parallel HNSW inserts + "*can corrupt the HNSW graph*" — but the lock **file** is HOME-derived (`~/.mempalace/locks/ + mine_palace_{sha256(realpath(palace))[:16]}.lock`) while the key is palace-derived. Two containers + with different `~/.mempalace` mounts but the same palace path compute the same key on *different + files* → **no mutual exclusion**. +- **Therefore: reject "put the palace on a NAS/SMB share and point everyone at it."** flock over + NFS/SMB is unreliable, and the lock wouldn't be shared anyway. One process owning the files and + serving HTTP is the supported model — which is exactly why `serve` exists. + +--- + +## 8. Phasing + +| Phase | Effort | Deliverable | +| --- | --- | --- | +| **0 — hygiene** | hours | §7 runbook: move KG/hallways/entities, add provenance metadata, ban `sync` on shared palaces, per-device tokens, fix stale "unauthenticated" docs | +| **1 — primary up** | hours, **no code** | `mempalace serve --token --tls-cert` on a private-net host (reuse `docker-compose.mempalace.yml`, mind port 8765 vs pi-studio — tor-ms22 already moved to 8766). Repoint pi clients via `MEMPALACE_REMOTE_URL`/`MEMPALACE_REMOTE_TOKEN`. **Shared memory today, no offline.** | +| **2 — `mempalace-edge`** | ~1 week | The actual ask: local-first writes + outbox flush + merged reads + per-wing policy. Fixes opencode as a side effect. | +| **3 — pull replication** | ~1 week | **DEFERRED (2026-08-08).** Server-side op-log with monotonic seq → each edge a full offline replica. Only needed if a laptop must hold *everything* offline. Accepted trade-off: offline recall = own writes + last-reachable state. | +| **4 — authz** | days | Per-wing ACL, per-device scopes, audit log, token rotation | + +Phase 1 is worth doing on its own — it is pure configuration and immediately ends KG fragmentation +for online clients. + +--- + +## 9. Open questions + +1. **Does opencode's MCP config support a remote/HTTP transport at all?** Every sampled entry in + `myconfigs` is `"type":"local"`; no documentation found either way. **If it does not, the edge + proxy is not merely nicer — it is the only option for opencode clients.** Verify against opencode's + MCP client source before Phase 2. +2. **Upstream or local?** `mempalace-edge` needs no core changes, so it belongs in this repo + (`bin/` + `extensions/`). But `origin_host` metadata, per-wing ACL, the `kg_supersede` + classification fix, and a `sync --refuse-shared` guard all want to go **upstream** + (`github.com/MemPalace/mempalace`). +3. **Chunked drawers under merge.** Oversized content splits into `{drawer_id}_chunk_NNNNNN` with + `parent_drawer_id`, and the add-time idempotency probe checks only the *last* chunk id — a + partially transferred chunk set may look "already present" and stay truncated. Unverified; test + before trusting bulk replay. +4. **`migrate.py` as a bootstrap tool.** `extract_drawers_from_sqlite()` reads `{id, document, + metadata}` straight out of chroma's SQLite (bypassing the chromadb API) and re-`add`s them with ids + and metadata preserved and **embeddings recomputed** (`migrate.py:326`) — the right shape for a + one-shot "seed the primary from an existing palace". Unverified: `col.add` behaviour on **id + collision into a non-empty target**. Test before relying on it. (`exporter.py` is markdown-only, + lossy, and has **zero callers** — not a transfer format. `backups.py` is retention pruning only.) +5. **Does `_HTTP_REQUEST_LOCK` stay held for the duration of an MCP-triggered `mine`?** Strongly + suggested by the code shape; if yes, a remote mine makes the primary unusable for its duration + (another argument for §5). + +--- + +## 10. Evidence index + +Re-verify without re-reading the package. Paths relative to +`/opt/uv-tools/mempalace/lib/python3.13/site-packages/mempalace/` (mempalace 3.6.0) unless noted. + +| Claim | Where | +| --- | --- | +| Server auth/TLS/read-only | `cli.py:1448-1503`; `mcp_server.py:282-320`, `5205-5215`, `5284-5289` | +| Single global request lock | `mcp_server.py:5115`, `5346`, `5513` | +| KG path follows `--palace` flag only | `mcp_server.py:325`, `708-711`; `knowledge_graph.py:49` | +| KG DDL, no `(s,p,o)` uniqueness, open-fact guard | `knowledge_graph.py:163-178`, `305-313` | +| ID recipes | `ids.py:25`, `56`, `71`; diary at `mcp_server.py:3511-3514` | +| Tool classification (+ `kg_supersede` gap) | `service.py:29`, `54`, `72-84` | +| Write-routing seam | `write_routing.py:1-60` | +| Outbox prior art (queue DDL, token, retries) | `daemon.py` (`_init_db`, `ensure_token`, `MAX_ATTEMPTS`) | +| `sync` is destructive pruning | `sync.py:1-12` | +| WAL redacts + has no reader | `wal.py:74`, `_WAL_REDACT_KEYS` | +| Client-side embeddings | `backends/pgvector.py:6-8`; `embedding.py:6-11`; `_sidecar.py` | +| Dedup scoped by `source_file` | `dedup.py:get_source_groups` | +| Store location rules | `hallways.py:73,83`; `miner.py:701` | +| Mine lock (HOME-derived file) | `palace.py:1090`, `1123-1128` | +| Migrate as export primitive | `migrate.py:extract_drawers_from_sqlite`, `:326` | +| pi remote transport + fail-soft | `mempalace-toolkit/extensions/pi/mempalace.ts:11-12`, `87`, `108`, `390`, `629-641`, `665-673` (commit `96699f2`) | +| Server compose + port history | `pi-devbox/docker-compose.mempalace.yml`; `pi-devbox/CHANGELOG.md` v1.3.0; `docker-compose-repo/tor-ms22/pi-devbox/` (`df2c2ae`) | + +## 11. See also + +- [`ARCHITECTURE.md`](../ARCHITECTURE.md) — producer side (how the palace gets fed); §6 upstream roadmap +- [`extensions/pi/README.md`](../extensions/pi/README.md) — pi bridge internals +- [`SKILL.md`](../SKILL.md) — consumer-side protocol (search before answering, diary before exit)