opencode-mempalace-persistence 1.0.1 → 1.1.1

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.
Files changed (3) hide show
  1. package/README.md +81 -28
  2. package/dist/index.js +30 -107
  3. package/package.json +12 -4
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # opencode-mempalace-persistence
2
2
 
3
- An OpenCode plugin that automatically saves every conversation to MemPalace and uses stored memory to provide better, context-aware responses. Real-time, zero cron, zero external scripts.
3
+ > **Community plugin** not officially maintained by the MemPalace team. Fully open source, ~120 lines of TypeScript.
4
4
 
5
- > **It just works** install, use OpenCode, the plugin handles the rest.
5
+ An OpenCode plugin that automatically saves every conversation to MemPalace and uses stored memory to provide better, context-aware responses. Real-time, zero cron, zero external scripts.
6
6
 
7
7
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
8
 
@@ -14,7 +14,7 @@ An OpenCode plugin that automatically saves every conversation to MemPalace and
14
14
  |---|---|
15
15
  | Every session starts from scratch | The model knows who you are and what you've done |
16
16
  | You repeat context each time | Memory is automatic |
17
- | No Knowledge Graph | Decisions, milestones, problems tracked |
17
+ | Model starts from scratch each time | Memory persists across sessions |
18
18
 
19
19
  The model searches MemPalace on every question (via AGENTS.md + MCP), and the plugin saves every response. A perfect feedback loop.
20
20
 
@@ -39,11 +39,40 @@ Create `~/.config/opencode/AGENTS.md`:
39
39
  ```markdown
40
40
  # Memory & Knowledge instructions
41
41
 
42
- Before answering the user, always search your MemPalace memory for relevant context using the MCP tools.
42
+ ## CRITICAL: You MUST follow these steps BEFORE every response.
43
+
44
+ ### Step 1 — Search MemPalace
45
+ Call `mempalace_mempalace_search` with the user's question or key topics as query. Get the top 5-10 most relevant memory drawers.
46
+ **This is mandatory. Never skip this step. No exceptions.** Every question — even seemingly trivial ones — may relate to past conversations, projects, or preferences you've forgotten. Searching costs ~nothing; skipping costs continuity.
47
+
48
+ ### Step 2 — Query Knowledge Graph
49
+ 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.
50
+
51
+ ### Step 3 — Use the context
52
+ Incorporate relevant memory and facts into your response. If the user's question relates to a past conversation, project, or decision, reference it naturally. If the search returned nothing useful, you are done — do NOT skip steps 1-2 just because you *expect* nothing.
53
+
54
+ ### Naming reminder
55
+ All MemPalace tools use the prefix `mempalace_mempalace_*` (not `mempalace_*`). Examples:
56
+ - `mempalace_mempalace_search` (NOT `mempalace_search`)
57
+ - `mempalace_mempalace_kg_query` (NOT `mempalace_kg_query`)
58
+ - `mempalace_mempalace_add_drawer`
59
+ - `mempalace_mempalace_get_drawer`
60
+ - `mempalace_mempalace_list_wings`
61
+ If you ever catch yourself typing `mempalace_search` or `mempalace_kg_query`, STOP — the correct prefix is `mempalace_mempalace_`.
62
+
63
+ ## Note
43
64
 
44
- 1. **Search MemPalace**: Call `mempalace_search` with the user's question or key topics as query. Get the top 5-10 most relevant memory drawers.
45
- 2. **Query Knowledge Graph**: Call `mempalace_kg_query` for entity "user". Then filter the returned facts — keep only those whose text contains keywords from the user's question.
46
- 3. **Use the context**: Incorporate relevant memory and facts into your response, referencing the source naturally when useful.
65
+ The KG stores structured facts like: decision (choices made), milestone (completed tasks), problem (issues encountered), preference (likes/dislikes), emotional (feelings). Use these to understand the user's history and context better.
66
+
67
+ ### Knowledge Graph management
68
+
69
+ When you discover structured facts during conversation (decisions made, milestones reached, problems encountered, preferences expressed, emotional states), record them in the Knowledge Graph:
70
+
71
+ - **New facts**: Call `mempalace_mempalace_kg_add` with subject → predicate → object (e.g. `subject="user"`, `predicate="preference"`, `object="prefers TypeScript over Python"`)
72
+ - **Changed facts**: First call `mempalace_mempalace_kg_invalidate` on the old fact, then `mempalace_mempalace_kg_add` for the new one
73
+ - **Retrieval**: Call `mempalace_mempalace_kg_query` for entity "user" to see all known facts
74
+
75
+ This is optional but recommended — the more facts you record, the better the model understands the user's history and preferences.
47
76
  ```
48
77
 
49
78
  ### 3. Identity (who you are)
@@ -78,8 +107,10 @@ This is loaded automatically at session start via `instructions` in opencode.jso
78
107
  ### 5. MemPalace (if not already installed)
79
108
 
80
109
  ```bash
81
- # Install
82
- pipx install mempalace
110
+ # Install (requires mempalace>=3.3.5 for HNSW corruption fix)
111
+ uv tool install "mempalace>=3.3.5"
112
+ # or
113
+ pipx install "mempalace>=3.3.5"
83
114
 
84
115
  # Create palace
85
116
  mempalace init ~/opencode-memory
@@ -102,8 +133,8 @@ You ask a question
102
133
 
103
134
  The model responds
104
135
  → The opencode-mempalace-persistence plugin detects the response is complete
105
- → Saves the conversation to MemPalace
106
- Extracts Knowledge Graph facts
136
+ → Saves the conversation to MemPalace (flat export, no hardcoded wings)
137
+ The model may record KG facts via MCP tools (optional, per-session)
107
138
 
108
139
  Next time you ask
109
140
  → The model finds the previous memory → coherent responses
@@ -114,15 +145,7 @@ Next time you ask
114
145
 
115
146
  ## What gets saved
116
147
 
117
- Every turn (question + answer):
118
-
119
- - **Text** categorized by wing (developer, creative, emotions, family, consciousness)
120
- - **Knowledge Graph**: automatically extracted facts
121
- - `decision` → "decided to use TypeScript"
122
- - `milestone` → "backend deploy completed"
123
- - `problem` → "chromadb ModuleNotFoundError"
124
- - `preference` → "prefer Svelte over React"
125
- - `emotional` → "frustrated with Docker compose"
148
+ Every turn (question + answer) is saved as a drawer in MemPalace. No forced categorization — MemPalace's own mining handles organization. The model can optionally record KG facts (decisions, milestones, preferences) during conversation via MCP tools.
126
149
 
127
150
  ---
128
151
 
@@ -137,15 +160,13 @@ Every turn (question + answer):
137
160
  │ Query OpenCode DB │
138
161
  │ (messages since lastSync)│
139
162
  │ ↓ │
140
- Categorize by wing
141
-
142
- │ Export delta → tmp │
163
+ Export sessions → flat
164
+ (no wing subdirs)
143
165
  │ ↓ │
144
166
  │ Save state immediately │
145
167
  │ ↓ │
146
- │ mempalace mine (async) │ ← non-blocking
147
-
148
- │ Extract KG facts │
168
+ │ mempalace mine (async) │
169
+ single call, serialized
149
170
  │ ↓ │
150
171
  Session idle ─►│ session.idle hook │
151
172
  │ (saves last turn) │
@@ -155,7 +176,7 @@ Every turn (question + answer):
155
176
  ┌──────────────────────────┐
156
177
  │ MemPalace │
157
178
  │ ~/opencode-memory/ │
158
- │ Vector DB + KG SQLite
179
+ │ Vector DB
159
180
  └──────────────────────────┘
160
181
 
161
182
 
@@ -163,7 +184,9 @@ Every turn (question + answer):
163
184
  │ AGENTS.md + MCP │
164
185
  │ The model searches │
165
186
  │ MemPalace on every │
166
- │ question
187
+ │ question. Optionally
188
+ │ records KG facts via │
189
+ │ kg_add / kg_invalidate │
167
190
  └──────────────────────────┘
168
191
  ```
169
192
 
@@ -209,6 +232,36 @@ When set, the plugin writes a debug log to `/tmp/opencode-mempalace.log`.
209
232
 
210
233
  ---
211
234
 
235
+ ## Recommendations
236
+
237
+ ### Model choice affects memory retrieval reliability
238
+
239
+ Empirical data from the MemPalace community (Cat-9a diagnostic) shows that the model's tool-use discipline significantly impacts how reliably `mempalace_search` is invoked:
240
+
241
+ | Model | Skips search | Mean recall |
242
+ |-------|:-:|:-:|
243
+ | gemma4:e4b (4B) | **60%** | 0.417 |
244
+ | qwen3.5:4b (4B, Tau2-tuned) | **13%** | 0.717 |
245
+
246
+ For reliable read-side memory retrieval, **Qwen 3.5 4B+ or equivalent** is recommended as the minimum orchestrator. Smaller or older models may skip memory search on most questions regardless of AGENTS.md instructions.
247
+
248
+ ### Forced invocation (belt and suspenders)
249
+
250
+ A plugin-level config flag that injects a mandatory `mempalace_search` directive into the system prompt (on top of AGENTS.md) can recover ~15pp of recall on low-discipline models. This is being evaluated as a future config option — suggestions welcome.
251
+
252
+ ### Complementary: upstream OpenCode source adapter
253
+
254
+ The MemPalace project has an upstream PR ([#1484](https://github.com/MemPalace/mempalace/pull/1484)) adding `mempalace mine --source opencode` — a pull-based adapter for retrospective ingest of existing OpenCode sessions. This plugin (push, real-time) and the adapter (pull, backfill) are complementary:
255
+
256
+ | Approach | Direction | Captures |
257
+ |----------|-----------|----------|
258
+ | This plugin | Push | Live conversation turns |
259
+ | `mempalace mine --source opencode` (PR #1484) | Pull | Existing OpenCode session files |
260
+
261
+ For full coverage: install this plugin for live capture, run `mempalace mine --source opencode` once for backfill, never think about it again.
262
+
263
+ ---
264
+
212
265
  ## License
213
266
 
214
267
  MIT
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execSync, exec } from "child_process";
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, rmdirSync, unlinkSync, appendFileSync } from "fs";
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";
@@ -8,7 +8,6 @@ const VENV_PYTHON = join(HOME, ".local/share/pipx/venvs/mempalace/bin/python3");
8
8
  const MEMPALACE_BIN = join(HOME, ".local/bin/mempalace");
9
9
  const OPENCODE_DB = join(HOME, ".local/share/opencode/opencode.db");
10
10
  const STATE_FILE = join(HOME, ".mempalace/sync_state.json");
11
- const KG_DB = join(HOME, ".mempalace/knowledge_graph.sqlite3");
12
11
  const OUT_DIR = "/tmp/oc-sessions";
13
12
  const TMP_SCRIPT = "/tmp/oc-plugin-query.py";
14
13
  const DEBUG = !!process.env.OPENCODE_MEMPALACE_DEBUG;
@@ -22,7 +21,7 @@ function log(msg) {
22
21
  }
23
22
  catch { }
24
23
  }
25
- let pendingSync = false;
24
+ let miningLock = false;
26
25
  let lastSyncTs = 0;
27
26
  function runPython(code) {
28
27
  writeFileSync(TMP_SCRIPT, code);
@@ -33,29 +32,6 @@ function runPython(code) {
33
32
  unlinkSync(TMP_SCRIPT);
34
33
  }
35
34
  }
36
- const WING_KEYWORDS = {
37
- creative: ["eurovision", "music", "art", "film", "game", "festival", "song", "concert"],
38
- family: ["family", "kid", "son", "daughter", "wife", "husband", "parent", "mother", "father"],
39
- emotions: ["feel", "happy", "sad", "love", "hate", "angry", "fear", "anxiety", "grateful"],
40
- consciousness: ["think", "mind", "conscious", "self", "identity", "exist", "meaning"],
41
- };
42
- const KG_PATTERNS = {
43
- decision: ["ho deciso", "ho scelto", "implementato", "added ", "ho aggiunto", "decided", "chosen", "created", "creato", "modified"],
44
- milestone: ["completato", "finito", "rilasciato", "deployato", "ho completato", "successo", "funziona", "completed", "done", "deployed"],
45
- problem: ["problema", "bug", "errore", "issue", "fix", "fixato", "risolto", "solved", "fixed", "error"],
46
- preference: ["preferisco", "mi piace", "non mi piace", "prefer", "like to", "better"],
47
- emotional: ["sentimento", "emozione", "frustrato", "contento", "happy", "sad", "feel", "emotion"],
48
- };
49
- function categorize(text) {
50
- if (!text)
51
- return "developer";
52
- const lower = text.toLowerCase();
53
- for (const [wing, keywords] of Object.entries(WING_KEYWORDS)) {
54
- if (keywords.some((kw) => lower.includes(kw)))
55
- return wing;
56
- }
57
- return "developer";
58
- }
59
35
  function hasText(parts) {
60
36
  return parts
61
37
  .filter((p) => p?.type === "text" && p?.text?.trim())
@@ -73,9 +49,8 @@ function getLastSync() {
73
49
  }
74
50
  }
75
51
  function dbSync() {
76
- if (pendingSync)
52
+ if (miningLock)
77
53
  return;
78
- pendingSync = true;
79
54
  try {
80
55
  doDbSync();
81
56
  }
@@ -106,17 +81,15 @@ print(json.dumps(rows))
106
81
  sessionsArr = JSON.parse(sessions);
107
82
  }
108
83
  catch {
109
- pendingSync = false;
110
84
  return;
111
85
  }
112
- if (!sessionsArr || sessionsArr.length === 0) {
113
- pendingSync = false;
86
+ if (!sessionsArr || sessionsArr.length === 0)
114
87
  return;
115
- }
116
88
  const now = Date.now();
117
89
  const exported = [];
90
+ mkdirSync(OUT_DIR, { recursive: true });
118
91
  for (const sess of sessionsArr) {
119
- const [sessId, title, worktree, directory] = sess;
92
+ const [sessId, title] = sess;
120
93
  const label = (title || "").replace(/[^a-zA-Z0-9 _-]/g, "_") || (sessId || "").slice(0, 12);
121
94
  const prefix = `${new Date().toISOString().slice(0, 10)}_${label.slice(0, 30)}_${(sessId || "").slice(0, 8)}`;
122
95
  const msgs = runPython(`
@@ -158,7 +131,7 @@ print(json.dumps(texts))
158
131
  ];
159
132
  for (const m of msgList) {
160
133
  const ts = m.ts ? new Date(m.ts).toISOString().slice(11, 19) : "";
161
- lines.push(`## ${m.role.toUpperCase()} ${ts}`);
134
+ lines.push(`## ${m.role.toUpperCase()} \u2014 ${ts}`);
162
135
  lines.push("");
163
136
  lines.push(m.text);
164
137
  lines.push("");
@@ -166,88 +139,38 @@ print(json.dumps(texts))
166
139
  const content = lines.join("\n").trim();
167
140
  if (!content)
168
141
  continue;
169
- const wing = categorize(content);
170
142
  const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 12);
171
143
  const fname = `sync_${prefix}_${contentHash}.txt`;
172
- const wingDir = join(OUT_DIR, wing);
173
- mkdirSync(wingDir, { recursive: true });
174
- writeFileSync(join(wingDir, fname), content + "\n");
175
- exported.push({ wing, fname, dir: wingDir });
144
+ writeFileSync(join(OUT_DIR, fname), content + "\n");
145
+ exported.push(join(OUT_DIR, fname));
176
146
  }
177
- if (exported.length === 0) {
178
- pendingSync = false;
147
+ if (exported.length === 0)
179
148
  return;
180
- }
181
- // Save state immediately so next sync doesn't re-process
182
149
  writeFileSync(STATE_FILE, JSON.stringify({ last_sync_ms: now }));
183
150
  lastSyncTs = Date.now();
184
- pendingSync = false;
185
- log(`queued ${exported.length} files for mining`);
186
- // Mining async (fire-and-forget, non-blocking)
187
- for (const { wing, fname, dir: wingDir } of exported) {
188
- const filePath = join(wingDir, fname);
189
- exec(`${MEMPALACE_BIN} mine ${wingDir} --mode convos --extract general --wing ${wing}`, {
190
- encoding: "utf-8",
191
- timeout: 120000,
192
- }, (err) => {
193
- if (err) {
194
- log(`mine err ${wing}: ${err.message}`);
195
- return;
196
- }
197
- rmSync(filePath);
198
- try {
199
- rmdirSync(wingDir);
200
- }
201
- catch { }
202
- log(`mined ${wing}`);
203
- // KG extraction after mining
151
+ miningLock = true;
152
+ log(`mining ${exported.length} sessions`);
153
+ exec(`${MEMPALACE_BIN} mine ${OUT_DIR} --mode convos`, {
154
+ encoding: "utf-8",
155
+ timeout: 120000,
156
+ }, (err) => {
157
+ miningLock = false;
158
+ if (err) {
159
+ log(`mine err: ${err.message}`);
160
+ return;
161
+ }
162
+ for (const f of exported) {
204
163
  try {
205
- const raw = runPython(`
206
- from mempalace.config import MempalaceConfig
207
- import chromadb, json, re
208
- c = MempalaceConfig()
209
- col = chromadb.PersistentClient(path=c.palace_path).get_collection(c.collection_name)
210
- d = col.get(limit=5000, include=["metadatas","documents"])
211
- p = ${JSON.stringify(KG_PATTERNS)}
212
- r = []
213
- for i,m,doc in zip(d.get("ids",[]), d.get("metadatas",[]), d.get("documents",[])):
214
- if not m or not doc: continue
215
- rm = m.get("room","")
216
- if rm not in ("decision","milestone","problem","preference","emotional"): continue
217
- da = doc.split("Date: ")[1][:10] if "Date: " in doc else ""
218
- if not da: continue
219
- if not any(pat in doc.lower() for pat in p.get(rm,[])): continue
220
- for ln in doc.strip().split("\\n")[:5]:
221
- l = ln.strip()
222
- if l and not l.startswith("#") and len(l) > 20 and len(re.findall(r"[^a-zA-Z0-9\\s]", l))/len(l) <= 0.3:
223
- r.append([da, rm, l[:120]]); break
224
- print(json.dumps(r))
225
- `);
226
- const nf = JSON.parse(raw) || [];
227
- if (nf.length > 0) {
228
- const ex = runPython(`import sqlite3,json;d=sqlite3.connect(${JSON.stringify(KG_DB)});r=d.execute("SELECT predicate,object FROM triples WHERE subject='user'").fetchall();d.close();print(json.dumps(r))`);
229
- const seen = new Set();
230
- try {
231
- for (const [p, o] of JSON.parse(ex))
232
- seen.add(p + "::" + o);
233
- }
234
- catch { }
235
- const ins = [];
236
- for (const [d, t, x] of nf) {
237
- const k = t + "::" + x;
238
- if (seen.has(k))
239
- continue;
240
- seen.add(k);
241
- ins.push(`INSERT OR IGNORE INTO triples(id,subject,predicate,object,valid_from,confidence,extracted_at) VALUES('t_user_${t}_${createHash("sha256").update(x).digest("hex").slice(0, 12)}','user','${t}','${x.replace(/'/g, "''")}','${d}',1.0,datetime('now'))`);
242
- }
243
- if (ins.length > 0) {
244
- runPython(`import sqlite3;db=sqlite3.connect(${JSON.stringify(KG_DB)});[db.execute(s) for s in ${JSON.stringify(ins)}];db.commit();db.close()`);
245
- }
246
- }
164
+ unlinkSync(f);
247
165
  }
248
166
  catch { }
249
- });
250
- }
167
+ }
168
+ try {
169
+ rmdirSync(OUT_DIR);
170
+ }
171
+ catch { }
172
+ log("mine done");
173
+ });
251
174
  }
252
175
  export default (async () => {
253
176
  mkdirSync(OUT_DIR, { recursive: true });
package/package.json CHANGED
@@ -1,15 +1,23 @@
1
1
  {
2
2
  "name": "opencode-mempalace-persistence",
3
- "version": "1.0.1",
4
- "description": "OpenCode plugin — auto-sync conversations to MemPalace memory in real-time with auto-categorization and Knowledge Graph",
3
+ "version": "1.1.1",
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
- "files": ["dist"],
7
+ "files": [
8
+ "dist"
9
+ ],
8
10
  "scripts": {
9
11
  "build": "tsc",
10
12
  "prepublishOnly": "npm run build"
11
13
  },
12
- "keywords": ["opencode", "mempalace", "memory", "opencode-plugin", "typescript"],
14
+ "keywords": [
15
+ "opencode",
16
+ "mempalace",
17
+ "memory",
18
+ "opencode-plugin",
19
+ "typescript"
20
+ ],
13
21
  "author": "geco",
14
22
  "license": "MIT",
15
23
  "type": "module",