opencode-mempalace-persistence 2.5.0 → 2.5.2
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/dist/index.js +127 -34
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -82,8 +82,37 @@ function toastsEnabled() {
|
|
|
82
82
|
catch { }
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
|
-
//
|
|
86
|
-
//
|
|
85
|
+
// Messages arrived after each wing's sync cursor: still waiting for the
|
|
86
|
+
// next run. Per-wing, so one slow wing never masks the others.
|
|
87
|
+
function countPendingMessages() {
|
|
88
|
+
try {
|
|
89
|
+
const st = readSyncState();
|
|
90
|
+
const out = runPython(`
|
|
91
|
+
import sqlite3, json, re
|
|
92
|
+
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
93
|
+
cursors = json.loads(${JSON.stringify(JSON.stringify(st.wings || {}))})
|
|
94
|
+
default = ${st.last_sync_ms || 0}
|
|
95
|
+
rows = db.execute("""
|
|
96
|
+
SELECT s.directory, m.time_created FROM message m
|
|
97
|
+
INNER JOIN session s ON s.id = m.session_id
|
|
98
|
+
""").fetchall()
|
|
99
|
+
db.close()
|
|
100
|
+
by = {}
|
|
101
|
+
for (directory, mts) in rows:
|
|
102
|
+
base = ((directory or "").rstrip("/").split("/") or ["global"])[-1] or "global"
|
|
103
|
+
wing = re.sub("[^a-zA-Z0-9_-]", "_", base)[:40] or "global"
|
|
104
|
+
if mts > cursors.get(wing, default):
|
|
105
|
+
by[wing] = by.get(wing, 0) + 1
|
|
106
|
+
print(json.dumps(by))
|
|
107
|
+
`);
|
|
108
|
+
const byWing = JSON.parse(out);
|
|
109
|
+
const total = Object.values(byWing).reduce((a, b) => a + b, 0);
|
|
110
|
+
return { total, byWing };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { total: 0, byWing: {} };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
87
116
|
let cachedName = undefined;
|
|
88
117
|
let cachedVersion = undefined;
|
|
89
118
|
function pluginName() {
|
|
@@ -364,15 +393,26 @@ function summarizeToolCall(tool, args, out) {
|
|
|
364
393
|
const answered = (out || "").replace(/\s+/g, " ").slice(0, 80) || "(empty)";
|
|
365
394
|
return `${short} · asked: ${asked} → ${answered}`.slice(0, 220);
|
|
366
395
|
}
|
|
367
|
-
function
|
|
368
|
-
if (!existsSync(STATE_FILE))
|
|
369
|
-
return 0;
|
|
396
|
+
function readSyncState() {
|
|
370
397
|
try {
|
|
371
|
-
|
|
398
|
+
const raw = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
399
|
+
if (typeof raw?.last_sync_ms === "number") {
|
|
400
|
+
return { last_sync_ms: raw.last_sync_ms, wings: raw.wings && typeof raw.wings === "object" ? raw.wings : {} };
|
|
401
|
+
}
|
|
372
402
|
}
|
|
373
|
-
catch {
|
|
403
|
+
catch { }
|
|
404
|
+
return { last_sync_ms: 0, wings: {} };
|
|
405
|
+
}
|
|
406
|
+
// Per-wing cursors (see PR #1524 follow-up): a global cursor stalls
|
|
407
|
+
// forever when one wing keeps failing while others succeed. Each wing
|
|
408
|
+
// advances independently; last_sync_ms stays the min for compatibility.
|
|
409
|
+
function getLastSync(wing) {
|
|
410
|
+
if (!existsSync(STATE_FILE))
|
|
374
411
|
return 0;
|
|
375
|
-
|
|
412
|
+
const st = readSyncState();
|
|
413
|
+
if (wing && st.wings && typeof st.wings[wing] === "number")
|
|
414
|
+
return st.wings[wing];
|
|
415
|
+
return st.last_sync_ms || 0;
|
|
376
416
|
}
|
|
377
417
|
function dbSync() {
|
|
378
418
|
if (miningLock)
|
|
@@ -387,11 +427,12 @@ function dbSync() {
|
|
|
387
427
|
function backfillRequested() {
|
|
388
428
|
return !!process.env.OPENCODE_MEMPALACE_BACKFILL;
|
|
389
429
|
}
|
|
390
|
-
// Export
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// content hash, so re-exports are naturally idempotent.
|
|
394
|
-
function exportNewSessions(
|
|
430
|
+
// Export sessions with new messages as flat transcripts, grouped by
|
|
431
|
+
// project wing. cursorFor(wing) gives each wing its own cursor (null =
|
|
432
|
+
// discovery floor: sessions with anything newer anywhere). Filenames
|
|
433
|
+
// embed a content hash, so re-exports are naturally idempotent.
|
|
434
|
+
function exportNewSessions(cursorFor) {
|
|
435
|
+
const sinceMs = cursorFor(null);
|
|
395
436
|
const sessions = runPython(`
|
|
396
437
|
import sqlite3, json
|
|
397
438
|
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
@@ -427,12 +468,13 @@ print(json.dumps(rows))
|
|
|
427
468
|
.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40) || "global";
|
|
428
469
|
const label = (title || "").replace(/[^a-zA-Z0-9 _-]/g, "_") || (sessId || "").slice(0, 12);
|
|
429
470
|
const prefix = `${new Date().toISOString().slice(0, 10)}_${label.slice(0, 30)}_${(sessId || "").slice(0, 8)}`;
|
|
471
|
+
const wingSince = cursorFor(wing);
|
|
430
472
|
const msgs = runPython(`
|
|
431
473
|
import sqlite3, json
|
|
432
474
|
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
433
475
|
rows = db.execute("""
|
|
434
476
|
SELECT m.id, m.time_created, m.data FROM message m
|
|
435
|
-
WHERE m.session_id = ${JSON.stringify(sessId)} AND m.time_created > ${
|
|
477
|
+
WHERE m.session_id = ${JSON.stringify(sessId)} AND m.time_created > ${wingSince}
|
|
436
478
|
ORDER BY m.time_created
|
|
437
479
|
""").fetchall()
|
|
438
480
|
texts = []
|
|
@@ -501,8 +543,23 @@ print(json.dumps({"texts": texts, "incomplete": incomplete}))
|
|
|
501
543
|
}
|
|
502
544
|
return { wings, now: cursor };
|
|
503
545
|
}
|
|
504
|
-
function markSynced(now) {
|
|
505
|
-
|
|
546
|
+
function markSynced(now, wing) {
|
|
547
|
+
try {
|
|
548
|
+
const st = readSyncState();
|
|
549
|
+
if (wing) {
|
|
550
|
+
st.wings = st.wings || {};
|
|
551
|
+
st.wings[wing] = now;
|
|
552
|
+
const vals = Object.values(st.wings);
|
|
553
|
+
st.last_sync_ms = vals.length > 0 ? Math.min(...vals) : now;
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
st.last_sync_ms = now;
|
|
557
|
+
}
|
|
558
|
+
writeFileSync(STATE_FILE, JSON.stringify(st));
|
|
559
|
+
}
|
|
560
|
+
catch (e) {
|
|
561
|
+
log("state write err: " + String(e));
|
|
562
|
+
}
|
|
506
563
|
lastSyncTs = Date.now();
|
|
507
564
|
}
|
|
508
565
|
// Default `exchange` extraction: one drawer per exchange pair, verbatim,
|
|
@@ -516,15 +573,18 @@ function mineArgs(wingDir, wing) {
|
|
|
516
573
|
return ["mine", wingDir, "--mode", "convos", "--agent", "opencode", "--wing", wing];
|
|
517
574
|
}
|
|
518
575
|
function cleanupExport(wings) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
576
|
+
// Wipe whole wing dirs: a successful mine filed everything in them,
|
|
577
|
+
// including orphan files from previously failed runs.
|
|
578
|
+
for (const wing of wings.keys()) {
|
|
579
|
+
try {
|
|
580
|
+
for (const f of readdirSync(join(OUT_DIR, wing))) {
|
|
581
|
+
try {
|
|
582
|
+
unlinkSync(join(OUT_DIR, wing, f));
|
|
583
|
+
}
|
|
584
|
+
catch { }
|
|
523
585
|
}
|
|
524
|
-
catch { }
|
|
525
586
|
}
|
|
526
|
-
|
|
527
|
-
for (const wing of wings.keys()) {
|
|
587
|
+
catch { }
|
|
528
588
|
try {
|
|
529
589
|
rmdirSync(join(OUT_DIR, wing));
|
|
530
590
|
}
|
|
@@ -547,7 +607,10 @@ function doDbSync() {
|
|
|
547
607
|
const sinceMs = backfillRequested() ? 0 : getLastSync();
|
|
548
608
|
if (backfillRequested())
|
|
549
609
|
log("backfill requested: exporting full history");
|
|
550
|
-
const
|
|
610
|
+
const cursorFor = backfillRequested()
|
|
611
|
+
? (_wing) => 0
|
|
612
|
+
: (wing) => (wing ? getLastSync(wing) : getLastSync());
|
|
613
|
+
const { wings, now } = exportNewSessions(cursorFor);
|
|
551
614
|
if (wings.size === 0)
|
|
552
615
|
return;
|
|
553
616
|
miningLock = true;
|
|
@@ -568,17 +631,16 @@ function doDbSync() {
|
|
|
568
631
|
const mineNext = (i, attempt = 0) => {
|
|
569
632
|
if (i >= entries.length) {
|
|
570
633
|
miningLock = false;
|
|
571
|
-
// Advance state only on full success: on failure the same
|
|
572
|
-
// content-hashed files are re-exported and retried at the next
|
|
573
|
-
// sync (mine is idempotent).
|
|
574
|
-
markSynced(now);
|
|
575
634
|
cleanupExport(wings);
|
|
576
635
|
log("mine done");
|
|
577
636
|
const names = [...wings.keys()].join(", ");
|
|
578
637
|
const totalDrawers = [...wingDrawers.values()].reduce((a, b) => a + b, 0);
|
|
579
638
|
const detail = totalDrawers > 0 ? ` (${totalDrawers} drawers)` : "";
|
|
580
|
-
|
|
581
|
-
|
|
639
|
+
// Anything that arrived while this mine was running stays pending.
|
|
640
|
+
const remaining = countPendingMessages();
|
|
641
|
+
const tail = remaining.total > 0 ? `, ${remaining.total} message(s) still waiting` : ", queue empty";
|
|
642
|
+
toast("success", "MemPalace", `mined ${wingCount(wings)} session(s) → ${names}${detail}${tail}`);
|
|
643
|
+
ilog("mine", { outcome: "ok", sessions: wingCount(wings), wings: [...wings.keys()], drawers: totalDrawers, remaining: remaining.total });
|
|
582
644
|
return;
|
|
583
645
|
}
|
|
584
646
|
const [wing, files] = entries[i];
|
|
@@ -626,6 +688,19 @@ function doDbSync() {
|
|
|
626
688
|
}
|
|
627
689
|
log(`mined wing ${wing} (${files.length} sessions)`);
|
|
628
690
|
wingDrawers.set(wing, parseDrawers(stdout));
|
|
691
|
+
// Per-wing cursor: this wing's progress is banked even if a later
|
|
692
|
+
// wing fails — the counter never stalls on one slow wing again.
|
|
693
|
+
markSynced(now, wing);
|
|
694
|
+
for (const f of files) {
|
|
695
|
+
try {
|
|
696
|
+
unlinkSync(f);
|
|
697
|
+
}
|
|
698
|
+
catch { }
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
rmdirSync(join(OUT_DIR, wing));
|
|
702
|
+
}
|
|
703
|
+
catch { }
|
|
629
704
|
// Truthful progress: one toast per completed wing (an exact % is
|
|
630
705
|
// impossible — the mine CLI is a black box with ~4s startup cost
|
|
631
706
|
// per invocation, so per-file mines would only add overhead).
|
|
@@ -647,10 +722,11 @@ function exitSync() {
|
|
|
647
722
|
const bin = resolveBin();
|
|
648
723
|
if (!bin)
|
|
649
724
|
return;
|
|
650
|
-
const { wings, now } = exportNewSessions(getLastSync());
|
|
725
|
+
const { wings, now } = exportNewSessions((wing) => (wing ? getLastSync(wing) : getLastSync()));
|
|
651
726
|
if (wings.size === 0)
|
|
652
727
|
return;
|
|
653
728
|
const deadline = Date.now() + EXIT_BUDGET_MS;
|
|
729
|
+
const done = [];
|
|
654
730
|
for (const [wing] of wings) {
|
|
655
731
|
const remaining = deadline - Date.now();
|
|
656
732
|
if (remaining <= 0) {
|
|
@@ -663,12 +739,29 @@ function exitSync() {
|
|
|
663
739
|
timeout: Math.min(EXIT_WING_TIMEOUT_MS, remaining),
|
|
664
740
|
});
|
|
665
741
|
if (res.error || res.status !== 0) {
|
|
666
|
-
|
|
742
|
+
const why = res.error?.message || res.signal || res.status;
|
|
743
|
+
errLog(`exit mine err (${wing}): ${String(why)}`);
|
|
667
744
|
return;
|
|
668
745
|
}
|
|
746
|
+
markSynced(now, wing);
|
|
747
|
+
done.push(wing);
|
|
669
748
|
}
|
|
670
|
-
|
|
671
|
-
|
|
749
|
+
for (const wing of done) {
|
|
750
|
+
for (const f of wings.get(wing) || []) {
|
|
751
|
+
try {
|
|
752
|
+
unlinkSync(f);
|
|
753
|
+
}
|
|
754
|
+
catch { }
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
rmdirSync(join(OUT_DIR, wing));
|
|
758
|
+
}
|
|
759
|
+
catch { }
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
rmdirSync(OUT_DIR);
|
|
763
|
+
}
|
|
764
|
+
catch { }
|
|
672
765
|
log("exit save done");
|
|
673
766
|
}
|
|
674
767
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-mempalace-persistence",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
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",
|