opencode-mempalace-persistence 2.3.0 → 2.5.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 +56 -22
- package/dist/index.d.ts +10 -0
- package/dist/index.js +188 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,32 +65,29 @@ mempalace mcp
|
|
|
65
65
|
|
|
66
66
|
The `mempalace mcp` command gives you the exact MCP setup string for your configuration.
|
|
67
67
|
|
|
68
|
-
### 4.
|
|
68
|
+
### 4. Plugin config (all optional)
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```json
|
|
75
|
-
{
|
|
76
|
-
"autoInjectContext": true
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Optional tuning:
|
|
70
|
+
No config file is needed to start: every setting has a default, and **not
|
|
71
|
+
creating anything means all defaults**. When you want to change one,
|
|
72
|
+
create `~/.mempalace/plugin-config.json`:
|
|
81
73
|
|
|
82
74
|
```json
|
|
83
75
|
{
|
|
84
|
-
"autoInjectContext":
|
|
85
|
-
"saveInterval": 15
|
|
76
|
+
"autoInjectContext": false,
|
|
77
|
+
"saveInterval": 15,
|
|
78
|
+
"toasts": true
|
|
86
79
|
}
|
|
87
80
|
```
|
|
88
81
|
|
|
89
|
-
|
|
82
|
+
| Key | Default | What it does |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `autoInjectContext` | `false` | Inject identity + `mempalace search` results into every prompt. Needs no model discipline, but adds context (and noise) to every turn. Off by default: recall happens via the skill instead |
|
|
85
|
+
| `saveInterval` | `15` (min `5`) | Human messages between AI checkpoints — same cadence as the official MemPalace save hook |
|
|
86
|
+
| `toasts` | `true` | TUI toasts for mines, checkpoints and MemPalace calls. Set `false` to silence them |
|
|
90
87
|
|
|
91
88
|
**Do NOT put this in `opencode.json`** — OpenCode's schema validation rejects unknown keys. The plugin reads its config from `~/.mempalace/plugin-config.json` instead.
|
|
92
89
|
|
|
93
|
-
When enabled:
|
|
90
|
+
When `autoInjectContext` is enabled:
|
|
94
91
|
- **First message**: Injects your identity from `~/.mempalace/identity.txt`
|
|
95
92
|
- **Every message**: Runs `mempalace search` and injects relevant results
|
|
96
93
|
|
|
@@ -101,12 +98,14 @@ Create `~/.config/opencode/AGENTS.md`:
|
|
|
101
98
|
```markdown
|
|
102
99
|
# Memory & Knowledge instructions
|
|
103
100
|
|
|
104
|
-
## Recall (
|
|
101
|
+
## Recall (via skill, unless auto-inject is on)
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
Recall follows the bundled `mempalace-recall` skill (question-driven
|
|
104
|
+
search). If you enabled `autoInjectContext`, identity + relevant memories
|
|
105
|
+
are additionally injected into every prompt — then only search MemPalace
|
|
106
|
+
yourself when the question is about past work, decisions, people, or
|
|
107
|
+
projects AND the injected context has nothing. Either way, quote results
|
|
108
|
+
verbatim, never paraphrase.
|
|
110
109
|
|
|
111
110
|
## Record facts (after responding, only when something new emerged)
|
|
112
111
|
|
|
@@ -181,6 +180,19 @@ Session goes idle / process exits
|
|
|
181
180
|
→ Background mine of everything new since last sync
|
|
182
181
|
→ TUI toast confirms what was mined (disable with `"toasts": false`)
|
|
183
182
|
|
|
183
|
+
Every MemPalace call — plugin searches, model MCP calls (search, diary,
|
|
184
|
+
KG) — also raises a short TUI toast with what was asked and a result
|
|
185
|
+
preview, so background memory activity is always visible. A startup toast
|
|
186
|
+
shows the loaded plugin version (`plugin v2.x loaded`), so you always know
|
|
187
|
+
whether you're running the npm release or a local build.
|
|
188
|
+
|
|
189
|
+
Multiple opencode instances are supported: mines coordinate through the
|
|
190
|
+
palace lock with backoff-and-retry (up to ~10min per wing), so concurrent
|
|
191
|
+
instances interleave wing by wing instead of starving each other — backfills
|
|
192
|
+
complete even with two sessions open. Routine contention shows one info
|
|
193
|
+
toast every 5 minutes max. To reduce contention during huge backfills, a
|
|
194
|
+
single instance is still fastest.
|
|
195
|
+
|
|
184
196
|
Compaction starts
|
|
185
197
|
→ [MemPalace Pre-Compact Emergency Save]: model files everything first
|
|
186
198
|
→ Identity + wake-up context re-attached so the summary cannot lose them
|
|
@@ -256,7 +268,7 @@ The plugin exports everything in the opencode database on the next sync, then re
|
|
|
256
268
|
|---|---|
|
|
257
269
|
| `~/.config/opencode/opencode.json` | OpenCode config with plugin + MCP |
|
|
258
270
|
| `~/.config/opencode/AGENTS.md` | Tells the model to manage KG facts |
|
|
259
|
-
| `~/.mempalace/plugin-config.json` | Plugin config (`autoInjectContext`, `saveInterval`) |
|
|
271
|
+
| `~/.mempalace/plugin-config.json` | Plugin config (`autoInjectContext`, `saveInterval`, `toasts` — all optional, see §4) |
|
|
260
272
|
| `~/.config/opencode/skills/mempalace-recall/SKILL.md` | Bundled recall skill (copy from `skills/` in this repo) |
|
|
261
273
|
| `~/.mempalace/identity.txt` | Your identity (injected by plugin) |
|
|
262
274
|
| `~/.mempalace/hook_state/opencode_counters.json` | Per-session message counters (checkpoint cadence) |
|
|
@@ -295,6 +307,28 @@ When set, the plugin writes a debug log to `/tmp/opencode-mempalace.log`.
|
|
|
295
307
|
|
|
296
308
|
---
|
|
297
309
|
|
|
310
|
+
## Observability: toasts and commands
|
|
311
|
+
|
|
312
|
+
Background memory activity is visible three ways — ephemeral first,
|
|
313
|
+
history on demand, never polluting session context:
|
|
314
|
+
|
|
315
|
+
- **TUI toasts** (on by default, `"toasts": false` to disable): mine
|
|
316
|
+
results and errors, armed checkpoints, and every MemPalace call
|
|
317
|
+
(plugin searches and model MCP calls) with what was asked plus a
|
|
318
|
+
short answer preview. A startup toast shows the loaded build
|
|
319
|
+
(`opencode-mempalace-persistence v2.x loaded`), so npm-cache vs
|
|
320
|
+
local build is never a mystery.
|
|
321
|
+
- **`/memory-status`** — palace health in the transcript: drawers,
|
|
322
|
+
KG stats, last sync, pending backlog, recent activity, errors with
|
|
323
|
+
explanations, active config. Read-only.
|
|
324
|
+
- **`/memory-log [N] [filter]`** — the interaction history: every
|
|
325
|
+
search (query → result count), tool call (asked → answered preview),
|
|
326
|
+
mine (outcome per wing) and checkpoint, newest last. Backed by
|
|
327
|
+
`~/.mempalace/hook_state/interactions.log` (JSON lines, auto-rotated).
|
|
328
|
+
Read-only.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
298
332
|
## License
|
|
299
333
|
|
|
300
334
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ declare const _default: ({ client }: any) => Promise<{
|
|
|
24
24
|
context: string[];
|
|
25
25
|
prompt?: string;
|
|
26
26
|
}) => Promise<void>;
|
|
27
|
+
"tool.execute.after": (input: {
|
|
28
|
+
tool: string;
|
|
29
|
+
sessionID: string;
|
|
30
|
+
callID: string;
|
|
31
|
+
args: any;
|
|
32
|
+
}, output: {
|
|
33
|
+
title: string;
|
|
34
|
+
output: string;
|
|
35
|
+
metadata: any;
|
|
36
|
+
}) => Promise<void>;
|
|
27
37
|
event: ({ event }: any) => Promise<void>;
|
|
28
38
|
}>;
|
|
29
39
|
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { execSync, execFileSync, execFile, spawnSync } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmdirSync, unlinkSync, appendFileSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmdirSync, unlinkSync, appendFileSync, statSync, readdirSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
6
7
|
const HOME = homedir();
|
|
7
8
|
const MEMPALACE_BIN = join(HOME, ".local/bin/mempalace");
|
|
8
9
|
const OPENCODE_DB = join(HOME, ".local/share/opencode/opencode.db");
|
|
@@ -12,6 +13,9 @@ const IDENTITY_FILE = join(HOME, ".mempalace/identity.txt");
|
|
|
12
13
|
const HOOK_STATE_DIR = join(HOME, ".mempalace/hook_state");
|
|
13
14
|
const COUNTERS_FILE = join(HOOK_STATE_DIR, "opencode_counters.json");
|
|
14
15
|
const HOOK_LOG = join(HOOK_STATE_DIR, "hook.log");
|
|
16
|
+
const INTERACTIONS_LOG = join(HOOK_STATE_DIR, "interactions.log");
|
|
17
|
+
// Cap the interactions log so it never grows unbounded (approx lines).
|
|
18
|
+
const INTERACTIONS_MAX_LINES = 2000;
|
|
15
19
|
// Private sync workspace (0700): transcripts contain conversation text,
|
|
16
20
|
// so they must never sit world-readable in /tmp (see PR #1524 review).
|
|
17
21
|
const SYNC_DIR = join(HOME, ".mempalace/oc-sessions");
|
|
@@ -46,6 +50,28 @@ function errLog(msg) {
|
|
|
46
50
|
log("ERROR: " + msg);
|
|
47
51
|
hookLog("ERROR: " + msg);
|
|
48
52
|
}
|
|
53
|
+
// Structured interaction log (JSON lines): every MemPalace question and
|
|
54
|
+
// answer, readable by /memory-log. Ephemeral TUI toasts show the moment;
|
|
55
|
+
// this file keeps the history — without polluting session context.
|
|
56
|
+
function ilog(kind, data) {
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(HOOK_STATE_DIR, { recursive: true });
|
|
59
|
+
appendFileSync(INTERACTIONS_LOG, JSON.stringify({ ts: new Date().toISOString(), kind, ...data }) + "\n");
|
|
60
|
+
// Cheap rotation: count lines only when the file looks big.
|
|
61
|
+
let size = 0;
|
|
62
|
+
try {
|
|
63
|
+
size = statSync(INTERACTIONS_LOG).size;
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
if (size > 600 * 1024) {
|
|
67
|
+
const lines = readFileSync(INTERACTIONS_LOG, "utf-8").split("\n");
|
|
68
|
+
if (lines.length > INTERACTIONS_MAX_LINES) {
|
|
69
|
+
writeFileSync(INTERACTIONS_LOG, lines.slice(-INTERACTIONS_MAX_LINES).join("\n"));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
}
|
|
49
75
|
function toastsEnabled() {
|
|
50
76
|
try {
|
|
51
77
|
const raw = readFileSync(PLUGIN_CONFIG, "utf-8");
|
|
@@ -56,9 +82,63 @@ function toastsEnabled() {
|
|
|
56
82
|
catch { }
|
|
57
83
|
return true;
|
|
58
84
|
}
|
|
85
|
+
// Plugin's own name + version (shown in the startup toast and
|
|
86
|
+
// /memory-status, so you always know which build is loaded).
|
|
87
|
+
let cachedName = undefined;
|
|
88
|
+
let cachedVersion = undefined;
|
|
89
|
+
function pluginName() {
|
|
90
|
+
if (cachedName !== undefined)
|
|
91
|
+
return cachedName;
|
|
92
|
+
try {
|
|
93
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
94
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8"));
|
|
95
|
+
cachedName = typeof pkg?.name === "string" ? pkg.name : "opencode-mempalace-persistence";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
cachedName = "opencode-mempalace-persistence";
|
|
99
|
+
}
|
|
100
|
+
return cachedName ?? "opencode-mempalace-persistence";
|
|
101
|
+
}
|
|
102
|
+
function pluginVersion() {
|
|
103
|
+
if (cachedVersion !== undefined)
|
|
104
|
+
return cachedVersion;
|
|
105
|
+
try {
|
|
106
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
107
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8"));
|
|
108
|
+
cachedVersion = typeof pkg?.version === "string" ? pkg.version : "unknown";
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
cachedVersion = "unknown";
|
|
112
|
+
}
|
|
113
|
+
return cachedVersion ?? "unknown";
|
|
114
|
+
}
|
|
115
|
+
// Pending export files (backlog). Pure fs walk, no shell.
|
|
116
|
+
function countPendingFiles() {
|
|
117
|
+
let n = 0;
|
|
118
|
+
try {
|
|
119
|
+
for (const w of readdirSync(SYNC_DIR, { withFileTypes: true })) {
|
|
120
|
+
if (!w.isDirectory())
|
|
121
|
+
continue;
|
|
122
|
+
try {
|
|
123
|
+
n += readdirSync(join(SYNC_DIR, w.name)).length;
|
|
124
|
+
}
|
|
125
|
+
catch { }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
return n;
|
|
130
|
+
}
|
|
131
|
+
function countPendingSuffix() {
|
|
132
|
+
const pending = countPendingFiles();
|
|
133
|
+
return pending > 0 ? `, ${pending} file(s) waiting to mine` : ", queue empty";
|
|
134
|
+
}
|
|
59
135
|
// TUI toast client (set by the factory). Fire-and-forget: headless runs
|
|
60
136
|
// (`opencode run`, no TUI attached) must never break on this.
|
|
61
137
|
let tuiClient = null;
|
|
138
|
+
// Throttle for routine skip notices (busy palace): at most one toast
|
|
139
|
+
// per window, otherwise active sessions get spammed every turn.
|
|
140
|
+
let lastBusyToastTs = 0;
|
|
141
|
+
const BUSY_TOAST_WINDOW_MS = 5 * 60 * 1000;
|
|
62
142
|
function toast(variant, title, message) {
|
|
63
143
|
if (!toastsEnabled() || !tuiClient?.tui?.showToast)
|
|
64
144
|
return;
|
|
@@ -244,6 +324,7 @@ function mempalaceSearch(query) {
|
|
|
244
324
|
const bin = resolveBin();
|
|
245
325
|
if (!bin)
|
|
246
326
|
return "";
|
|
327
|
+
const started = Date.now();
|
|
247
328
|
try {
|
|
248
329
|
// argv array, no shell (see PR #2): the query is raw user message
|
|
249
330
|
// text, so it must never pass through /bin/sh. No manual escaping needed.
|
|
@@ -251,14 +332,38 @@ function mempalaceSearch(query) {
|
|
|
251
332
|
encoding: "utf-8",
|
|
252
333
|
timeout: 15000,
|
|
253
334
|
}).trim();
|
|
254
|
-
if (!out || out.includes("No results"))
|
|
335
|
+
if (!out || out.includes("No results")) {
|
|
336
|
+
toast("info", "MemPalace", `search "${query.slice(0, 50)}" → no results`);
|
|
337
|
+
ilog("search", { via: "cli", query: query.slice(0, 200), results: 0, ms: Date.now() - started });
|
|
255
338
|
return "";
|
|
339
|
+
}
|
|
340
|
+
const n = (out.match(/\n\s*\[\d+\]/g) || []).length || 1;
|
|
341
|
+
toast("info", "MemPalace", `search "${query.slice(0, 50)}" → ${n} result(s)`);
|
|
342
|
+
ilog("search", { via: "cli", query: query.slice(0, 200), results: n, ms: Date.now() - started });
|
|
256
343
|
return out.slice(0, MAX_INJECT_CHARS);
|
|
257
344
|
}
|
|
258
345
|
catch {
|
|
259
346
|
return "";
|
|
260
347
|
}
|
|
261
348
|
}
|
|
349
|
+
// TUI visibility for model-driven MCP calls (skill recall, diary, KG):
|
|
350
|
+
// the plugin can't see inside the agent, but it sees every tool result.
|
|
351
|
+
function isMemPalaceTool(name) {
|
|
352
|
+
return typeof name === "string" && name.toLowerCase().includes("mempalace");
|
|
353
|
+
}
|
|
354
|
+
function summarizeToolCall(tool, args, out) {
|
|
355
|
+
const short = tool.replace(/^mcp_+/, "").replace(/^mempalace_mempalace_/, "").replace(/^mempalace_/, "");
|
|
356
|
+
let asked = "";
|
|
357
|
+
try {
|
|
358
|
+
const a = typeof args === "string" ? args : JSON.stringify(args || {});
|
|
359
|
+
asked = a.replace(/\s+/g, " ").slice(0, 60);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
asked = "";
|
|
363
|
+
}
|
|
364
|
+
const answered = (out || "").replace(/\s+/g, " ").slice(0, 80) || "(empty)";
|
|
365
|
+
return `${short} · asked: ${asked} → ${answered}`.slice(0, 220);
|
|
366
|
+
}
|
|
262
367
|
function getLastSync() {
|
|
263
368
|
if (!existsSync(STATE_FILE))
|
|
264
369
|
return 0;
|
|
@@ -448,7 +553,19 @@ function doDbSync() {
|
|
|
448
553
|
miningLock = true;
|
|
449
554
|
log(`mining ${wingCount(wings)} sessions across ${wings.size} wings`);
|
|
450
555
|
const entries = [...wings.entries()];
|
|
451
|
-
|
|
556
|
+
// Per-wing drawers tally for the final toast (parsed from mine stdout).
|
|
557
|
+
const wingDrawers = new Map();
|
|
558
|
+
const parseDrawers = (stdout) => {
|
|
559
|
+
const m = String(stdout || "").match(/Drawers filed:\s*(\d+)/i);
|
|
560
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
561
|
+
};
|
|
562
|
+
// Retry schedule for lock contention: two instances (or an MCP write)
|
|
563
|
+
// interleave wing by wing instead of starving each other. Total ~10min
|
|
564
|
+
// of retries per wing, then give up until the next trigger (idle/exit).
|
|
565
|
+
// miningLock stays held during backoff so one process never piles up.
|
|
566
|
+
const RETRY_DELAYS_MS = [15000, 30000, 60000, 120000, 180000, 300000];
|
|
567
|
+
const jitter = (ms) => ms + Math.floor(Math.random() * 10000);
|
|
568
|
+
const mineNext = (i, attempt = 0) => {
|
|
452
569
|
if (i >= entries.length) {
|
|
453
570
|
miningLock = false;
|
|
454
571
|
// Advance state only on full success: on failure the same
|
|
@@ -458,7 +575,10 @@ function doDbSync() {
|
|
|
458
575
|
cleanupExport(wings);
|
|
459
576
|
log("mine done");
|
|
460
577
|
const names = [...wings.keys()].join(", ");
|
|
461
|
-
|
|
578
|
+
const totalDrawers = [...wingDrawers.values()].reduce((a, b) => a + b, 0);
|
|
579
|
+
const detail = totalDrawers > 0 ? ` (${totalDrawers} drawers)` : "";
|
|
580
|
+
toast("success", "MemPalace", `mined ${wingCount(wings)} session(s) → ${names}${detail}`);
|
|
581
|
+
ilog("mine", { outcome: "ok", sessions: wingCount(wings), wings: [...wings.keys()], drawers: totalDrawers });
|
|
462
582
|
return;
|
|
463
583
|
}
|
|
464
584
|
const [wing, files] = entries[i];
|
|
@@ -474,14 +594,42 @@ function doDbSync() {
|
|
|
474
594
|
}
|
|
475
595
|
execFile(bin, mineArgs(join(OUT_DIR, wing), wing), {
|
|
476
596
|
encoding: "utf-8",
|
|
477
|
-
}, (err) => {
|
|
597
|
+
}, (err, stdout) => {
|
|
478
598
|
if (err) {
|
|
599
|
+
const msg = err.message || String(err);
|
|
600
|
+
// Lock contention (second opencode instance mining, or an MCP
|
|
601
|
+
// write in flight) is routine, not a failure: back off and retry
|
|
602
|
+
// the same wing — the holder releases between its own wings, so
|
|
603
|
+
// concurrent instances interleave instead of starving. Anything
|
|
604
|
+
// else is a real error.
|
|
605
|
+
if (/is held by/i.test(msg) && attempt < RETRY_DELAYS_MS.length) {
|
|
606
|
+
const wait = jitter(RETRY_DELAYS_MS[attempt]);
|
|
607
|
+
log(`palace busy (${wing}), retry ${attempt + 1}/${RETRY_DELAYS_MS.length} in ${Math.round(wait / 1000)}s`);
|
|
608
|
+
const nowTs = Date.now();
|
|
609
|
+
if (nowTs - lastBusyToastTs > BUSY_TOAST_WINDOW_MS) {
|
|
610
|
+
lastBusyToastTs = nowTs;
|
|
611
|
+
toast("info", "MemPalace", "palace busy (another instance mining?) — backing off, will retry");
|
|
612
|
+
}
|
|
613
|
+
setTimeout(() => mineNext(i, attempt + 1), wait);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
479
616
|
miningLock = false;
|
|
480
|
-
|
|
481
|
-
|
|
617
|
+
if (/is held by/i.test(msg)) {
|
|
618
|
+
log(`mine skipped, palace busy (${wing}) after ${attempt} retries — next trigger will retry`);
|
|
619
|
+
ilog("mine", { outcome: "busy", wing });
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
errLog(`mine err (${wing}): ${msg}`);
|
|
623
|
+
toast("error", "MemPalace", `mine failed (${wing}): ${msg.slice(0, 120)}`);
|
|
624
|
+
ilog("mine", { outcome: "error", wing, error: msg.slice(0, 200) });
|
|
482
625
|
return;
|
|
483
626
|
}
|
|
484
627
|
log(`mined wing ${wing} (${files.length} sessions)`);
|
|
628
|
+
wingDrawers.set(wing, parseDrawers(stdout));
|
|
629
|
+
// Truthful progress: one toast per completed wing (an exact % is
|
|
630
|
+
// impossible — the mine CLI is a black box with ~4s startup cost
|
|
631
|
+
// per invocation, so per-file mines would only add overhead).
|
|
632
|
+
toast("info", "MemPalace", `wing ${wing} done (${i + 1}/${entries.length})`);
|
|
485
633
|
mineNext(i + 1);
|
|
486
634
|
});
|
|
487
635
|
};
|
|
@@ -538,8 +686,17 @@ export default (async ({ client }) => {
|
|
|
538
686
|
// Catch anything missed by a previous run (e.g. content skipped when
|
|
539
687
|
// the exit budget ran out). Fires once per server lifetime.
|
|
540
688
|
setTimeout(() => dbSync(), 10000);
|
|
689
|
+
// Startup toast (delayed so the TUI is attached): shows exactly which
|
|
690
|
+
// plugin build is loaded — no more guessing npm-cache vs local build —
|
|
691
|
+
// plus pending backlog so a restarted-into-backlog state is visible.
|
|
692
|
+
setTimeout(() => {
|
|
693
|
+
toast("info", "MemPalace", `${pluginName()} v${pluginVersion()} loaded${countPendingSuffix()}`);
|
|
694
|
+
log(`startup toast fired (${pluginName()} v${pluginVersion()})`);
|
|
695
|
+
}, 15000);
|
|
541
696
|
// Crash safety: best-effort synchronous save on hard exit.
|
|
542
697
|
// Mirrors the official emergency-save intent (nothing async allowed here).
|
|
698
|
+
// SIGHUP included: closing the terminal / dropping SSH kills the process
|
|
699
|
+
// group, otherwise mines would die mid-run with no save attempt.
|
|
543
700
|
let exitHandled = false;
|
|
544
701
|
const onExit = () => {
|
|
545
702
|
if (exitHandled)
|
|
@@ -549,6 +706,7 @@ export default (async ({ client }) => {
|
|
|
549
706
|
};
|
|
550
707
|
process.once("SIGINT", onExit);
|
|
551
708
|
process.once("SIGTERM", onExit);
|
|
709
|
+
process.once("SIGHUP", onExit);
|
|
552
710
|
process.once("exit", onExit);
|
|
553
711
|
return {
|
|
554
712
|
"chat.message": async (input, output) => {
|
|
@@ -574,6 +732,7 @@ export default (async ({ client }) => {
|
|
|
574
732
|
c.lastCheckpoint = boundary;
|
|
575
733
|
pendingCheckpoint = { sessionID, count: c.humanMsgs };
|
|
576
734
|
hookLog(`session ${sessionID}: ${c.humanMsgs} human msgs — checkpoint armed`);
|
|
735
|
+
ilog("checkpoint", { sessionID, count: c.humanMsgs });
|
|
577
736
|
toast("info", "MemPalace", `checkpoint armed (~${c.humanMsgs} msgs): the model will file memories now`);
|
|
578
737
|
}
|
|
579
738
|
counters[sessionID] = c;
|
|
@@ -652,6 +811,27 @@ export default (async ({ client }) => {
|
|
|
652
811
|
output.context.push(`[MemPalace Rescue — core memory, must survive compaction]\n${rescue.join("\n\n")}`);
|
|
653
812
|
}
|
|
654
813
|
},
|
|
814
|
+
"tool.execute.after": async (input, output) => {
|
|
815
|
+
try {
|
|
816
|
+
const name = input?.tool || "";
|
|
817
|
+
if (!isMemPalaceTool(name))
|
|
818
|
+
return;
|
|
819
|
+
const summary = summarizeToolCall(name, input?.args, output?.output || "");
|
|
820
|
+
log(`tool: ${summary}`);
|
|
821
|
+
toast("info", "MemPalace", summary);
|
|
822
|
+
ilog("tool", {
|
|
823
|
+
tool: String(name).replace(/^mcp_+/, "").replace(/^mempalace_mempalace_/, "").replace(/^mempalace_/, ""),
|
|
824
|
+
asked: (() => { try {
|
|
825
|
+
return JSON.stringify(input?.args || {}).replace(/\s+/g, " ").slice(0, 200);
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
return "";
|
|
829
|
+
} })(),
|
|
830
|
+
answered: String(output?.output || "").replace(/\s+/g, " ").slice(0, 300),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
catch { }
|
|
834
|
+
},
|
|
655
835
|
event: async ({ event }) => {
|
|
656
836
|
if (event?.type === "session.idle" || event?.type === "session.deleted") {
|
|
657
837
|
log(`${event.type} - queue sync`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-mempalace-persistence",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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",
|