opencode-mempalace-persistence 2.5.1 → 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 +116 -41
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -82,21 +82,35 @@ function toastsEnabled() {
|
|
|
82
82
|
catch { }
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
|
-
// Messages arrived after
|
|
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() {
|
|
87
88
|
try {
|
|
89
|
+
const st = readSyncState();
|
|
88
90
|
const out = runPython(`
|
|
89
|
-
import sqlite3
|
|
91
|
+
import sqlite3, json, re
|
|
90
92
|
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
91
|
-
|
|
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()
|
|
92
99
|
db.close()
|
|
93
|
-
|
|
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))
|
|
94
107
|
`);
|
|
95
|
-
const
|
|
96
|
-
|
|
108
|
+
const byWing = JSON.parse(out);
|
|
109
|
+
const total = Object.values(byWing).reduce((a, b) => a + b, 0);
|
|
110
|
+
return { total, byWing };
|
|
97
111
|
}
|
|
98
112
|
catch {
|
|
99
|
-
return 0;
|
|
113
|
+
return { total: 0, byWing: {} };
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
116
|
let cachedName = undefined;
|
|
@@ -379,15 +393,26 @@ function summarizeToolCall(tool, args, out) {
|
|
|
379
393
|
const answered = (out || "").replace(/\s+/g, " ").slice(0, 80) || "(empty)";
|
|
380
394
|
return `${short} · asked: ${asked} → ${answered}`.slice(0, 220);
|
|
381
395
|
}
|
|
382
|
-
function
|
|
383
|
-
if (!existsSync(STATE_FILE))
|
|
384
|
-
return 0;
|
|
396
|
+
function readSyncState() {
|
|
385
397
|
try {
|
|
386
|
-
|
|
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
|
+
}
|
|
387
402
|
}
|
|
388
|
-
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))
|
|
389
411
|
return 0;
|
|
390
|
-
|
|
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;
|
|
391
416
|
}
|
|
392
417
|
function dbSync() {
|
|
393
418
|
if (miningLock)
|
|
@@ -402,11 +427,12 @@ function dbSync() {
|
|
|
402
427
|
function backfillRequested() {
|
|
403
428
|
return !!process.env.OPENCODE_MEMPALACE_BACKFILL;
|
|
404
429
|
}
|
|
405
|
-
// Export
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
// content hash, so re-exports are naturally idempotent.
|
|
409
|
-
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);
|
|
410
436
|
const sessions = runPython(`
|
|
411
437
|
import sqlite3, json
|
|
412
438
|
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
@@ -442,12 +468,13 @@ print(json.dumps(rows))
|
|
|
442
468
|
.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40) || "global";
|
|
443
469
|
const label = (title || "").replace(/[^a-zA-Z0-9 _-]/g, "_") || (sessId || "").slice(0, 12);
|
|
444
470
|
const prefix = `${new Date().toISOString().slice(0, 10)}_${label.slice(0, 30)}_${(sessId || "").slice(0, 8)}`;
|
|
471
|
+
const wingSince = cursorFor(wing);
|
|
445
472
|
const msgs = runPython(`
|
|
446
473
|
import sqlite3, json
|
|
447
474
|
db = sqlite3.connect(${JSON.stringify(OPENCODE_DB)})
|
|
448
475
|
rows = db.execute("""
|
|
449
476
|
SELECT m.id, m.time_created, m.data FROM message m
|
|
450
|
-
WHERE m.session_id = ${JSON.stringify(sessId)} AND m.time_created > ${
|
|
477
|
+
WHERE m.session_id = ${JSON.stringify(sessId)} AND m.time_created > ${wingSince}
|
|
451
478
|
ORDER BY m.time_created
|
|
452
479
|
""").fetchall()
|
|
453
480
|
texts = []
|
|
@@ -516,8 +543,23 @@ print(json.dumps({"texts": texts, "incomplete": incomplete}))
|
|
|
516
543
|
}
|
|
517
544
|
return { wings, now: cursor };
|
|
518
545
|
}
|
|
519
|
-
function markSynced(now) {
|
|
520
|
-
|
|
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
|
+
}
|
|
521
563
|
lastSyncTs = Date.now();
|
|
522
564
|
}
|
|
523
565
|
// Default `exchange` extraction: one drawer per exchange pair, verbatim,
|
|
@@ -531,15 +573,18 @@ function mineArgs(wingDir, wing) {
|
|
|
531
573
|
return ["mine", wingDir, "--mode", "convos", "--agent", "opencode", "--wing", wing];
|
|
532
574
|
}
|
|
533
575
|
function cleanupExport(wings) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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 { }
|
|
538
585
|
}
|
|
539
|
-
catch { }
|
|
540
586
|
}
|
|
541
|
-
|
|
542
|
-
for (const wing of wings.keys()) {
|
|
587
|
+
catch { }
|
|
543
588
|
try {
|
|
544
589
|
rmdirSync(join(OUT_DIR, wing));
|
|
545
590
|
}
|
|
@@ -562,7 +607,10 @@ function doDbSync() {
|
|
|
562
607
|
const sinceMs = backfillRequested() ? 0 : getLastSync();
|
|
563
608
|
if (backfillRequested())
|
|
564
609
|
log("backfill requested: exporting full history");
|
|
565
|
-
const
|
|
610
|
+
const cursorFor = backfillRequested()
|
|
611
|
+
? (_wing) => 0
|
|
612
|
+
: (wing) => (wing ? getLastSync(wing) : getLastSync());
|
|
613
|
+
const { wings, now } = exportNewSessions(cursorFor);
|
|
566
614
|
if (wings.size === 0)
|
|
567
615
|
return;
|
|
568
616
|
miningLock = true;
|
|
@@ -583,20 +631,16 @@ function doDbSync() {
|
|
|
583
631
|
const mineNext = (i, attempt = 0) => {
|
|
584
632
|
if (i >= entries.length) {
|
|
585
633
|
miningLock = false;
|
|
586
|
-
// Advance state only on full success: on failure the same
|
|
587
|
-
// content-hashed files are re-exported and retried at the next
|
|
588
|
-
// sync (mine is idempotent).
|
|
589
|
-
markSynced(now);
|
|
590
634
|
cleanupExport(wings);
|
|
591
635
|
log("mine done");
|
|
592
636
|
const names = [...wings.keys()].join(", ");
|
|
593
637
|
const totalDrawers = [...wingDrawers.values()].reduce((a, b) => a + b, 0);
|
|
594
638
|
const detail = totalDrawers > 0 ? ` (${totalDrawers} drawers)` : "";
|
|
595
639
|
// Anything that arrived while this mine was running stays pending.
|
|
596
|
-
const remaining = countPendingMessages(
|
|
597
|
-
const tail = remaining > 0 ? `, ${remaining} message(s) still waiting` : ", queue empty";
|
|
640
|
+
const remaining = countPendingMessages();
|
|
641
|
+
const tail = remaining.total > 0 ? `, ${remaining.total} message(s) still waiting` : ", queue empty";
|
|
598
642
|
toast("success", "MemPalace", `mined ${wingCount(wings)} session(s) → ${names}${detail}${tail}`);
|
|
599
|
-
ilog("mine", { outcome: "ok", sessions: wingCount(wings), wings: [...wings.keys()], drawers: totalDrawers, remaining });
|
|
643
|
+
ilog("mine", { outcome: "ok", sessions: wingCount(wings), wings: [...wings.keys()], drawers: totalDrawers, remaining: remaining.total });
|
|
600
644
|
return;
|
|
601
645
|
}
|
|
602
646
|
const [wing, files] = entries[i];
|
|
@@ -644,6 +688,19 @@ function doDbSync() {
|
|
|
644
688
|
}
|
|
645
689
|
log(`mined wing ${wing} (${files.length} sessions)`);
|
|
646
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 { }
|
|
647
704
|
// Truthful progress: one toast per completed wing (an exact % is
|
|
648
705
|
// impossible — the mine CLI is a black box with ~4s startup cost
|
|
649
706
|
// per invocation, so per-file mines would only add overhead).
|
|
@@ -665,10 +722,11 @@ function exitSync() {
|
|
|
665
722
|
const bin = resolveBin();
|
|
666
723
|
if (!bin)
|
|
667
724
|
return;
|
|
668
|
-
const { wings, now } = exportNewSessions(getLastSync());
|
|
725
|
+
const { wings, now } = exportNewSessions((wing) => (wing ? getLastSync(wing) : getLastSync()));
|
|
669
726
|
if (wings.size === 0)
|
|
670
727
|
return;
|
|
671
728
|
const deadline = Date.now() + EXIT_BUDGET_MS;
|
|
729
|
+
const done = [];
|
|
672
730
|
for (const [wing] of wings) {
|
|
673
731
|
const remaining = deadline - Date.now();
|
|
674
732
|
if (remaining <= 0) {
|
|
@@ -681,12 +739,29 @@ function exitSync() {
|
|
|
681
739
|
timeout: Math.min(EXIT_WING_TIMEOUT_MS, remaining),
|
|
682
740
|
});
|
|
683
741
|
if (res.error || res.status !== 0) {
|
|
684
|
-
|
|
742
|
+
const why = res.error?.message || res.signal || res.status;
|
|
743
|
+
errLog(`exit mine err (${wing}): ${String(why)}`);
|
|
685
744
|
return;
|
|
686
745
|
}
|
|
746
|
+
markSynced(now, wing);
|
|
747
|
+
done.push(wing);
|
|
687
748
|
}
|
|
688
|
-
|
|
689
|
-
|
|
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 { }
|
|
690
765
|
log("exit save done");
|
|
691
766
|
}
|
|
692
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",
|