opencode-mempalace-persistence 2.5.2 → 2.5.3

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
@@ -177,7 +177,8 @@ The model responds
177
177
  → Model records new KG facts via MCP tools (only when something new emerged)
178
178
 
179
179
  Session goes idle / process exits
180
- → Background mine of everything new since last sync
180
+ → Background mine of everything new since last sync (per-wing cursors:
181
+ each wing advances independently, so one slow wing never stalls the rest)
181
182
  → TUI toast confirms what was mined (disable with `"toasts": false`)
182
183
 
183
184
  Every MemPalace call — plugin searches, model MCP calls (search, diary,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  declare const _default: ({ client }: any) => Promise<{
2
+ tool: {
3
+ mempalace_sync: {
4
+ description: string;
5
+ args: {};
6
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
7
+ };
8
+ };
2
9
  "chat.message": (input: {
3
10
  sessionID: string;
4
11
  agent?: string;
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { homedir } from "os";
4
4
  import { join, dirname } from "path";
5
5
  import { createHash } from "crypto";
6
6
  import { fileURLToPath } from "url";
7
+ import { tool } from "@opencode-ai/plugin";
7
8
  const HOME = homedir();
8
9
  const MEMPALACE_BIN = join(HOME, ".local/bin/mempalace");
9
10
  const OPENCODE_DB = join(HOME, ".local/share/opencode/opencode.db");
@@ -161,7 +162,52 @@ function countPendingSuffix() {
161
162
  const pending = countPendingFiles();
162
163
  return pending > 0 ? `, ${pending} file(s) waiting to mine` : ", queue empty";
163
164
  }
164
- // TUI toast client (set by the factory). Fire-and-forget: headless runs
165
+ // On-demand sync snapshot for the /memory-toast command: same facts as
166
+ // the startup toast, plus live-mine detection. Fires a toast AND returns
167
+ // the text (visible in transcript too).
168
+ function liveMiners() {
169
+ const found = [];
170
+ let dir = [];
171
+ try {
172
+ dir = readdirSync(join(HOME, ".mempalace/locks"));
173
+ }
174
+ catch {
175
+ return found;
176
+ }
177
+ for (const f of dir) {
178
+ if (!f.endsWith(".lock"))
179
+ continue;
180
+ try {
181
+ const content = readFileSync(join(HOME, ".mempalace/locks", f), "utf-8");
182
+ const pid = parseInt((content.match(/(\d+)/) || [])[1] || "", 10);
183
+ if (!pid)
184
+ continue;
185
+ try {
186
+ process.kill(pid, 0);
187
+ found.push(`PID ${pid} (${f.replace("mine_palace_", "").replace(".lock", "").slice(0, 8)}…)`);
188
+ }
189
+ catch { }
190
+ }
191
+ catch { }
192
+ }
193
+ return found;
194
+ }
195
+ function syncSnapshot() {
196
+ const st = readSyncState();
197
+ const pending = countPendingFiles();
198
+ const miners = liveMiners();
199
+ const agoMin = st.last_sync_ms > 0 ? Math.round((Date.now() - st.last_sync_ms) / 60000) : -1;
200
+ const syncAge = agoMin < 0 ? "never" : agoMin === 0 ? "<1 min ago" : `${agoMin} min ago`;
201
+ const mining = miners.length > 0 ? `mining NOW (${miners.join(", ")})` : "no mine running";
202
+ const text = `MemPalace sync — plugin v${pluginVersion()}\n` +
203
+ `Last sync: ${syncAge}\n` +
204
+ `Backlog: ${pending} file(s) waiting\n` +
205
+ `Status: ${mining}`;
206
+ const toastMsg = miners.length > 0
207
+ ? `mining now (${miners.length}), ${pending} file(s) waiting, last sync ${syncAge}`
208
+ : `idle, ${pending} file(s) waiting, last sync ${syncAge}`;
209
+ return { text, toastMsg };
210
+ }
165
211
  // (`opencode run`, no TUI attached) must never break on this.
166
212
  let tuiClient = null;
167
213
  // Throttle for routine skip notices (busy palace): at most one toast
@@ -487,11 +533,24 @@ for (mid, mts, mdata_raw) in rows:
487
533
  # is created when a reply STARTS, parts stream in afterwards, and
488
534
  # finish is set only on completion. Exporting mid-reply would
489
535
  # snapshot partial parts while the cursor advances past the message
490
- # timestamp — losing the rest of the reply forever. So assistant
491
- # messages without finish are skipped and revisited next sync.
536
+ # timestamp — losing the rest of the reply forever. So unfinished
537
+ # replies are skipped and revisited next sync — BUT only while
538
+ # recently active. A reply with no new parts for a while is dead
539
+ # (killed session, crashed run): treating it as perpetually
540
+ # in-flight would pin the cursor forever (seen live: a stillborn
541
+ # message froze sync for 7h). Dead replies are exported as-is.
542
+ now_ms = int(__import__("time").time() * 1000)
543
+ STALE_PART_MS = 30 * 60 * 1000
544
+ STALE_EMPTY_MS = 10 * 60 * 1000
492
545
  if role == "assistant" and not mdata.get("finish"):
493
- incomplete.append(mts)
494
- continue
546
+ max_part = db.execute("SELECT MAX(time_created) FROM part WHERE message_id = ?", (mid,)).fetchone()[0]
547
+ if max_part is None:
548
+ alive = (now_ms - mts) < STALE_EMPTY_MS
549
+ else:
550
+ alive = (now_ms - max_part) < STALE_PART_MS
551
+ if alive:
552
+ incomplete.append(mts)
553
+ continue
495
554
  for (pdata_raw,) in db.execute("SELECT data FROM part WHERE message_id = ? ORDER BY time_created", (mid,)).fetchall():
496
555
  try:
497
556
  pdata = json.loads(pdata_raw)
@@ -802,6 +861,18 @@ export default (async ({ client }) => {
802
861
  process.once("SIGHUP", onExit);
803
862
  process.once("exit", onExit);
804
863
  return {
864
+ tool: {
865
+ mempalace_sync: tool({
866
+ description: "Show live MemPalace sync state (backlog, last sync, running mines) as a TUI toast and text. Use when the user asks how mining is going.",
867
+ args: {},
868
+ async execute() {
869
+ const snap = syncSnapshot();
870
+ toast("info", "MemPalace", snap.toastMsg);
871
+ ilog("status", { via: "tool" });
872
+ return snap.text;
873
+ },
874
+ }),
875
+ },
805
876
  "chat.message": async (input, output) => {
806
877
  const role = output.message.role;
807
878
  if (role !== "user")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mempalace-persistence",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
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",