@pi-unipi/memory 0.1.5 → 0.1.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/index.ts +30 -7
- package/package.json +1 -1
- package/storage.ts +85 -0
package/index.ts
CHANGED
|
@@ -73,7 +73,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
73
73
|
// Initialize project storage
|
|
74
74
|
const projectName = getProjectName(ctx.cwd);
|
|
75
75
|
projectStorage = new MemoryStorage(projectName);
|
|
76
|
-
|
|
76
|
+
try {
|
|
77
|
+
projectStorage.init();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.warn("[unipi/memory] Failed to initialize storage, running without memory:", (err as any)?.message ?? err);
|
|
80
|
+
projectStorage = null;
|
|
81
|
+
}
|
|
77
82
|
|
|
78
83
|
|
|
79
84
|
// Announce module
|
|
@@ -127,8 +132,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
132
|
};
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
let projectMemories: Array<{ id: string; title: string; type: string }> = [];
|
|
136
|
+
let allMemories: Array<{ project: string; id: string; title: string; type: string }> = [];
|
|
137
|
+
try {
|
|
138
|
+
projectMemories = projectStorage.listAll();
|
|
139
|
+
allMemories = listAllProjects();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.warn("[unipi/memory] Failed to list memories for info panel:", err);
|
|
142
|
+
}
|
|
132
143
|
const uniqueProjects = [...new Set(allMemories.map((m) => m.project))];
|
|
133
144
|
|
|
134
145
|
// Get 3 most recent memories (sorted by updated DESC in listAll)
|
|
@@ -153,9 +164,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
153
164
|
|
|
154
165
|
// Show memory status in UI
|
|
155
166
|
if (ctx.hasUI) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
167
|
+
let projectCount = 0;
|
|
168
|
+
let projectCountAll = 0;
|
|
169
|
+
try {
|
|
170
|
+
projectCount = projectStorage?.listAll()?.length ?? 0;
|
|
171
|
+
projectCountAll = listAllProjects().length;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.warn("[unipi/memory] Failed to count memories for status:", err);
|
|
174
|
+
}
|
|
159
175
|
const vecReady = isEmbeddingReady();
|
|
160
176
|
const vecIcon = vecReady ? "⚡" : "📝";
|
|
161
177
|
ctx.ui.setStatus(
|
|
@@ -171,7 +187,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
187
|
if (!projectStorage) return;
|
|
172
188
|
|
|
173
189
|
const projectName = getProjectName(ctx.cwd);
|
|
174
|
-
|
|
190
|
+
let projectMemories: Array<{ id: string; title: string; type: string }> = [];
|
|
191
|
+
try {
|
|
192
|
+
projectMemories = projectStorage.listAll();
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.warn("[unipi/memory] Failed to list memories for recall:", err);
|
|
195
|
+
recallDone = true; // Skip recall on error
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
175
198
|
|
|
176
199
|
if (projectMemories.length === 0) {
|
|
177
200
|
recallDone = true; // Nothing to recall, skip
|
package/package.json
CHANGED
package/storage.ts
CHANGED
|
@@ -173,6 +173,12 @@ export class MemoryStorage {
|
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
175
|
* Initialize the storage (create DB, tables, load extension).
|
|
176
|
+
*
|
|
177
|
+
* Uses retry logic to handle concurrent access from multiple Pi sessions,
|
|
178
|
+
* especially on WSL/Windows filesystem where SQLite locking can be flaky.
|
|
179
|
+
*
|
|
180
|
+
* IMPORTANT: We never delete the DB here — another session may have it open.
|
|
181
|
+
* If all retries fail, we throw and let this session run without memory.
|
|
176
182
|
*/
|
|
177
183
|
init(): void {
|
|
178
184
|
// Ensure directory exists
|
|
@@ -181,6 +187,51 @@ export class MemoryStorage {
|
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
const dbPath = path.join(this.scopeDir, MEMORY_DB_NAME);
|
|
190
|
+
const maxRetries = 5;
|
|
191
|
+
|
|
192
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
193
|
+
try {
|
|
194
|
+
this.initDb(dbPath);
|
|
195
|
+
return; // Success
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
const isTransient =
|
|
198
|
+
err?.message?.includes("disk I/O error") ||
|
|
199
|
+
err?.code === "SQLITE_IOERR" ||
|
|
200
|
+
err?.code === "SQLITE_BUSY" ||
|
|
201
|
+
err?.message?.includes("database is locked");
|
|
202
|
+
|
|
203
|
+
this.close();
|
|
204
|
+
|
|
205
|
+
if (isTransient && attempt < maxRetries) {
|
|
206
|
+
// Likely concurrent access — back off and retry.
|
|
207
|
+
// Do NOT delete the DB: another session may have it open
|
|
208
|
+
// and deleting open files on WSL/Windows is unsafe.
|
|
209
|
+
const delayMs = 50 * Math.pow(2, attempt - 1); // 50, 100, 200, 400
|
|
210
|
+
console.warn(
|
|
211
|
+
`[unipi/memory] Transient error on attempt ${attempt}/${maxRetries}, retrying in ${delayMs}ms...`
|
|
212
|
+
);
|
|
213
|
+
const end = Date.now() + delayMs;
|
|
214
|
+
while (Date.now() < end) { /* busy wait */ }
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Either non-transient error, or retries exhausted.
|
|
219
|
+
// Log and throw — this session will run without memory.
|
|
220
|
+
if (isTransient) {
|
|
221
|
+
console.warn(
|
|
222
|
+
"[unipi/memory] Could not open database after retries. " +
|
|
223
|
+
"Another session may have the DB locked. Memory unavailable this session."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Open database and set up schema. Called by init() with retry logic.
|
|
233
|
+
*/
|
|
234
|
+
private initDb(dbPath: string): void {
|
|
184
235
|
this.db = new Database(dbPath);
|
|
185
236
|
|
|
186
237
|
// Enable WAL mode for concurrent reads
|
|
@@ -216,6 +267,9 @@ export class MemoryStorage {
|
|
|
216
267
|
} catch {
|
|
217
268
|
// vec0 table may already exist or sqlite-vec not loaded
|
|
218
269
|
}
|
|
270
|
+
|
|
271
|
+
// Verify database is usable
|
|
272
|
+
this.db.prepare("SELECT 1 FROM memories LIMIT 0").get();
|
|
219
273
|
}
|
|
220
274
|
|
|
221
275
|
/**
|
|
@@ -228,6 +282,37 @@ export class MemoryStorage {
|
|
|
228
282
|
}
|
|
229
283
|
}
|
|
230
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Remove corrupted database files (db, wal, shm).
|
|
287
|
+
*/
|
|
288
|
+
private removeCorruptedDb(): void {
|
|
289
|
+
const dbPath = path.join(this.scopeDir, MEMORY_DB_NAME);
|
|
290
|
+
const files = [dbPath, `${dbPath}-wal`, `${dbPath}-shm`];
|
|
291
|
+
for (const file of files) {
|
|
292
|
+
try {
|
|
293
|
+
if (fs.existsSync(file)) {
|
|
294
|
+
fs.unlinkSync(file);
|
|
295
|
+
console.warn(`[unipi/memory] Removed corrupted file: ${file}`);
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Ignore removal errors
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check if database is healthy.
|
|
305
|
+
*/
|
|
306
|
+
isHealthy(): boolean {
|
|
307
|
+
if (!this.db) return false;
|
|
308
|
+
try {
|
|
309
|
+
this.db.prepare("SELECT 1").get();
|
|
310
|
+
return true;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
231
316
|
/**
|
|
232
317
|
* Store or update a memory record.
|
|
233
318
|
*/
|