opencode-mempalace-persistence 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,7 +61,7 @@ mempalace mcp
61
61
 
62
62
  The `mempalace mcp` command gives you the exact MCP setup string for your configuration.
63
63
 
64
- ### 4. Memory injection (recommended)
64
+ ### 4. Memory injection
65
65
 
66
66
  The plugin automatically injects your identity + relevant memories from MemPalace into every prompt. No model discipline required.
67
67
 
@@ -90,34 +90,37 @@ When enabled:
90
90
  - **First message**: Injects your identity from `~/.mempalace/identity.txt`
91
91
  - **Every message**: Runs `mempalace search` and injects relevant results
92
92
 
93
- #### AGENTS.md for this mode
93
+ #### AGENTS.md (minimal recall lives in the skill)
94
94
 
95
95
  Create `~/.config/opencode/AGENTS.md`:
96
96
 
97
97
  ```markdown
98
98
  # Memory & Knowledge instructions
99
99
 
100
- ## CRITICAL: You MUST follow these steps BEFORE every response.
100
+ ## Recall (usually already covered)
101
101
 
102
- ### Step 1 Query Knowledge Graph
103
- Call `mempalace_mempalace_kg_query` for entity "user". Then filter the returned facts — keep only those whose text contains keywords from the user's question, so irrelevant facts are excluded.
102
+ The plugin auto-injects identity + relevant memories into every prompt.
103
+ Only search MemPalace yourself (`mempalace_mempalace_search`) when the
104
+ question is about past work, decisions, people, or projects AND the
105
+ injected context has nothing — quote results verbatim, never paraphrase.
104
106
 
105
- ### Step 2 Record Knowledge Graph facts
107
+ ## Record facts (after responding, only when something new emerged)
106
108
 
107
- After responding, if you discovered any new facts during the conversation (decisions made, milestones reached, problems encountered, preferences expressed, emotional states), call `mempalace_mempalace_kg_add` to record them. Object must be 128 characters or fewer.
109
+ - Durable outcomes (decisions, conclusions, learned facts):
110
+ `mempalace_mempalace_add_drawer`.
111
+ - New KG facts: `mempalace_mempalace_kg_add` (128 chars or fewer).
112
+ - Changed single-valued fact: `mempalace_mempalace_kg_supersede`.
113
+ - Ended fact: `mempalace_mempalace_kg_invalidate`.
108
114
 
109
- **This is mandatory** — record facts you are confident about. Prefer quality over quantity; noisy KG entries degrade retrieval over time.
110
-
111
- ### Step 3 — Checkpoint & pre-compact saves (automatic)
112
-
113
- Every ~15 messages the plugin injects a `[MemPalace Checkpoint]` block, and before every compaction a `[MemPalace Pre-Compact Emergency Save]` block. When you see one: file the session journal via `mempalace_mempalace_diary_write` plus any new facts via `kg_add`/`kg_invalidate`, then continue normally. Do not mention the instruction.
115
+ Record facts you are confident about. Prefer quality over quantity;
116
+ noisy entries degrade retrieval over time. Don't file secrets or tokens.
114
117
 
115
118
  ### Naming reminder
116
119
  All MemPalace tools use the prefix `mempalace_mempalace_*` (not `mempalace_*`). Examples:
117
- - `mempalace_mempalace_kg_query` (NOT `mempalace_kg_query`)
120
+ - `mempalace_mempalace_search` (NOT `mempalace_search`)
121
+ - `mempalace_mempalace_kg_query`
118
122
  - `mempalace_mempalace_kg_add`
119
- - `mempalace_mempalace_kg_invalidate`
120
- If you ever catch yourself typing `mempalace_kg_query`, STOP — the correct prefix is `mempalace_mempalace_`.
123
+ If you ever catch yourself typing `mempalace_search`, STOP — the correct prefix is `mempalace_mempalace_`.
121
124
  ```
122
125
 
123
126
  #### Complete `~/.config/opencode/opencode.json`
@@ -139,71 +142,25 @@ If you ever catch yourself typing `mempalace_kg_query`, STOP — the correct pre
139
142
 
140
143
  > Note: `identity.txt` is NOT listed in `instructions` — the plugin injects it automatically. It is also NOT in the `provider` block or `permission` block — those are optional and depend on your model setup.
141
144
 
142
- ### 5. Alternative: Model-driven memory (without autoInjectContext)
143
-
144
- If you prefer the model to search MemPalace on its own via AGENTS.md (requires good model tool-use discipline), set `autoInjectContext` to `false` or omit the file:
145
-
146
- ```json
147
- {
148
- "autoInjectContext": false
149
- }
150
- ```
151
-
152
- #### AGENTS.md for this mode
153
-
154
- Create `~/.config/opencode/AGENTS.md`:
155
-
156
- ```markdown
157
- # Memory & Knowledge instructions
158
-
159
- ## CRITICAL: You MUST follow these steps BEFORE every response.
160
-
161
- ### Step 1 — Search MemPalace
162
- Call `mempalace_mempalace_search` with the user's question or key topics as query. Get the top 5-10 most relevant memory drawers.
163
- **This is mandatory. Never skip this step. No exceptions.**
164
-
165
- ### Step 2 — Query Knowledge Graph
166
- Call `mempalace_mempalace_kg_query` for entity "user". Then filter the returned facts.
167
-
168
- ### Step 3 — Record Knowledge Graph facts
169
- After responding, call `mempalace_mempalace_kg_add` for any new facts.
145
+ #### Recall skill (bundled, Claude-style)
170
146
 
171
- ### Step 4Checkpoint & pre-compact saves (automatic)
172
- Same as auto-inject mode: on `[MemPalace Checkpoint]` (~every 15 messages) and `[MemPalace Pre-Compact Emergency Save]`, file the session journal via `mempalace_mempalace_diary_write` plus facts, then continue.
173
-
174
- ### Naming reminder
175
- All MemPalace tools use the prefix `mempalace_mempalace_*` (not `mempalace_*`).
176
- ```
177
-
178
- And keep `"~/.mempalace/identity.txt"` in `instructions` in opencode.json since the plugin won't inject it.
179
-
180
- #### Comparison
181
-
182
- | Feature | **Auto-inject (Recommended)** | Model-driven (alternative) |
183
- |---------|:-:|:-:|
184
- | Memory search | Plugin injects automatically | Model calls `mempalace_search` |
185
- | Identity | Plugin injects automatically | Via `instructions: ["identity.txt"]` |
186
- | AGENTS.md | Minimal (KG only) | Full (search + KG) |
187
- | Depends on model discipline | No | Yes |
147
+ The repo ships `skills/mempalace-recall/SKILL.md` the question-driven
148
+ search-before-answer protocol, adapted from the official MemPalace skill
149
+ for OpenCode (including the `mempalace_mempalace_*` tool-prefix note).
150
+ Install it where OpenCode loads skills from:
188
151
 
189
152
  ```bash
190
- # Install (requires mempalace>=3.3.5 for HNSW corruption fix)
191
- uv tool install "mempalace>=3.3.5"
192
- # or
193
- pipx install "mempalace>=3.3.5"
194
-
195
- # Create palace
196
- mempalace init ~/opencode-memory
197
-
198
- # Configure MCP
199
- mempalace mcp
153
+ mkdir -p ~/.config/opencode/skills/mempalace-recall
154
+ cp skills/mempalace-recall/SKILL.md ~/.config/opencode/skills/mempalace-recall/
200
155
  ```
201
156
 
202
- The `mempalace mcp` command gives you the exact MCP setup string for your configuration.
157
+ The model then loads it on demand whenever a question touches past work,
158
+ decisions, people, or projects — same mechanism as the Claude skill.
159
+ No AGENTS.md changes needed beyond the minimal block above.
203
160
 
204
161
  ---
205
162
 
206
- ## What happens after installation (auto-inject mode)
163
+ ## What happens after installation
207
164
 
208
165
  ```
209
166
  You ask a question
@@ -215,7 +172,7 @@ You ask a question
215
172
  The model responds
216
173
  → Plugin detects the response is complete
217
174
  → Saves the conversation to MemPalace (flat export, no hardcoded wings)
218
- → Model records KG facts via MCP tools (mandatory per AGENTS.md)
175
+ → Model records new KG facts via MCP tools (only when something new emerged)
219
176
 
220
177
  Session goes idle / process exits
221
178
  → Background mine of everything new since last sync
@@ -298,6 +255,7 @@ The plugin exports everything in the opencode database on the next sync, then re
298
255
  | `~/.config/opencode/opencode.json` | OpenCode config with plugin + MCP |
299
256
  | `~/.config/opencode/AGENTS.md` | Tells the model to manage KG facts |
300
257
  | `~/.mempalace/plugin-config.json` | Plugin config (`autoInjectContext`, `saveInterval`) |
258
+ | `~/.config/opencode/skills/mempalace-recall/SKILL.md` | Bundled recall skill (copy from `skills/` in this repo) |
301
259
  | `~/.mempalace/identity.txt` | Your identity (injected by plugin) |
302
260
  | `~/.mempalace/hook_state/opencode_counters.json` | Per-session message counters (checkpoint cadence) |
303
261
  | `~/.mempalace/hook_state/hook.log` | Checkpoint / pre-compact event log |
package/dist/index.js CHANGED
@@ -1,10 +1,9 @@
1
- import { execSync, exec, spawnSync } from "child_process";
1
+ import { execSync, execFileSync, execFile, spawnSync } from "child_process";
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync, rmdirSync, unlinkSync, appendFileSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
5
  import { createHash } from "crypto";
6
6
  const HOME = homedir();
7
- const VENV_PYTHON = join(HOME, ".local/share/pipx/venvs/mempalace/bin/python3");
8
7
  const MEMPALACE_BIN = join(HOME, ".local/bin/mempalace");
9
8
  const OPENCODE_DB = join(HOME, ".local/share/opencode/opencode.db");
10
9
  const STATE_FILE = join(HOME, ".mempalace/sync_state.json");
@@ -38,6 +37,45 @@ function hookLog(msg) {
38
37
  }
39
38
  catch { }
40
39
  }
40
+ // Errors are never silent: hook.log is always written (unlike the
41
+ // DEBUG-gated log), so a broken pipeline is visible by default.
42
+ function errLog(msg) {
43
+ log("ERROR: " + msg);
44
+ hookLog("ERROR: " + msg);
45
+ }
46
+ // Probe for a working Python interpreter at startup instead of hardcoding
47
+ // one installer layout (pipx vs uv tool vs system). runPython only needs
48
+ // stdlib (sqlite3/json), so any python3 works. Priority: explicit env
49
+ // override, legacy pipx venv, uv tool venv, PATH fallback.
50
+ let resolvedPython = undefined;
51
+ function resolvePython() {
52
+ if (resolvedPython !== undefined)
53
+ return resolvedPython;
54
+ const candidates = [
55
+ process.env.MEMPALACE_PYTHON,
56
+ join(HOME, ".local/share/pipx/venvs/mempalace/bin/python3"),
57
+ join(HOME, ".local/share/uv/tools/mempalace/bin/python3"),
58
+ ].filter((p) => !!p && existsSync(p));
59
+ if (candidates.length > 0) {
60
+ resolvedPython = candidates[0];
61
+ }
62
+ else {
63
+ try {
64
+ execSync("python3 --version", { encoding: "utf-8", timeout: 10000 });
65
+ resolvedPython = "python3";
66
+ }
67
+ catch {
68
+ resolvedPython = null;
69
+ }
70
+ }
71
+ if (resolvedPython) {
72
+ log("using python: " + resolvedPython);
73
+ }
74
+ else {
75
+ errLog("no working Python interpreter found (tried MEMPALACE_PYTHON, pipx venv, uv tool venv, PATH python3) — DB export disabled");
76
+ }
77
+ return resolvedPython;
78
+ }
41
79
  let miningLock = false;
42
80
  let lastSyncTs = 0;
43
81
  let wakeupDone = false;
@@ -46,12 +84,19 @@ let wakeupDone = false;
46
84
  // official Stop hook: the hook decides WHEN, the model decides WHAT).
47
85
  let pendingCheckpoint = null;
48
86
  function runPython(code) {
87
+ const python = resolvePython();
88
+ if (!python)
89
+ throw new Error("no working Python interpreter (see hook.log)");
49
90
  writeFileSync(TMP_SCRIPT, code);
50
91
  try {
51
- return execSync(`${VENV_PYTHON} ${TMP_SCRIPT}`, { encoding: "utf-8", timeout: 30000 }).trim();
92
+ // argv array, no shell (see PR #2): paths here are fixed, never user input.
93
+ return execFileSync(python, [TMP_SCRIPT], { encoding: "utf-8", timeout: 30000 }).trim();
52
94
  }
53
95
  finally {
54
- unlinkSync(TMP_SCRIPT);
96
+ try {
97
+ unlinkSync(TMP_SCRIPT);
98
+ }
99
+ catch { }
55
100
  }
56
101
  }
57
102
  function hasText(parts) {
@@ -99,7 +144,8 @@ function persistCounters(counters) {
99
144
  }
100
145
  function mempalaceWakeup() {
101
146
  try {
102
- const out = execSync(`${MEMPALACE_BIN} wake-up`, { encoding: "utf-8", timeout: 15000 }).trim();
147
+ // argv array, no shell.
148
+ const out = execFileSync(MEMPALACE_BIN, ["wake-up"], { encoding: "utf-8", timeout: 15000 }).trim();
103
149
  if (!out)
104
150
  return "";
105
151
  return out.slice(0, MAX_WAKEUP_CHARS);
@@ -133,7 +179,9 @@ function readIdentity() {
133
179
  }
134
180
  function mempalaceSearch(query) {
135
181
  try {
136
- const out = execSync(`${MEMPALACE_BIN} search "${query.replace(/"/g, '\\"')}" --results ${MAX_SEARCH_RESULTS}`, {
182
+ // argv array, no shell (see PR #2): the query is raw user message
183
+ // text, so it must never pass through /bin/sh. No manual escaping needed.
184
+ const out = execFileSync(MEMPALACE_BIN, ["search", query, "--results", String(MAX_SEARCH_RESULTS)], {
137
185
  encoding: "utf-8",
138
186
  timeout: 15000,
139
187
  }).trim();
@@ -162,7 +210,7 @@ function dbSync() {
162
210
  doDbSync();
163
211
  }
164
212
  catch (e) {
165
- log("sync err: " + String(e));
213
+ errLog("sync err: " + String(e));
166
214
  }
167
215
  }
168
216
  function backfillRequested() {
@@ -270,8 +318,10 @@ function markSynced(now) {
270
318
  // Official classification: decisions, preferences, milestones, problems,
271
319
  // emotional context. Agent tag keeps opencode-mined drawers attributable.
272
320
  // One wing per project (official multi-project pattern).
273
- function mineCommand(wingDir, wing) {
274
- return `${MEMPALACE_BIN} mine ${wingDir} --mode convos --extract general --agent opencode --wing ${wing}`;
321
+ // Argv array, no shell (see PR #2): wing names are sanitized, but the
322
+ // spawn path stays shell-free regardless.
323
+ function mineArgs(wingDir, wing) {
324
+ return ["mine", wingDir, "--mode", "convos", "--extract", "general", "--agent", "opencode", "--wing", wing];
275
325
  }
276
326
  function cleanupExport(wings) {
277
327
  for (const files of wings.values()) {
@@ -323,13 +373,16 @@ function doDbSync() {
323
373
  return;
324
374
  }
325
375
  const [wing, files] = entries[i];
326
- exec(mineCommand(join(OUT_DIR, wing), wing), {
376
+ // No timeout here by design (see PR #4): Node would kill only the
377
+ // wrapper shell and orphan the python mine process, which keeps
378
+ // holding the palace lock while the next mine piles up. miningLock
379
+ // already serializes concurrent mines; long mines run to completion.
380
+ execFile(MEMPALACE_BIN, mineArgs(join(OUT_DIR, wing), wing), {
327
381
  encoding: "utf-8",
328
- timeout: 300000,
329
382
  }, (err) => {
330
383
  if (err) {
331
384
  miningLock = false;
332
- log(`mine err (${wing}): ${err.message}`);
385
+ errLog(`mine err (${wing}): ${err.message}`);
333
386
  return;
334
387
  }
335
388
  log(`mined wing ${wing} (${files.length} sessions)`);
@@ -347,12 +400,12 @@ function exitSync() {
347
400
  return;
348
401
  for (const [wing] of wings) {
349
402
  log(`exit save: mining wing ${wing}`);
350
- const res = spawnSync(MEMPALACE_BIN, ["mine", join(OUT_DIR, wing), "--mode", "convos", "--extract", "general", "--agent", "opencode", "--wing", wing], {
403
+ const res = spawnSync(MEMPALACE_BIN, mineArgs(join(OUT_DIR, wing), wing), {
351
404
  encoding: "utf-8",
352
405
  timeout: 60000,
353
406
  });
354
407
  if (res.error || res.status !== 0) {
355
- log(`exit mine err (${wing}): ${String(res.error || res.status)}`);
408
+ errLog(`exit mine err (${wing}): ${String(res.error || res.status)}`);
356
409
  return;
357
410
  }
358
411
  }
@@ -361,7 +414,7 @@ function exitSync() {
361
414
  log("exit save done");
362
415
  }
363
416
  catch (e) {
364
- log("exit save err: " + String(e));
417
+ errLog("exit save err: " + String(e));
365
418
  }
366
419
  }
367
420
  export default (async () => {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "opencode-mempalace-persistence",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "OpenCode plugin — auto-sync conversations to MemPalace memory in real-time. No forced wings, KG extraction via MCP tools.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
- "dist"
8
+ "dist",
9
+ "skills"
9
10
  ],
10
11
  "scripts": {
11
12
  "build": "tsc",
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: mempalace-recall
3
+ description: Recall protocol for MemPalace — search the palace before answering about past work, people, projects, or prior decisions. Use when the user asks what was decided, what happened before, who someone is, what was discussed last time, or anything that may already be filed in their memory palace. Requires the mempalace MCP server (or the opencode-mempalace-persistence plugin).
4
+ ---
5
+
6
+ # MemPalace Recall
7
+
8
+ Search-before-answer protocol for MemPalace. Read the user's memory
9
+ palace before answering anything that may already be filed there,
10
+ instead of guessing from model memory.
11
+
12
+ If the `opencode-mempalace-persistence` plugin is active, identity and
13
+ relevant memories are usually already injected into the prompt — this
14
+ skill covers the cases where they are not, plus filing.
15
+
16
+ ## When to recall
17
+
18
+ Search the palace **before answering** whenever the user asks about
19
+ something that may already be filed:
20
+
21
+ - Past work or prior decisions — "what did we decide / try / do?"
22
+ - A person, project, or entity — "who is …", "what is …"
23
+ - An earlier session — "remember when …", "last time …"
24
+ - A preference or fact that could have changed over time
25
+
26
+ Do **not** search on pure greenfield work with no memory relevance
27
+ (e.g. "rename this variable", "fix this typo"). Recall is
28
+ question-driven, not reflexive — a search on every turn wastes latency.
29
+
30
+ ## Protocol
31
+
32
+ 1. Before responding about people / projects / past events / prior
33
+ decisions: call `mempalace_search` first (short natural-language
34
+ query, optional `wing` / `room` filters, `limit` default 5). Use
35
+ `mempalace_kg_query` for relational or time-bound facts.
36
+ 2. If unsure about a fact: say "let me check the palace" and query.
37
+ 3. Return the drawer's **verbatim** text. Never summarize or paraphrase
38
+ stored content — quoting the exact words is the point of the system.
39
+ 4. After a substantive session, record continuity with
40
+ `mempalace_diary_write` (skip if a checkpoint hook already saved).
41
+ 5. File durable outcomes with `mempalace_add_drawer`; new KG facts with
42
+ `mempalace_kg_add` (128 chars or fewer); single-valued replacements
43
+ with `mempalace_kg_supersede`; ended facts with
44
+ `mempalace_kg_invalidate`.
45
+
46
+ ## Tool names in OpenCode
47
+
48
+ When the MCP server is registered in OpenCode as `mempalace`, every
49
+ tool carries a double prefix: `mempalace_mempalace_search`,
50
+ `mempalace_mempalace_kg_query`, `mempalace_mempalace_diary_write`, and
51
+ so on. (In Claude Code the same tools have a single `mempalace_`
52
+ prefix.)
53
+
54
+ ## Unhappy paths
55
+
56
+ - **Empty results.** Say the palace has nothing on this; do not invent
57
+ an answer. Offer to widen the search or file the new information.
58
+ - **MCP error / server down.** Surface the error and suggest
59
+ `mempalace status`. Never fall back to guessing.
60
+ - **Conflicting facts.** Trust the knowledge graph's time-valid answer.
61
+
62
+ ## References
63
+
64
+ - MemPalace: <https://github.com/MemPalace/mempalace>
65
+ - Recall protocol: <https://github.com/MemPalace/mempalace/blob/main/integrations/shared/recall-protocol.md>
66
+
67
+ Derived from the official `mempalace-recall` skill (MIT), adapted for
68
+ OpenCode and the opencode-mempalace-persistence plugin.