@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.
Files changed (3) hide show
  1. package/index.ts +30 -7
  2. package/package.json +1 -1
  3. 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
- projectStorage.init();
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
- const projectMemories = projectStorage.listAll();
131
- const allMemories = listAllProjects();
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
- const projectCount = projectStorage.listAll().length;
157
- const allMemories = listAllProjects();
158
- const projectCountAll = allMemories.length;
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
- const projectMemories = projectStorage.listAll();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Persistent cross-session memory with vector search for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
  */