opencode-mempalace-persistence 2.1.0 → 2.3.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 +19 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +140 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,12 @@ An OpenCode plugin that automatically saves every conversation to MemPalace and
|
|
|
6
6
|
|
|
7
7
|
Follows the official MemPalace automation pattern (same as the Claude Code hooks): the plugin decides **when** to save, the model decides **what** to file via the MemPalace MCP tools.
|
|
8
8
|
|
|
9
|
+
[](https://www.npmjs.com/package/opencode-mempalace-persistence)
|
|
10
|
+
[](https://www.npmjs.com/package/opencode-mempalace-persistence)
|
|
9
11
|
[](LICENSE)
|
|
10
12
|
|
|
13
|
+

|
|
14
|
+
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
## How it works in 3 seconds
|
|
@@ -170,12 +174,12 @@ You ask a question
|
|
|
170
174
|
→ Model files topics/decisions/quotes via MCP tools, then answers
|
|
171
175
|
|
|
172
176
|
The model responds
|
|
173
|
-
→
|
|
174
|
-
→ Saves the conversation to MemPalace (flat export, no hardcoded wings)
|
|
177
|
+
→ Once the turn completes, the next idle/exit/startup mines it to MemPalace (flat export, no hardcoded wings)
|
|
175
178
|
→ Model records new KG facts via MCP tools (only when something new emerged)
|
|
176
179
|
|
|
177
180
|
Session goes idle / process exits
|
|
178
181
|
→ Background mine of everything new since last sync
|
|
182
|
+
→ TUI toast confirms what was mined (disable with `"toasts": false`)
|
|
179
183
|
|
|
180
184
|
Compaction starts
|
|
181
185
|
→ [MemPalace Pre-Compact Emergency Save]: model files everything first
|
|
@@ -190,7 +194,7 @@ Next time you ask
|
|
|
190
194
|
|
|
191
195
|
## What gets saved
|
|
192
196
|
|
|
193
|
-
Every turn (question + answer) is saved as a drawer in MemPalace.
|
|
197
|
+
Every turn (question + answer) is saved as a drawer in MemPalace. Mining runs with `--mode convos` (default `exchange` extraction: one drawer per exchange pair, verbatim, no paraphrasing). Exports are grouped one wing per project (official multi-project pattern: `bot-oc` sessions land in wing `bot-oc`, never leaking across projects). Only completed turns are exported (in-flight replies are revisited by the next sync). The model additionally records KG facts (decisions, milestones, preferences) during conversation and at each checkpoint via MCP tools.
|
|
194
198
|
|
|
195
199
|
### Backfill existing sessions
|
|
196
200
|
|
|
@@ -218,18 +222,16 @@ The plugin exports everything in the opencode database on the next sync, then re
|
|
|
218
222
|
│ ↓ │
|
|
219
223
|
│ Model sees context → answers │
|
|
220
224
|
│ ↓ │
|
|
221
|
-
Answer done ──►│ chat.message (count) + session.idle
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
│ single serialized call │
|
|
232
|
-
└──────────────────────────────┘
|
|
225
|
+
Answer done ──►│ chat.message (count) + session.idle │
|
|
226
|
+
│ mine on idle / exit / startup │
|
|
227
|
+
│ ↓ │
|
|
228
|
+
│ Query OpenCode DB (completed turns) │
|
|
229
|
+
│ ↓ │
|
|
230
|
+
│ Export → flat text files (0700) │
|
|
231
|
+
│ ↓ │
|
|
232
|
+
│ mempalace mine --mode convos │
|
|
233
|
+
│ single serialized call │
|
|
234
|
+
└──────────────────────────────────────────┘
|
|
233
235
|
│
|
|
234
236
|
▼
|
|
235
237
|
┌──────────────────────────┐
|
|
@@ -258,7 +260,8 @@ The plugin exports everything in the opencode database on the next sync, then re
|
|
|
258
260
|
| `~/.config/opencode/skills/mempalace-recall/SKILL.md` | Bundled recall skill (copy from `skills/` in this repo) |
|
|
259
261
|
| `~/.mempalace/identity.txt` | Your identity (injected by plugin) |
|
|
260
262
|
| `~/.mempalace/hook_state/opencode_counters.json` | Per-session message counters (checkpoint cadence) |
|
|
261
|
-
| `~/.mempalace/hook_state/hook.log` | Checkpoint / pre-compact event log |
|
|
263
|
+
| `~/.mempalace/hook_state/hook.log` | Checkpoint / pre-compact event log (errors always land here) |
|
|
264
|
+
| `~/.mempalace/oc-sessions/` | Private (0700) export workspace for pending transcripts |
|
|
262
265
|
| `~/.mempalace/config.json` | MemPalace config (palace path) |
|
|
263
266
|
| `~/.mempalace/knowledge_graph.sqlite3` | Knowledge Graph (structured facts) |
|
|
264
267
|
| `~/opencode-memory/` | MemPalace vector DB (all drawers) |
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -12,8 +12,11 @@ const IDENTITY_FILE = join(HOME, ".mempalace/identity.txt");
|
|
|
12
12
|
const HOOK_STATE_DIR = join(HOME, ".mempalace/hook_state");
|
|
13
13
|
const COUNTERS_FILE = join(HOOK_STATE_DIR, "opencode_counters.json");
|
|
14
14
|
const HOOK_LOG = join(HOOK_STATE_DIR, "hook.log");
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Private sync workspace (0700): transcripts contain conversation text,
|
|
16
|
+
// so they must never sit world-readable in /tmp (see PR #1524 review).
|
|
17
|
+
const SYNC_DIR = join(HOME, ".mempalace/oc-sessions");
|
|
18
|
+
const OUT_DIR = SYNC_DIR;
|
|
19
|
+
const TMP_SCRIPT = join(SYNC_DIR, "oc-plugin-query.py");
|
|
17
20
|
const DEBUG = !!process.env.OPENCODE_MEMPALACE_DEBUG;
|
|
18
21
|
const LOG_FILE = "/tmp/opencode-mempalace.log";
|
|
19
22
|
const MAX_INJECT_CHARS = 900;
|
|
@@ -43,6 +46,29 @@ function errLog(msg) {
|
|
|
43
46
|
log("ERROR: " + msg);
|
|
44
47
|
hookLog("ERROR: " + msg);
|
|
45
48
|
}
|
|
49
|
+
function toastsEnabled() {
|
|
50
|
+
try {
|
|
51
|
+
const raw = readFileSync(PLUGIN_CONFIG, "utf-8");
|
|
52
|
+
const v = JSON.parse(raw)?.toasts;
|
|
53
|
+
if (v === false)
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// TUI toast client (set by the factory). Fire-and-forget: headless runs
|
|
60
|
+
// (`opencode run`, no TUI attached) must never break on this.
|
|
61
|
+
let tuiClient = null;
|
|
62
|
+
function toast(variant, title, message) {
|
|
63
|
+
if (!toastsEnabled() || !tuiClient?.tui?.showToast)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
const p = tuiClient.tui.showToast({ body: { title, message, variant, duration: 5000 } });
|
|
67
|
+
if (p && typeof p.catch === "function")
|
|
68
|
+
p.catch(() => { });
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
}
|
|
46
72
|
// Probe for a working Python interpreter at startup instead of hardcoding
|
|
47
73
|
// one installer layout (pipx vs uv tool vs system). runPython only needs
|
|
48
74
|
// stdlib (sqlite3/json), so any python3 works. Priority: explicit env
|
|
@@ -87,7 +113,8 @@ function runPython(code) {
|
|
|
87
113
|
const python = resolvePython();
|
|
88
114
|
if (!python)
|
|
89
115
|
throw new Error("no working Python interpreter (see hook.log)");
|
|
90
|
-
|
|
116
|
+
mkdirSync(SYNC_DIR, { recursive: true, mode: 0o700 });
|
|
117
|
+
writeFileSync(TMP_SCRIPT, code, { mode: 0o600 });
|
|
91
118
|
try {
|
|
92
119
|
// argv array, no shell (see PR #2): paths here are fixed, never user input.
|
|
93
120
|
return execFileSync(python, [TMP_SCRIPT], { encoding: "utf-8", timeout: 30000 }).trim();
|
|
@@ -99,6 +126,39 @@ function runPython(code) {
|
|
|
99
126
|
catch { }
|
|
100
127
|
}
|
|
101
128
|
}
|
|
129
|
+
// Resolve the mempalace CLI without hardcoding one installer layout:
|
|
130
|
+
// explicit env override, PATH lookup (cross-platform, incl. Windows),
|
|
131
|
+
// legacy ~/.local/bin fallback.
|
|
132
|
+
let resolvedBin = undefined;
|
|
133
|
+
function resolveBin() {
|
|
134
|
+
if (resolvedBin !== undefined)
|
|
135
|
+
return resolvedBin;
|
|
136
|
+
const envBin = process.env.MEMPALACE_BIN;
|
|
137
|
+
if (envBin && existsSync(envBin)) {
|
|
138
|
+
resolvedBin = envBin;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
try {
|
|
142
|
+
const found = execSync(process.platform === "win32" ? "where mempalace" : "command -v mempalace", {
|
|
143
|
+
encoding: "utf-8", timeout: 10000,
|
|
144
|
+
}).trim().split(/\r?\n/)[0]?.trim();
|
|
145
|
+
if (found)
|
|
146
|
+
resolvedBin = found;
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
if (!resolvedBin && existsSync(MEMPALACE_BIN))
|
|
150
|
+
resolvedBin = MEMPALACE_BIN;
|
|
151
|
+
if (!resolvedBin)
|
|
152
|
+
resolvedBin = null;
|
|
153
|
+
}
|
|
154
|
+
if (resolvedBin) {
|
|
155
|
+
log("using mempalace: " + resolvedBin);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
errLog("mempalace CLI not found (tried MEMPALACE_BIN env, PATH, ~/.local/bin) — search/wake-up/mine disabled");
|
|
159
|
+
}
|
|
160
|
+
return resolvedBin;
|
|
161
|
+
}
|
|
102
162
|
function hasText(parts) {
|
|
103
163
|
return parts
|
|
104
164
|
.filter((p) => p?.type === "text" && p?.text?.trim())
|
|
@@ -143,9 +203,12 @@ function persistCounters(counters) {
|
|
|
143
203
|
}
|
|
144
204
|
}
|
|
145
205
|
function mempalaceWakeup() {
|
|
206
|
+
const bin = resolveBin();
|
|
207
|
+
if (!bin)
|
|
208
|
+
return "";
|
|
146
209
|
try {
|
|
147
210
|
// argv array, no shell.
|
|
148
|
-
const out = execFileSync(
|
|
211
|
+
const out = execFileSync(bin, ["wake-up"], { encoding: "utf-8", timeout: 15000 }).trim();
|
|
149
212
|
if (!out)
|
|
150
213
|
return "";
|
|
151
214
|
return out.slice(0, MAX_WAKEUP_CHARS);
|
|
@@ -178,10 +241,13 @@ function readIdentity() {
|
|
|
178
241
|
}
|
|
179
242
|
}
|
|
180
243
|
function mempalaceSearch(query) {
|
|
244
|
+
const bin = resolveBin();
|
|
245
|
+
if (!bin)
|
|
246
|
+
return "";
|
|
181
247
|
try {
|
|
182
248
|
// argv array, no shell (see PR #2): the query is raw user message
|
|
183
249
|
// text, so it must never pass through /bin/sh. No manual escaping needed.
|
|
184
|
-
const out = execFileSync(
|
|
250
|
+
const out = execFileSync(bin, ["search", query, "--results", String(MAX_SEARCH_RESULTS)], {
|
|
185
251
|
encoding: "utf-8",
|
|
186
252
|
timeout: 15000,
|
|
187
253
|
}).trim();
|
|
@@ -245,8 +311,11 @@ print(json.dumps(rows))
|
|
|
245
311
|
if (!sessionsArr || sessionsArr.length === 0)
|
|
246
312
|
return { wings: new Map(), now: Date.now() };
|
|
247
313
|
const now = Date.now();
|
|
314
|
+
// Never advance the cursor past an in-flight reply: anything skipped
|
|
315
|
+
// as incomplete is revisited by the next sync (idle/exit/startup).
|
|
316
|
+
let cursor = now;
|
|
248
317
|
const wings = new Map();
|
|
249
|
-
mkdirSync(OUT_DIR, { recursive: true });
|
|
318
|
+
mkdirSync(OUT_DIR, { recursive: true, mode: 0o700 });
|
|
250
319
|
for (const sess of sessionsArr) {
|
|
251
320
|
const [sessId, title, , directory] = sess;
|
|
252
321
|
const wing = ((directory || "").split("/").filter(Boolean).pop() || "global")
|
|
@@ -262,10 +331,20 @@ rows = db.execute("""
|
|
|
262
331
|
ORDER BY m.time_created
|
|
263
332
|
""").fetchall()
|
|
264
333
|
texts = []
|
|
334
|
+
incomplete = []
|
|
265
335
|
for (mid, mts, mdata_raw) in rows:
|
|
266
336
|
try: mdata = json.loads(mdata_raw)
|
|
267
337
|
except: mdata = {}
|
|
268
338
|
role = mdata.get("role", "unknown")
|
|
339
|
+
# Completion tracking (see PR #1524 review): the assistant message row
|
|
340
|
+
# is created when a reply STARTS, parts stream in afterwards, and
|
|
341
|
+
# finish is set only on completion. Exporting mid-reply would
|
|
342
|
+
# snapshot partial parts while the cursor advances past the message
|
|
343
|
+
# timestamp — losing the rest of the reply forever. So assistant
|
|
344
|
+
# messages without finish are skipped and revisited next sync.
|
|
345
|
+
if role == "assistant" and not mdata.get("finish"):
|
|
346
|
+
incomplete.append(mts)
|
|
347
|
+
continue
|
|
269
348
|
for (pdata_raw,) in db.execute("SELECT data FROM part WHERE message_id = ? ORDER BY time_created", (mid,)).fetchall():
|
|
270
349
|
try:
|
|
271
350
|
pdata = json.loads(pdata_raw)
|
|
@@ -273,16 +352,22 @@ for (mid, mts, mdata_raw) in rows:
|
|
|
273
352
|
texts.append({"role": role, "text": pdata.get("text").strip(), "ts": mts})
|
|
274
353
|
except: pass
|
|
275
354
|
db.close()
|
|
276
|
-
print(json.dumps(texts))
|
|
355
|
+
print(json.dumps({"texts": texts, "incomplete": incomplete}))
|
|
277
356
|
`);
|
|
278
357
|
let msgList;
|
|
358
|
+
let incompleteTs = [];
|
|
279
359
|
try {
|
|
280
|
-
|
|
360
|
+
const parsed = JSON.parse(msgs);
|
|
361
|
+
msgList = parsed.texts;
|
|
362
|
+
incompleteTs = parsed.incomplete || [];
|
|
281
363
|
}
|
|
282
364
|
catch {
|
|
283
365
|
continue;
|
|
284
366
|
}
|
|
285
|
-
if (
|
|
367
|
+
if (incompleteTs.length > 0) {
|
|
368
|
+
cursor = Math.min(cursor, Math.min(...incompleteTs) - 1);
|
|
369
|
+
}
|
|
370
|
+
if (msgList.length < 2 && incompleteTs.length === 0)
|
|
286
371
|
continue;
|
|
287
372
|
const lines = [
|
|
288
373
|
`# ${title || label}`,
|
|
@@ -302,26 +387,28 @@ print(json.dumps(texts))
|
|
|
302
387
|
continue;
|
|
303
388
|
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
304
389
|
const wingDir = join(OUT_DIR, wing);
|
|
305
|
-
mkdirSync(wingDir, { recursive: true });
|
|
390
|
+
mkdirSync(wingDir, { recursive: true, mode: 0o700 });
|
|
306
391
|
const fname = `sync_${prefix}_${contentHash}.txt`;
|
|
307
|
-
writeFileSync(join(wingDir, fname), content + "\n");
|
|
392
|
+
writeFileSync(join(wingDir, fname), content + "\n", { mode: 0o600 });
|
|
308
393
|
if (!wings.has(wing))
|
|
309
394
|
wings.set(wing, []);
|
|
310
395
|
wings.get(wing).push(join(wingDir, fname));
|
|
311
396
|
}
|
|
312
|
-
return { wings, now };
|
|
397
|
+
return { wings, now: cursor };
|
|
313
398
|
}
|
|
314
399
|
function markSynced(now) {
|
|
315
400
|
writeFileSync(STATE_FILE, JSON.stringify({ last_sync_ms: now }));
|
|
316
401
|
lastSyncTs = Date.now();
|
|
317
402
|
}
|
|
318
|
-
//
|
|
319
|
-
//
|
|
403
|
+
// Default `exchange` extraction: one drawer per exchange pair, verbatim,
|
|
404
|
+
// no paraphrasing (see PR #1524 review). Intelligent filing (decisions,
|
|
405
|
+
// KG facts, diary) happens through AI checkpoints, not the miner.
|
|
406
|
+
// Agent tag keeps opencode-mined drawers attributable.
|
|
320
407
|
// One wing per project (official multi-project pattern).
|
|
321
408
|
// Argv array, no shell (see PR #2): wing names are sanitized, but the
|
|
322
409
|
// spawn path stays shell-free regardless.
|
|
323
410
|
function mineArgs(wingDir, wing) {
|
|
324
|
-
return ["mine", wingDir, "--mode", "convos", "--
|
|
411
|
+
return ["mine", wingDir, "--mode", "convos", "--agent", "opencode", "--wing", wing];
|
|
325
412
|
}
|
|
326
413
|
function cleanupExport(wings) {
|
|
327
414
|
for (const files of wings.values()) {
|
|
@@ -370,6 +457,8 @@ function doDbSync() {
|
|
|
370
457
|
markSynced(now);
|
|
371
458
|
cleanupExport(wings);
|
|
372
459
|
log("mine done");
|
|
460
|
+
const names = [...wings.keys()].join(", ");
|
|
461
|
+
toast("success", "MemPalace", `mined ${wingCount(wings)} session(s) → ${names}`);
|
|
373
462
|
return;
|
|
374
463
|
}
|
|
375
464
|
const [wing, files] = entries[i];
|
|
@@ -377,12 +466,19 @@ function doDbSync() {
|
|
|
377
466
|
// wrapper shell and orphan the python mine process, which keeps
|
|
378
467
|
// holding the palace lock while the next mine piles up. miningLock
|
|
379
468
|
// already serializes concurrent mines; long mines run to completion.
|
|
380
|
-
|
|
469
|
+
const bin = resolveBin();
|
|
470
|
+
if (!bin) {
|
|
471
|
+
miningLock = false;
|
|
472
|
+
errLog("mine skipped: mempalace CLI not found");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
execFile(bin, mineArgs(join(OUT_DIR, wing), wing), {
|
|
381
476
|
encoding: "utf-8",
|
|
382
477
|
}, (err) => {
|
|
383
478
|
if (err) {
|
|
384
479
|
miningLock = false;
|
|
385
480
|
errLog(`mine err (${wing}): ${err.message}`);
|
|
481
|
+
toast("error", "MemPalace", `mine failed (${wing}): ${err.message.slice(0, 120)}`);
|
|
386
482
|
return;
|
|
387
483
|
}
|
|
388
484
|
log(`mined wing ${wing} (${files.length} sessions)`);
|
|
@@ -392,17 +488,31 @@ function doDbSync() {
|
|
|
392
488
|
mineNext(0);
|
|
393
489
|
}
|
|
394
490
|
// Best-effort synchronous save for process exit (SIGINT/SIGTERM/exit):
|
|
395
|
-
// only synchronous calls are allowed here.
|
|
491
|
+
// only synchronous calls are allowed here. Bounded by EXIT_BUDGET_MS so
|
|
492
|
+
// shutdown stays fast; miningLock is deliberately ignored here because
|
|
493
|
+
// any in-flight async mine dies with the process — at exit this sync
|
|
494
|
+
// mine takes ownership (see PR #1524 review).
|
|
495
|
+
const EXIT_BUDGET_MS = 45000;
|
|
496
|
+
const EXIT_WING_TIMEOUT_MS = 30000;
|
|
396
497
|
function exitSync() {
|
|
397
498
|
try {
|
|
499
|
+
const bin = resolveBin();
|
|
500
|
+
if (!bin)
|
|
501
|
+
return;
|
|
398
502
|
const { wings, now } = exportNewSessions(getLastSync());
|
|
399
503
|
if (wings.size === 0)
|
|
400
504
|
return;
|
|
505
|
+
const deadline = Date.now() + EXIT_BUDGET_MS;
|
|
401
506
|
for (const [wing] of wings) {
|
|
507
|
+
const remaining = deadline - Date.now();
|
|
508
|
+
if (remaining <= 0) {
|
|
509
|
+
log("exit save: budget exhausted, rest covered next startup");
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
402
512
|
log(`exit save: mining wing ${wing}`);
|
|
403
|
-
const res = spawnSync(
|
|
513
|
+
const res = spawnSync(bin, mineArgs(join(OUT_DIR, wing), wing), {
|
|
404
514
|
encoding: "utf-8",
|
|
405
|
-
timeout:
|
|
515
|
+
timeout: Math.min(EXIT_WING_TIMEOUT_MS, remaining),
|
|
406
516
|
});
|
|
407
517
|
if (res.error || res.status !== 0) {
|
|
408
518
|
errLog(`exit mine err (${wing}): ${String(res.error || res.status)}`);
|
|
@@ -417,13 +527,17 @@ function exitSync() {
|
|
|
417
527
|
errLog("exit save err: " + String(e));
|
|
418
528
|
}
|
|
419
529
|
}
|
|
420
|
-
export default (async () => {
|
|
421
|
-
|
|
530
|
+
export default (async ({ client }) => {
|
|
531
|
+
tuiClient = client || null;
|
|
532
|
+
mkdirSync(OUT_DIR, { recursive: true, mode: 0o700 });
|
|
422
533
|
mkdirSync(HOOK_STATE_DIR, { recursive: true });
|
|
423
534
|
const autoInject = isAutoInjectEnabled();
|
|
424
535
|
const identity = readIdentity();
|
|
425
536
|
const interval = saveInterval();
|
|
426
537
|
log(`loaded (autoInjectContext: ${autoInject}, saveInterval: ${interval})`);
|
|
538
|
+
// Catch anything missed by a previous run (e.g. content skipped when
|
|
539
|
+
// the exit budget ran out). Fires once per server lifetime.
|
|
540
|
+
setTimeout(() => dbSync(), 10000);
|
|
427
541
|
// Crash safety: best-effort synchronous save on hard exit.
|
|
428
542
|
// Mirrors the official emergency-save intent (nothing async allowed here).
|
|
429
543
|
let exitHandled = false;
|
|
@@ -448,6 +562,10 @@ export default (async () => {
|
|
|
448
562
|
// Official Save-hook cadence: count human messages per session,
|
|
449
563
|
// persist like ~/.mempalace/hook_state/, arm ONE AI checkpoint
|
|
450
564
|
// per boundary. The model decides WHAT to file.
|
|
565
|
+
// NOTE: no mine here by design (see PR #1524 review) — mining a
|
|
566
|
+
// mid-reply snapshot would export partial assistant parts. Mines
|
|
567
|
+
// run on idle/exit/startup, when turns are complete; the export
|
|
568
|
+
// additionally skips unfinished replies (finish tracking).
|
|
451
569
|
const counters = loadCounters();
|
|
452
570
|
const c = counters[sessionID] || { humanMsgs: 0, lastCheckpoint: 0 };
|
|
453
571
|
c.humanMsgs += 1;
|
|
@@ -456,8 +574,7 @@ export default (async () => {
|
|
|
456
574
|
c.lastCheckpoint = boundary;
|
|
457
575
|
pendingCheckpoint = { sessionID, count: c.humanMsgs };
|
|
458
576
|
hookLog(`session ${sessionID}: ${c.humanMsgs} human msgs — checkpoint armed`);
|
|
459
|
-
|
|
460
|
-
setTimeout(() => dbSync(), 500);
|
|
577
|
+
toast("info", "MemPalace", `checkpoint armed (~${c.humanMsgs} msgs): the model will file memories now`);
|
|
461
578
|
}
|
|
462
579
|
counters[sessionID] = c;
|
|
463
580
|
persistCounters(counters);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-mempalace-persistence",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|