opencode-memory-pro 1.3.5 → 1.3.6
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 +1 -1
- package/dist/store.js +96 -38
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { requestLLMCapture, isOwnSession } from "./llm.js";
|
|
|
11
11
|
import { createMemoryTools, createFeedbackTools, createEpisodicTools } from "./tools/index.js";
|
|
12
12
|
import { sweepExpiredMemories } from "./tools/memory.js";
|
|
13
13
|
import { createGraphStore } from "./graph.js";
|
|
14
|
-
const PLUGIN_VERSION = "1.3.
|
|
14
|
+
const PLUGIN_VERSION = "1.3.6";
|
|
15
15
|
const SCHEMA_VERSION = 1;
|
|
16
16
|
// Event-driven dedup: run consolidateDuplicates on session.idle (throttled to
|
|
17
17
|
// this interval so chatty sessions aren't re-scanning the store every turn)
|
package/dist/store.js
CHANGED
|
@@ -78,6 +78,17 @@ export class MemoryStore {
|
|
|
78
78
|
// skips this cycle (the 6h interval retries later). Stale locks (owner
|
|
79
79
|
// process dead or older than the TTL) are reclaimed.
|
|
80
80
|
static OPTIMIZE_LOCK_TTL_MS = 30 * 60 * 1000;
|
|
81
|
+
// OPTIMIZE_LOCK_WAIT (1.3.6): the 1.3.4 lock gave up instantly when a live
|
|
82
|
+
// process held it, and worse, it treated an EMPTY lock file as stale and
|
|
83
|
+
// deleted it. But the owner creates the file with open("wx") and only THEN
|
|
84
|
+
// writes its pid — a reader landing in that window read 0 bytes, declared
|
|
85
|
+
// the lock stale, deleted it, and both processes "owned" the lock and raced
|
|
86
|
+
// optimize(), which is what puts "Compaction commit failed; leaving N
|
|
87
|
+
// rewritten fragments in place for GC" back on the TUI. Now a contender
|
|
88
|
+
// WAITS a bounded amount of time for a live owner to finish (serializing
|
|
89
|
+
// the compaction), and only reclaims after the pid should have been
|
|
90
|
+
// written or the 30min TTL passes.
|
|
91
|
+
static OPTIMIZE_LOCK_WAIT_MS = 10 * 1000;
|
|
81
92
|
optimizing = false;
|
|
82
93
|
lastOptimizeAt = 0;
|
|
83
94
|
constructor(dbPath, cacheConfig) {
|
|
@@ -94,7 +105,9 @@ export class MemoryStore {
|
|
|
94
105
|
async acquireOptimizeLock() {
|
|
95
106
|
await mkdir(this.dbPath, { recursive: true }).catch(() => { });
|
|
96
107
|
const lockFile = join(this.dbPath, ".optimize.lock");
|
|
97
|
-
|
|
108
|
+
const deadline = Date.now() + MemoryStore.OPTIMIZE_LOCK_WAIT_MS;
|
|
109
|
+
let waitedMs = 0;
|
|
110
|
+
for (;;) {
|
|
98
111
|
try {
|
|
99
112
|
const handle = await open(lockFile, "wx");
|
|
100
113
|
try {
|
|
@@ -102,6 +115,9 @@ export class MemoryStore {
|
|
|
102
115
|
}
|
|
103
116
|
catch { }
|
|
104
117
|
await handle.close();
|
|
118
|
+
if (waitedMs > 0) {
|
|
119
|
+
log("debug", `[store] acquired compaction lock after ${waitedMs}ms wait`);
|
|
120
|
+
}
|
|
105
121
|
return true;
|
|
106
122
|
}
|
|
107
123
|
catch (error) {
|
|
@@ -114,6 +130,16 @@ export class MemoryStore {
|
|
|
114
130
|
const ownerPid = Number(pidStr);
|
|
115
131
|
const ownerTs = Number(tsStr);
|
|
116
132
|
if (!Number.isInteger(ownerPid) || ownerPid <= 0) {
|
|
133
|
+
// The owner creates the file with open("wx") and only
|
|
134
|
+
// THEN writes the pid; reading in between yields empty
|
|
135
|
+
// content. Treat that as "being initialized", not stale
|
|
136
|
+
// — this was the 1.3.4 bug that let two instances both
|
|
137
|
+
// own the lock and race optimize().
|
|
138
|
+
if (waitedMs < 250) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
140
|
+
waitedMs += 50;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
117
143
|
stale = true;
|
|
118
144
|
}
|
|
119
145
|
else if (Number.isFinite(ownerTs) && Date.now() - ownerTs > MemoryStore.OPTIMIZE_LOCK_TTL_MS) {
|
|
@@ -127,16 +153,37 @@ export class MemoryStore {
|
|
|
127
153
|
stale = true;
|
|
128
154
|
}
|
|
129
155
|
}
|
|
156
|
+
else {
|
|
157
|
+
// Same process already owns it (shouldn't happen with
|
|
158
|
+
// the optimizing guard; never deadlock on ourselves).
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
130
161
|
}
|
|
131
162
|
catch {
|
|
163
|
+
// Lock vanished between the EEXIST and the read (owner
|
|
164
|
+
// released); give it a short grace before reclaiming.
|
|
165
|
+
if (waitedMs < 150) {
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
167
|
+
waitedMs += 50;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
132
170
|
stale = true;
|
|
133
171
|
}
|
|
134
|
-
if (!stale)
|
|
135
|
-
|
|
172
|
+
if (!stale) {
|
|
173
|
+
// Live owner: wait for it to finish instead of racing it,
|
|
174
|
+
// until the bounded deadline (then skip this cycle).
|
|
175
|
+
if (Date.now() >= deadline) {
|
|
176
|
+
logFileOnly("debug", "[store] compaction lock still held after waiting; skipping this cycle");
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
180
|
+
waitedMs += 100;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
// Stale: reclaim and loop back to try creating the lock.
|
|
136
184
|
await rm(lockFile, { force: true }).catch(() => { });
|
|
137
185
|
}
|
|
138
186
|
}
|
|
139
|
-
return false;
|
|
140
187
|
}
|
|
141
188
|
async releaseOptimizeLock() {
|
|
142
189
|
await rm(join(this.dbPath, ".optimize.lock"), { force: true }).catch(() => { });
|
|
@@ -155,42 +202,51 @@ export class MemoryStore {
|
|
|
155
202
|
async maybeOptimizeAll(force = false) {
|
|
156
203
|
if (this.optimizing)
|
|
157
204
|
return;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
205
|
+
// OPTIMIZE_GUARD (1.3.6): set the in-process guard synchronously,
|
|
206
|
+
// BEFORE any await. The 1.3.4 code set it only after the async
|
|
207
|
+
// candidate enumeration, so two overlapping calls in one process (the
|
|
208
|
+
// fire-and-forget write trigger plus an awaited explicit call on the
|
|
209
|
+
// first turn) could both pass the guard and run optimize()
|
|
210
|
+
// concurrently — another way into the "Compaction commit failed" race.
|
|
211
|
+
this.optimizing = true;
|
|
212
|
+
let attempted = false;
|
|
213
|
+
try {
|
|
214
|
+
const elapsed = Date.now() - this.lastOptimizeAt;
|
|
215
|
+
if (!force && elapsed < MemoryStore.OPTIMIZE_INTERVAL_MS)
|
|
216
|
+
return;
|
|
217
|
+
const tables = [this.table, this.eventTable, this.episodicTaskTable].filter(Boolean);
|
|
218
|
+
const candidates = [];
|
|
219
|
+
for (const table of tables) {
|
|
220
|
+
let count = 0;
|
|
221
|
+
// LANCE_COMPACTION_FIX (1.1.6): LanceDB stores each table on disk as
|
|
222
|
+
// "<name>.lance", but Table.name only carries the bare name — so the
|
|
223
|
+
// old readdir(.../table.name/_versions) always hit ENOENT, the catch
|
|
224
|
+
// swallowed it, and optimize() NEVER ran. Result: 13k+ _versions and
|
|
225
|
+
// 11k+ fragment files accumulated (disk + native handle/cache growth
|
|
226
|
+
// per write, EMFILE/OOM risk). Try the real on-disk dir first.
|
|
227
|
+
for (const dirName of [`${table.name}.lance`, table.name]) {
|
|
228
|
+
try {
|
|
229
|
+
const entries = await readdir(join(this.dbPath, dirName, "_versions"), { withFileTypes: true });
|
|
230
|
+
count = entries.filter((e) => e.isFile()).length;
|
|
231
|
+
if (count > 0)
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
235
|
+
}
|
|
236
|
+
if (force || count >= MemoryStore.OPTIMIZE_MIN_VERSIONS) {
|
|
237
|
+
candidates.push({ table, count });
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
log("debug", `[store] optimize skipped for ${table.name}: ${count} versions (min ${MemoryStore.OPTIMIZE_MIN_VERSIONS})`);
|
|
177
241
|
}
|
|
178
|
-
catch { }
|
|
179
|
-
}
|
|
180
|
-
if (force || count >= MemoryStore.OPTIMIZE_MIN_VERSIONS) {
|
|
181
|
-
candidates.push({ table, count });
|
|
182
242
|
}
|
|
183
|
-
|
|
184
|
-
|
|
243
|
+
if (force) {
|
|
244
|
+
this.lastOptimizeAt = Date.now();
|
|
245
|
+
attempted = true;
|
|
185
246
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
if (candidates.length === 0)
|
|
191
|
-
return;
|
|
192
|
-
this.optimizing = true;
|
|
193
|
-
try {
|
|
247
|
+
if (candidates.length === 0)
|
|
248
|
+
return;
|
|
249
|
+
attempted = true;
|
|
194
250
|
const lockHeld = await this.acquireOptimizeLock();
|
|
195
251
|
if (!lockHeld) {
|
|
196
252
|
logFileOnly("warn", "[store] optimize skipped: another process holds the compaction lock (retries next interval)");
|
|
@@ -226,7 +282,9 @@ export class MemoryStore {
|
|
|
226
282
|
}
|
|
227
283
|
finally {
|
|
228
284
|
this.optimizing = false;
|
|
229
|
-
|
|
285
|
+
if (attempted) {
|
|
286
|
+
this.lastOptimizeAt = Date.now();
|
|
287
|
+
}
|
|
230
288
|
}
|
|
231
289
|
}
|
|
232
290
|
async init(vectorDim) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-memory-pro",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "LanceDB-backed long-term memory provider for OpenCode — standalone fork of lancedb-opencode-pro with entity graph, lifecycle, and retention",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|