@oh-my-pi/pi-mnemopi 17.2.0 → 17.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.2] - 2026-07-31
6
+
7
+ ### Fixed
8
+
9
+ - Fixed a resource leak where SQLite prepared statements were not properly released, keeping the database connection alive after calling close(). This resolves file locking issues on Windows (which prevented deleting, moving, or rotating database files) and silent file handle leaks on POSIX systems.
10
+
5
11
  ## [17.0.8] - 2026-07-22
6
12
 
7
13
  ### Changed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-mnemopi",
4
- "version": "17.2.0",
4
+ "version": "17.2.2",
5
5
  "description": "Local SQLite memory engine for Oh My Pi agents",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,10 +39,10 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "17.2.0",
43
- "@oh-my-pi/pi-catalog": "17.2.0",
44
- "@oh-my-pi/pi-natives": "17.2.0",
45
- "@oh-my-pi/pi-utils": "17.2.0",
42
+ "@oh-my-pi/pi-ai": "17.2.2",
43
+ "@oh-my-pi/pi-catalog": "17.2.2",
44
+ "@oh-my-pi/pi-natives": "17.2.2",
45
+ "@oh-my-pi/pi-utils": "17.2.2",
46
46
  "lru-cache": "11.5.2"
47
47
  },
48
48
  "peerDependencies": {
@@ -110,7 +110,7 @@ interface StatementRunResult {
110
110
  readonly lastInsertRowid: number | bigint;
111
111
  }
112
112
 
113
- interface WritableStatement {
113
+ interface WritableStatement extends Disposable {
114
114
  run(...params: SqlValue[]): StatementRunResult;
115
115
  }
116
116
 
@@ -249,11 +249,10 @@ export class AnnotationStore {
249
249
  }
250
250
 
251
251
  add(memoryId: string, kind: string, value: string, source = "", confidence = 1.0): number {
252
- const result = this.db
253
- .prepare(
254
- "INSERT OR IGNORE INTO annotations (memory_id, kind, value, source, confidence) VALUES (?, ?, ?, ?, ?)",
255
- )
256
- .run(memoryId, kind, value, source, confidence);
252
+ const result = this.db.run(
253
+ "INSERT OR IGNORE INTO annotations (memory_id, kind, value, source, confidence) VALUES (?, ?, ?, ?, ?)",
254
+ [memoryId, kind, value, source, confidence],
255
+ );
257
256
  return Number(result.lastInsertRowid);
258
257
  }
259
258
 
@@ -267,7 +266,7 @@ export class AnnotationStore {
267
266
  if (!values || values.length === 0) return 0;
268
267
  const rows = values.filter(value => value.length > 0 && value.trim().length > 0);
269
268
  if (rows.length === 0) return 0;
270
- const insert = this.db.prepare(
269
+ using insert = this.db.prepare(
271
270
  "INSERT OR IGNORE INTO annotations (memory_id, kind, value, source, confidence) VALUES (?, ?, ?, ?, ?)",
272
271
  );
273
272
  transaction(this.db, () => {
@@ -280,10 +279,8 @@ export class AnnotationStore {
280
279
  kind === null || kind === undefined
281
280
  ? "SELECT * FROM annotations WHERE memory_id = ? ORDER BY created_at ASC, id ASC"
282
281
  : "SELECT * FROM annotations WHERE memory_id = ? AND kind = ? ORDER BY created_at ASC, id ASC";
283
- const rows =
284
- kind === null || kind === undefined
285
- ? this.db.prepare(sql).all(memoryId)
286
- : this.db.prepare(sql).all(memoryId, kind);
282
+ using statement = this.db.prepare(sql);
283
+ const rows = kind === null || kind === undefined ? statement.all(memoryId) : statement.all(memoryId, kind);
287
284
  return (rows as AnnotationRow[]).map(normalizeRow);
288
285
  }
289
286
  queryByKind(
@@ -307,23 +304,24 @@ export class AnnotationStore {
307
304
  conditions.push("memory_id = ?");
308
305
  params.push(memoryId);
309
306
  }
310
- const rows = this.db
311
- .prepare(`SELECT * FROM annotations WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC, id ASC`)
312
- .all(...params) as AnnotationRow[];
307
+ using statement = this.db.prepare(
308
+ `SELECT * FROM annotations WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC, id ASC`,
309
+ );
310
+ const rows = statement.all(...params) as AnnotationRow[];
313
311
  const normalized = rows.map(normalizeRow);
314
312
  const filterNoise = options.filter_noise ?? options.filterNoise ?? true;
315
313
  return filterNoise && kind === "mentions" ? filterCleanMentions(normalized) : normalized;
316
314
  }
317
315
  getDistinctValues(kind: string): string[] {
318
- const rows = this.db
319
- .prepare("SELECT DISTINCT value FROM annotations WHERE kind = ? ORDER BY value")
320
- .all(kind) as { value: string }[];
316
+ using statement = this.db.prepare("SELECT DISTINCT value FROM annotations WHERE kind = ? ORDER BY value");
317
+ const rows = statement.all(kind) as { value: string }[];
321
318
  return rows.map(row => row.value);
322
319
  }
323
320
  exportAll(): AnnotationRow[] {
324
- const rows = this.db
325
- .prepare("SELECT id, memory_id, kind, value, source, confidence, created_at FROM annotations ORDER BY id")
326
- .all() as AnnotationRow[];
321
+ using statement = this.db.prepare(
322
+ "SELECT id, memory_id, kind, value, source, confidence, created_at FROM annotations ORDER BY id",
323
+ );
324
+ const rows = statement.all() as AnnotationRow[];
327
325
  return rows.map(normalizeRow);
328
326
  }
329
327
  importAll(annotations: readonly AnnotationInput[], force = false): AnnotationImportStats {
@@ -346,19 +344,20 @@ export class AnnotationStore {
346
344
  }
347
345
 
348
346
  transaction(this.db, () => {
349
- const existingRows = this.db
350
- .prepare("SELECT id, memory_id, kind, value, source, confidence, created_at FROM annotations")
351
- .all() as AnnotationRow[];
347
+ using existingStatement = this.db.prepare(
348
+ "SELECT id, memory_id, kind, value, source, confidence, created_at FROM annotations",
349
+ );
350
+ const existingRows = existingStatement.all() as AnnotationRow[];
352
351
  const existing = new Map<number, StoredAnnotationContent>();
353
352
  for (const row of existingRows) existing.set(Number(row.id), normalizeRow(row));
354
353
 
355
- const insertWithId = this.db.prepare(
354
+ using insertWithId = this.db.prepare(
356
355
  "INSERT INTO annotations (id, memory_id, kind, value, source, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
357
356
  ) as WritableStatement;
358
- const insertWithoutId = this.db.prepare(
357
+ using insertWithoutId = this.db.prepare(
359
358
  "INSERT INTO annotations (memory_id, kind, value, source, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?)",
360
359
  ) as WritableStatement;
361
- const deleteById = this.db.prepare("DELETE FROM annotations WHERE id = ?");
360
+ using deleteById = this.db.prepare("DELETE FROM annotations WHERE id = ?");
362
361
 
363
362
  for (const item of annotations) {
364
363
  const id = rowId(item.id);
@@ -828,8 +828,8 @@ function extractKeySignal(content: string, maxChars: number): string {
828
828
  }
829
829
 
830
830
  function invalidateEpisodicVectors(beam: BeamMemoryState, memoryId: string): void {
831
- beam.db.prepare("DELETE FROM memory_embeddings WHERE memory_id = ?").run(memoryId);
832
- beam.db.prepare("UPDATE episodic_memory SET binary_vector = NULL WHERE id = ?").run(memoryId);
831
+ beam.db.run("DELETE FROM memory_embeddings WHERE memory_id = ?", [memoryId]);
832
+ beam.db.run("UPDATE episodic_memory SET binary_vector = NULL WHERE id = ?", [memoryId]);
833
833
  }
834
834
 
835
835
  export function degradeEpisodic(beam: BeamMemoryState, dryRun = false): Record<string, JsonValue> {
@@ -782,7 +782,7 @@ async function runEmbedding(beam: BeamMemoryState, items: readonly EmbedItem[]):
782
782
  const matrix = await embed(items.map(item => item.content));
783
783
  if (matrix === null) return;
784
784
  const model = currentEmbeddingModel();
785
- const insertEmbedding = beam.db.prepare(
785
+ using insertEmbedding = beam.db.prepare(
786
786
  "INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model) VALUES (?, ?, ?)",
787
787
  );
788
788
  const insertMany = beam.db.transaction((rows: readonly EmbedItem[]) => {
@@ -157,18 +157,16 @@ function invalidateCaches(beam: BeamMemoryState): void {
157
157
  }
158
158
 
159
159
  function findDuplicate(beam: BeamMemoryState, content: string): string | null {
160
- const row = beam.db
161
- .prepare("SELECT id FROM working_memory WHERE content = ? AND session_id = ? LIMIT 1")
162
- .get(content, beam.sessionId) as { id: string } | null;
160
+ using statement = beam.db.prepare("SELECT id FROM working_memory WHERE content = ? AND session_id = ? LIMIT 1");
161
+ const row = statement.get(content, beam.sessionId) as { id: string } | null;
163
162
  return row?.id ?? null;
164
163
  }
165
164
 
166
165
  function tableExists(db: BeamMemoryState["db"], table: string): boolean {
167
- return (
168
- db
169
- .prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table','virtual table') AND name = ? LIMIT 1")
170
- .get(table) !== null
166
+ using statement = db.prepare(
167
+ "SELECT 1 FROM sqlite_master WHERE type IN ('table','virtual table') AND name = ? LIMIT 1",
171
168
  );
169
+ return statement.get(table) !== null;
172
170
  }
173
171
 
174
172
  /** Tables whose rows point back to a `working_memory` id via `source_memory_id`. */
@@ -197,29 +195,30 @@ function purgeWorkingMemoryArtifacts(db: BeamMemoryState["db"], ids: readonly st
197
195
  const graphRefs = new Set<string>(ids);
198
196
  for (const id of ids) graphRefs.add(`gist_${id}`);
199
197
  if (tableExists(db, "facts")) {
200
- const factRows = db.prepare(`SELECT fact_id FROM facts WHERE source_msg_id IN (${placeholders})`).all(...ids) as {
198
+ using factStatement = db.prepare(`SELECT fact_id FROM facts WHERE source_msg_id IN (${placeholders})`);
199
+ const factRows = factStatement.all(...ids) as {
201
200
  fact_id: string;
202
201
  }[];
203
202
  for (const row of factRows) graphRefs.add(row.fact_id);
204
- db.prepare(`DELETE FROM facts WHERE source_msg_id IN (${placeholders})`).run(...ids);
203
+ db.run(`DELETE FROM facts WHERE source_msg_id IN (${placeholders})`, [...ids]);
205
204
  }
206
205
 
207
- db.prepare(`DELETE FROM annotations WHERE memory_id IN (${placeholders})`).run(...ids);
208
- db.prepare(`DELETE FROM memory_embeddings WHERE memory_id IN (${placeholders})`).run(...ids);
206
+ db.run(`DELETE FROM annotations WHERE memory_id IN (${placeholders})`, [...ids]);
207
+ db.run(`DELETE FROM memory_embeddings WHERE memory_id IN (${placeholders})`, [...ids]);
209
208
  for (const table of MEMORIA_SOURCE_TABLES) {
210
- db.prepare(`DELETE FROM ${table} WHERE source_memory_id IN (${placeholders})`).run(...ids);
209
+ db.run(`DELETE FROM ${table} WHERE source_memory_id IN (${placeholders})`, [...ids]);
211
210
  }
212
211
 
213
212
  if (tableExists(db, "gists")) {
214
- db.prepare(`DELETE FROM gists WHERE memory_id IN (${placeholders})`).run(...ids);
213
+ db.run(`DELETE FROM gists WHERE memory_id IN (${placeholders})`, [...ids]);
215
214
  }
216
215
  if (tableExists(db, "graph_edges")) {
217
216
  const refs = [...graphRefs];
218
217
  const refPlaceholders = refs.map(() => "?").join(", ");
219
- db.prepare(`DELETE FROM graph_edges WHERE source IN (${refPlaceholders}) OR target IN (${refPlaceholders})`).run(
218
+ db.run(`DELETE FROM graph_edges WHERE source IN (${refPlaceholders}) OR target IN (${refPlaceholders})`, [
220
219
  ...refs,
221
220
  ...refs,
222
- );
221
+ ]);
223
222
  }
224
223
  }
225
224
 
@@ -237,30 +236,30 @@ function trimWorkingMemory(beam: BeamMemoryState): void {
237
236
  const ttlHours = beam.config.workingMemoryTtlHours;
238
237
  const cutoff = toUtcIso(new Date(Date.now() - ttlHours * 3_600_000));
239
238
  transaction(beam.db, () => {
240
- const ids = (
241
- beam.db
242
- .prepare(`
239
+ using selectStatement = beam.db.prepare(`
240
+ SELECT id FROM working_memory
241
+ WHERE session_id = ?
242
+ AND consolidated_at IS NULL
243
+ AND trust_tier IS NOT 'IMPORTED'
244
+ AND (
245
+ timestamp < ? OR
246
+ id NOT IN (
243
247
  SELECT id FROM working_memory
244
- WHERE session_id = ?
245
- AND consolidated_at IS NULL
246
- AND trust_tier IS NOT 'IMPORTED'
247
- AND (
248
- timestamp < ? OR
249
- id NOT IN (
250
- SELECT id FROM working_memory
251
- WHERE session_id = ? AND consolidated_at IS NULL AND trust_tier IS NOT 'IMPORTED'
252
- ORDER BY timestamp DESC
253
- LIMIT ?
254
- )
255
- )
256
- `)
257
- .all(beam.sessionId, cutoff, beam.sessionId, limit) as { id: string }[]
258
- ).map(row => row.id);
248
+ WHERE session_id = ? AND consolidated_at IS NULL AND trust_tier IS NOT 'IMPORTED'
249
+ ORDER BY timestamp DESC
250
+ LIMIT ?
251
+ )
252
+ )
253
+ `);
254
+ const ids = (selectStatement.all(beam.sessionId, cutoff, beam.sessionId, limit) as { id: string }[]).map(
255
+ row => row.id,
256
+ );
259
257
  if (ids.length === 0) return;
260
258
  const placeholders = ids.map(() => "?").join(", ");
261
- beam.db
262
- .prepare(`DELETE FROM working_memory WHERE id IN (${placeholders}) AND session_id = ?`)
263
- .run(...ids, beam.sessionId);
259
+ beam.db.run(`DELETE FROM working_memory WHERE id IN (${placeholders}) AND session_id = ?`, [
260
+ ...ids,
261
+ beam.sessionId,
262
+ ]);
264
263
  purgeWorkingMemoryArtifacts(beam.db, ids);
265
264
  });
266
265
  }
@@ -396,11 +395,11 @@ export function reconcileEmbeddingModel(beam: BeamMemoryState): void {
396
395
  .all() as EmbedItem[];
397
396
 
398
397
  transaction(beam.db, () => {
399
- beam.db.prepare("DELETE FROM memory_embeddings").run();
400
- beam.db.prepare("UPDATE episodic_memory SET binary_vector = NULL").run();
398
+ beam.db.run("DELETE FROM memory_embeddings");
399
+ beam.db.run("UPDATE episodic_memory SET binary_vector = NULL");
401
400
  if (vecAvailable(beam.db)) {
402
401
  try {
403
- beam.db.prepare("DELETE FROM vec_episodes").run();
402
+ beam.db.run("DELETE FROM vec_episodes");
404
403
  } catch {
405
404
  // sqlite-vec cleanup is best-effort; rebuild correctness takes precedence.
406
405
  }
@@ -451,8 +450,8 @@ export function remember(beam: BeamMemoryState, content: string, options: StoreR
451
450
 
452
451
  const existingId = findDuplicate(beam, content);
453
452
  if (existingId !== null) {
454
- beam.db
455
- .prepare(`
453
+ beam.db.run(
454
+ `
456
455
  UPDATE working_memory
457
456
  SET importance = MAX(importance, ?), timestamp = ?, source = ?,
458
457
  valid_until = COALESCE(?, valid_until),
@@ -466,8 +465,8 @@ export function remember(beam: BeamMemoryState, content: string, options: StoreR
466
465
  embed_text = COALESCE(?, embed_text),
467
466
  consolidated_at = NULL
468
467
  WHERE id = ? AND session_id = ?
469
- `)
470
- .run(
468
+ `,
469
+ [
471
470
  importance,
472
471
  timestamp,
473
472
  source,
@@ -483,7 +482,8 @@ export function remember(beam: BeamMemoryState, content: string, options: StoreR
483
482
  storedEmbeddingText(content, embedText),
484
483
  existingId,
485
484
  beam.sessionId,
486
- );
485
+ ],
486
+ );
487
487
  emitEvent(beam, "MEMORY_UPDATED", {
488
488
  memoryId: existingId,
489
489
  content,
@@ -497,14 +497,14 @@ export function remember(beam: BeamMemoryState, content: string, options: StoreR
497
497
  }
498
498
 
499
499
  const memoryId = options.memoryId ?? options.memory_id ?? generateId(content, new Date(timestamp));
500
- beam.db
501
- .prepare(`
500
+ beam.db.run(
501
+ `
502
502
  INSERT INTO working_memory
503
503
  (id, content, embed_text, source, timestamp, session_id, importance, metadata_json, valid_until, scope,
504
504
  author_id, author_type, channel_id, veracity, memory_type, trust_tier)
505
505
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
506
- `)
507
- .run(
506
+ `,
507
+ [
508
508
  memoryId,
509
509
  content,
510
510
  storedEmbeddingText(content, embedText),
@@ -521,7 +521,8 @@ export function remember(beam: BeamMemoryState, content: string, options: StoreR
521
521
  veracity,
522
522
  memoryType,
523
523
  trustTier,
524
- );
524
+ ],
525
+ );
525
526
  addTemporalAnnotations(beam, memoryId, timestamp, source);
526
527
  // `extractText` lets a caller decouple "what gets stored" from "what facts are
527
528
  // mined". coding-agent retains full multi-author transcripts but wants
@@ -560,7 +561,7 @@ export function rememberBatch(
560
561
  const trustTier = normalizeTrustTier(options.trustTier ?? "IMPORTED", "imported");
561
562
 
562
563
  transaction(beam.db, () => {
563
- const statement = beam.db.prepare(`
564
+ using statement = beam.db.prepare(`
564
565
  INSERT INTO working_memory
565
566
  (id, content, embed_text, source, timestamp, session_id, importance, metadata_json,
566
567
  author_id, author_type, channel_id, memory_type, veracity, trust_tier, scope)
@@ -625,41 +626,40 @@ export function rememberBatch(
625
626
 
626
627
  export function getContext(beam: BeamMemoryState, limit = 10): Row[] {
627
628
  const now = toUtcIso();
628
- return (
629
- beam.db
630
- .prepare(`
631
- SELECT id, content, source, timestamp, importance, scope
632
- FROM working_memory
633
- WHERE (session_id = ? OR scope = 'global')
634
- AND (valid_until IS NULL OR valid_until > ?)
635
- AND superseded_by IS NULL
636
- ORDER BY
637
- CASE WHEN scope = 'global' THEN 0 ELSE 1 END,
638
- importance DESC,
639
- timestamp DESC
640
- LIMIT ?
641
- `)
642
- .all(beam.sessionId, now, limit) as Row[]
643
- ).map(rowToDict);
629
+ using statement = beam.db.prepare(`
630
+ SELECT id, content, source, timestamp, importance, scope
631
+ FROM working_memory
632
+ WHERE (session_id = ? OR scope = 'global')
633
+ AND (valid_until IS NULL OR valid_until > ?)
634
+ AND superseded_by IS NULL
635
+ ORDER BY
636
+ CASE WHEN scope = 'global' THEN 0 ELSE 1 END,
637
+ importance DESC,
638
+ timestamp DESC
639
+ LIMIT ?
640
+ `);
641
+ return (statement.all(beam.sessionId, now, limit) as Row[]).map(rowToDict);
644
642
  }
645
643
 
646
644
  export function invalidate(beam: BeamMemoryState, memoryId: string, replacementId: string | null = null): boolean {
647
645
  const now = toUtcIso();
648
- const working = beam.db
649
- .prepare(`
646
+ const working = beam.db.run(
647
+ `
650
648
  UPDATE working_memory
651
649
  SET valid_until = ?, superseded_by = ?
652
650
  WHERE id = ? AND (session_id = ? OR scope = 'global')
653
- `)
654
- .run(now, replacementId, memoryId, beam.sessionId);
651
+ `,
652
+ [now, replacementId, memoryId, beam.sessionId],
653
+ );
655
654
  if (working.changes > 0) return true;
656
- const episodic = beam.db
657
- .prepare(`
655
+ const episodic = beam.db.run(
656
+ `
658
657
  UPDATE episodic_memory
659
658
  SET valid_until = ?, superseded_by = ?
660
659
  WHERE id = ? AND (session_id = ? OR scope = 'global')
661
- `)
662
- .run(now, replacementId, memoryId, beam.sessionId);
660
+ `,
661
+ [now, replacementId, memoryId, beam.sessionId],
662
+ );
663
663
  return episodic.changes > 0;
664
664
  }
665
665
 
@@ -684,12 +684,14 @@ export function getWorkingStats(
684
684
  params.push(channelId);
685
685
  }
686
686
  const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
687
- const total = beam.db.prepare(`SELECT COUNT(*) AS total FROM working_memory${where}`).get(...params) as {
687
+ using totalStatement = beam.db.prepare(`SELECT COUNT(*) AS total FROM working_memory${where}`);
688
+ const total = totalStatement.get(...params) as {
688
689
  total: number;
689
690
  };
690
- const last = beam.db
691
- .prepare(`SELECT timestamp FROM working_memory${where} ORDER BY timestamp DESC LIMIT 1`)
692
- .get(...params) as { timestamp: string | null } | null;
691
+ using lastStatement = beam.db.prepare(
692
+ `SELECT timestamp FROM working_memory${where} ORDER BY timestamp DESC LIMIT 1`,
693
+ );
694
+ const last = lastStatement.get(...params) as { timestamp: string | null } | null;
693
695
  return { total: total.total, count: total.total, last: last?.timestamp ?? null };
694
696
  }
695
697
 
@@ -715,9 +717,10 @@ export function updateWorking(
715
717
  }
716
718
  if (assignments.length === 0) return false;
717
719
  params.push(memoryId, beam.sessionId);
718
- const result = beam.db
719
- .prepare(`UPDATE working_memory SET ${assignments.join(", ")} WHERE id = ? AND session_id = ?`)
720
- .run(...params);
720
+ const result = beam.db.run(
721
+ `UPDATE working_memory SET ${assignments.join(", ")} WHERE id = ? AND session_id = ?`,
722
+ params,
723
+ );
721
724
  if (result.changes > 0) {
722
725
  invalidateCaches(beam);
723
726
  if (content !== null) scheduleEmbedding(beam, [{ memoryId, content }]);
@@ -726,24 +729,22 @@ export function updateWorking(
726
729
  }
727
730
 
728
731
  export function get(beam: BeamMemoryState, memoryId: string): Row | null {
729
- const working = beam.db
730
- .prepare(`
731
- SELECT id, content, source, timestamp, session_id,
732
- importance, metadata_json, veracity, created_at
733
- FROM working_memory
734
- WHERE id = ?
735
- `)
736
- .get(memoryId) as Row | null | undefined;
732
+ using workingStatement = beam.db.prepare(`
733
+ SELECT id, content, source, timestamp, session_id,
734
+ importance, metadata_json, veracity, created_at
735
+ FROM working_memory
736
+ WHERE id = ?
737
+ `);
738
+ const working = workingStatement.get(memoryId) as Row | null | undefined;
737
739
  if (working != null) return { ...working, metadata: working.metadata_json, memory_store: "working" };
738
740
 
739
- const episodic = beam.db
740
- .prepare(`
741
- SELECT id, content, source, timestamp, session_id,
742
- importance, metadata_json, veracity, created_at
743
- FROM episodic_memory
744
- WHERE id = ? AND (session_id = ? OR scope = 'global')
745
- `)
746
- .get(memoryId, beam.sessionId) as Row | null | undefined;
741
+ using episodicStatement = beam.db.prepare(`
742
+ SELECT id, content, source, timestamp, session_id,
743
+ importance, metadata_json, veracity, created_at
744
+ FROM episodic_memory
745
+ WHERE id = ? AND (session_id = ? OR scope = 'global')
746
+ `);
747
+ const episodic = episodicStatement.get(memoryId, beam.sessionId) as Row | null | undefined;
747
748
  if (episodic != null) return { ...episodic, metadata: episodic.metadata_json, memory_store: "episodic" };
748
749
 
749
750
  return getFact(beam, memoryId);
@@ -762,7 +763,8 @@ export function get(beam: BeamMemoryState, memoryId: string): Row | null {
762
763
  * path mutates `facts`.
763
764
  */
764
765
  function getFact(beam: BeamMemoryState, memoryId: string): Row | null {
765
- const fact = beam.db.prepare("SELECT * FROM facts WHERE fact_id = ?").get(memoryId) as Row | null | undefined;
766
+ using statement = beam.db.prepare("SELECT * FROM facts WHERE fact_id = ?");
767
+ const fact = statement.get(memoryId) as Row | null | undefined;
766
768
  if (fact == null) return null;
767
769
  if (fact.session_id !== beam.sessionId && fact.scope !== "global") return null;
768
770
  const subject = typeof fact.subject === "string" ? fact.subject : "";
@@ -789,9 +791,10 @@ function getFact(beam: BeamMemoryState, memoryId: string): Row | null {
789
791
  export function forgetWorking(beam: BeamMemoryState, memoryId: string): boolean {
790
792
  let deleted = 0;
791
793
  transaction(beam.db, () => {
792
- const result = beam.db
793
- .prepare("DELETE FROM working_memory WHERE id = ? AND session_id = ?")
794
- .run(memoryId, beam.sessionId);
794
+ const result = beam.db.run("DELETE FROM working_memory WHERE id = ? AND session_id = ?", [
795
+ memoryId,
796
+ beam.sessionId,
797
+ ]);
795
798
  deleted = result.changes;
796
799
  if (deleted > 0) {
797
800
  purgeWorkingMemoryArtifacts(beam.db, [memoryId]);
@@ -804,36 +807,65 @@ export function forgetWorking(beam: BeamMemoryState, memoryId: string): boolean
804
807
  export function scratchpadWrite(beam: BeamMemoryState, content: string): string {
805
808
  const padId = generateId(content);
806
809
  const timestamp = toUtcIso();
807
- beam.db
808
- .prepare(`
810
+ beam.db.run(
811
+ `
809
812
  INSERT INTO scratchpad (id, content, session_id, created_at, updated_at)
810
813
  VALUES (?, ?, ?, ?, ?)
811
814
  ON CONFLICT(id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at
812
- `)
813
- .run(padId, content, beam.sessionId, timestamp, timestamp);
815
+ `,
816
+ [padId, content, beam.sessionId, timestamp, timestamp],
817
+ );
814
818
  return padId;
815
819
  }
816
820
 
817
821
  export function scratchpadRead(beam: BeamMemoryState): Row[] {
822
+ using statement = beam.db.prepare(`
823
+ SELECT id, content, created_at, updated_at
824
+ FROM scratchpad
825
+ WHERE session_id = ?
826
+ ORDER BY updated_at DESC
827
+ LIMIT ?
828
+ `);
818
829
  return (
819
- beam.db
820
- .prepare(`
821
- SELECT id, content, created_at, updated_at
822
- FROM scratchpad
823
- WHERE session_id = ?
824
- ORDER BY updated_at DESC
825
- LIMIT ?
826
- `)
827
- .all(beam.sessionId, Number.isFinite(SCRATCHPAD_MAX_ITEMS) ? SCRATCHPAD_MAX_ITEMS : 1000) as Row[]
830
+ statement.all(beam.sessionId, Number.isFinite(SCRATCHPAD_MAX_ITEMS) ? SCRATCHPAD_MAX_ITEMS : 1000) as Row[]
828
831
  ).map(rowToDict);
829
832
  }
830
833
 
831
834
  export function scratchpadClear(beam: BeamMemoryState): void {
832
- beam.db.prepare("DELETE FROM scratchpad WHERE session_id = ?").run(beam.sessionId);
835
+ beam.db.run("DELETE FROM scratchpad WHERE session_id = ?", [beam.sessionId]);
833
836
  }
834
837
 
835
838
  export function exportToDict(beam: BeamMemoryState): Record<string, unknown> {
836
839
  const db = beam.db;
840
+ using workingStatement = db.prepare(`
841
+ SELECT id, content, source, timestamp, session_id, importance,
842
+ embed_text,
843
+ metadata_json, valid_until, superseded_by, scope,
844
+ recall_count, last_recalled, created_at, veracity, consolidated_at,
845
+ memory_type, author_id, author_type, channel_id, trust_tier,
846
+ event_date, event_date_precision, temporal_tags
847
+ FROM working_memory
848
+ ORDER BY session_id, timestamp
849
+ `);
850
+ using episodicStatement = db.prepare(`
851
+ SELECT rowid, id, content, source, timestamp, session_id, importance,
852
+ metadata_json, summary_of, valid_until, superseded_by, scope,
853
+ recall_count, last_recalled, created_at, veracity, memory_type,
854
+ author_id, author_type, channel_id, trust_tier,
855
+ event_date, event_date_precision, temporal_tags
856
+ FROM episodic_memory
857
+ ORDER BY session_id, timestamp
858
+ `);
859
+ using scratchpadStatement = db.prepare(`
860
+ SELECT id, content, session_id, created_at, updated_at
861
+ FROM scratchpad
862
+ ORDER BY session_id, updated_at
863
+ `);
864
+ using consolidationStatement = db.prepare(`
865
+ SELECT id, session_id, items_consolidated, summary_preview, created_at
866
+ FROM consolidation_log
867
+ ORDER BY session_id, created_at
868
+ `);
837
869
  return {
838
870
  mnemopi_export: {
839
871
  version: "1.0",
@@ -841,44 +873,11 @@ export function exportToDict(beam: BeamMemoryState): Record<string, unknown> {
841
873
  source_db: beam.dbPath ?? ":memory:",
842
874
  component: "beam",
843
875
  },
844
- working_memory: db
845
- .prepare(`
846
- SELECT id, content, source, timestamp, session_id, importance,
847
- embed_text,
848
- metadata_json, valid_until, superseded_by, scope,
849
- recall_count, last_recalled, created_at, veracity, consolidated_at,
850
- memory_type, author_id, author_type, channel_id, trust_tier,
851
- event_date, event_date_precision, temporal_tags
852
- FROM working_memory
853
- ORDER BY session_id, timestamp
854
- `)
855
- .all(),
856
- episodic_memory: db
857
- .prepare(`
858
- SELECT rowid, id, content, source, timestamp, session_id, importance,
859
- metadata_json, summary_of, valid_until, superseded_by, scope,
860
- recall_count, last_recalled, created_at, veracity, memory_type,
861
- author_id, author_type, channel_id, trust_tier,
862
- event_date, event_date_precision, temporal_tags
863
- FROM episodic_memory
864
- ORDER BY session_id, timestamp
865
- `)
866
- .all(),
876
+ working_memory: workingStatement.all(),
877
+ episodic_memory: episodicStatement.all(),
867
878
  episodic_embeddings: [],
868
- scratchpad: db
869
- .prepare(`
870
- SELECT id, content, session_id, created_at, updated_at
871
- FROM scratchpad
872
- ORDER BY session_id, updated_at
873
- `)
874
- .all(),
875
- consolidation_log: db
876
- .prepare(`
877
- SELECT id, session_id, items_consolidated, summary_preview, created_at
878
- FROM consolidation_log
879
- ORDER BY session_id, created_at
880
- `)
881
- .all(),
879
+ scratchpad: scratchpadStatement.all(),
880
+ consolidation_log: consolidationStatement.all(),
882
881
  };
883
882
  }
884
883
 
@@ -901,50 +900,54 @@ export function importFromDict(beam: BeamMemoryState, data: Record<string, unkno
901
900
  const item = jsonObject(raw);
902
901
  const id = String(item.id ?? "");
903
902
  if (id.length === 0) continue;
904
- const exists = db.prepare("SELECT 1 FROM working_memory WHERE id = ?").get(id) !== null;
903
+ using existsStatement = db.prepare("SELECT 1 FROM working_memory WHERE id = ?");
904
+ const exists = existsStatement.get(id) !== null;
905
905
  if (exists && !force) {
906
906
  stats.working_memory.skipped++;
907
907
  continue;
908
908
  }
909
909
  if (exists) {
910
- db.prepare("DELETE FROM working_memory WHERE id = ?").run(id);
910
+ db.run("DELETE FROM working_memory WHERE id = ?", [id]);
911
911
  purgeWorkingMemoryArtifacts(db, [id]);
912
912
  stats.working_memory.overwritten++;
913
913
  } else {
914
914
  stats.working_memory.inserted++;
915
915
  }
916
- db.prepare(`
916
+ db.run(
917
+ `
917
918
  INSERT INTO working_memory
918
919
  (id, content, source, timestamp, session_id, importance, metadata_json,
919
920
  valid_until, superseded_by, scope, recall_count, last_recalled, created_at,
920
921
  veracity, consolidated_at, memory_type, embed_text, author_id, author_type, channel_id,
921
922
  trust_tier, event_date, event_date_precision, temporal_tags)
922
923
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
923
- `).run(
924
- id,
925
- sqlBinding(item.content, ""),
926
- sqlBinding(item.source, null),
927
- sqlBinding(item.timestamp, null),
928
- sqlBinding(item.session_id, "default"),
929
- sqlBinding(item.importance, 0.5),
930
- sqlBinding(item.metadata_json, "{}"),
931
- sqlBinding(item.valid_until, null),
932
- sqlBinding(item.superseded_by, null),
933
- sqlBinding(item.scope, "session"),
934
- sqlBinding(item.recall_count, 0),
935
- sqlBinding(item.last_recalled, null),
936
- sqlBinding(item.created_at, null),
937
- clampVeracity(item.veracity),
938
- item.consolidated_at == null ? importedAt : sqlBinding(item.consolidated_at, importedAt),
939
- sqlBinding(item.memory_type, "unknown"),
940
- sqlBinding(item.embed_text, null),
941
- sqlBinding(item.author_id, null),
942
- sqlBinding(item.author_type, null),
943
- sqlBinding(item.channel_id, null),
944
- sqlBinding(item.trust_tier, "STATED"),
945
- sqlBinding(item.event_date, null),
946
- sqlBinding(item.event_date_precision, "unknown"),
947
- sqlBinding(item.temporal_tags, "[]"),
924
+ `,
925
+ [
926
+ id,
927
+ sqlBinding(item.content, ""),
928
+ sqlBinding(item.source, null),
929
+ sqlBinding(item.timestamp, null),
930
+ sqlBinding(item.session_id, "default"),
931
+ sqlBinding(item.importance, 0.5),
932
+ sqlBinding(item.metadata_json, "{}"),
933
+ sqlBinding(item.valid_until, null),
934
+ sqlBinding(item.superseded_by, null),
935
+ sqlBinding(item.scope, "session"),
936
+ sqlBinding(item.recall_count, 0),
937
+ sqlBinding(item.last_recalled, null),
938
+ sqlBinding(item.created_at, null),
939
+ clampVeracity(item.veracity),
940
+ item.consolidated_at == null ? importedAt : sqlBinding(item.consolidated_at, importedAt),
941
+ sqlBinding(item.memory_type, "unknown"),
942
+ sqlBinding(item.embed_text, null),
943
+ sqlBinding(item.author_id, null),
944
+ sqlBinding(item.author_type, null),
945
+ sqlBinding(item.channel_id, null),
946
+ sqlBinding(item.trust_tier, "STATED"),
947
+ sqlBinding(item.event_date, null),
948
+ sqlBinding(item.event_date_precision, "unknown"),
949
+ sqlBinding(item.temporal_tags, "[]"),
950
+ ],
948
951
  );
949
952
  }
950
953
 
@@ -952,61 +955,66 @@ export function importFromDict(beam: BeamMemoryState, data: Record<string, unkno
952
955
  const item = jsonObject(raw);
953
956
  const id = String(item.id ?? "");
954
957
  if (id.length === 0) continue;
955
- const exists = db.prepare("SELECT 1 FROM episodic_memory WHERE id = ?").get(id) !== null;
958
+ using existsStatement = db.prepare("SELECT 1 FROM episodic_memory WHERE id = ?");
959
+ const exists = existsStatement.get(id) !== null;
956
960
  if (exists && !force) {
957
961
  stats.episodic_memory.skipped++;
958
962
  continue;
959
963
  }
964
+ using rowidStatement = db.prepare("SELECT rowid FROM episodic_memory WHERE id = ?");
960
965
  if (exists) {
961
- const existingRow = db.prepare("SELECT rowid FROM episodic_memory WHERE id = ?").get(id) as {
966
+ const existingRow = rowidStatement.get(id) as {
962
967
  rowid: number;
963
968
  } | null;
964
969
  if (existingRow !== null && vecAvailable(db)) {
965
970
  try {
966
- db.prepare("DELETE FROM vec_episodes WHERE rowid = ?").run(existingRow.rowid);
971
+ db.run("DELETE FROM vec_episodes WHERE rowid = ?", [existingRow.rowid]);
967
972
  } catch {
968
973
  // sqlite-vec cleanup is best-effort; import correctness takes precedence.
969
974
  }
970
975
  }
971
- db.prepare("DELETE FROM episodic_memory WHERE id = ?").run(id);
976
+ db.run("DELETE FROM episodic_memory WHERE id = ?", [id]);
972
977
  stats.episodic_memory.overwritten++;
973
978
  } else {
974
979
  stats.episodic_memory.inserted++;
975
980
  }
976
- db.prepare(`
981
+ db.run(
982
+ `
977
983
  INSERT INTO episodic_memory
978
984
  (id, content, source, timestamp, session_id, importance, metadata_json,
979
985
  summary_of, valid_until, superseded_by, scope, recall_count, last_recalled, created_at,
980
986
  veracity, memory_type, author_id, author_type, channel_id, trust_tier,
981
987
  event_date, event_date_precision, temporal_tags)
982
988
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
983
- `).run(
984
- id,
985
- sqlBinding(item.content, ""),
986
- sqlBinding(item.source, null),
987
- sqlBinding(item.timestamp, null),
988
- sqlBinding(item.session_id, "default"),
989
- sqlBinding(item.importance, 0.5),
990
- sqlBinding(item.metadata_json, "{}"),
991
- sqlBinding(item.summary_of, ""),
992
- sqlBinding(item.valid_until, null),
993
- sqlBinding(item.superseded_by, null),
994
- sqlBinding(item.scope, "session"),
995
- sqlBinding(item.recall_count, 0),
996
- sqlBinding(item.last_recalled, null),
997
- sqlBinding(item.created_at, null),
998
- clampVeracity(item.veracity),
999
- sqlBinding(item.memory_type, "unknown"),
1000
- sqlBinding(item.author_id, null),
1001
- sqlBinding(item.author_type, null),
1002
- sqlBinding(item.channel_id, null),
1003
- sqlBinding(item.trust_tier, "STATED"),
1004
- sqlBinding(item.event_date, null),
1005
- sqlBinding(item.event_date_precision, "unknown"),
1006
- sqlBinding(item.temporal_tags, "[]"),
989
+ `,
990
+ [
991
+ id,
992
+ sqlBinding(item.content, ""),
993
+ sqlBinding(item.source, null),
994
+ sqlBinding(item.timestamp, null),
995
+ sqlBinding(item.session_id, "default"),
996
+ sqlBinding(item.importance, 0.5),
997
+ sqlBinding(item.metadata_json, "{}"),
998
+ sqlBinding(item.summary_of, ""),
999
+ sqlBinding(item.valid_until, null),
1000
+ sqlBinding(item.superseded_by, null),
1001
+ sqlBinding(item.scope, "session"),
1002
+ sqlBinding(item.recall_count, 0),
1003
+ sqlBinding(item.last_recalled, null),
1004
+ sqlBinding(item.created_at, null),
1005
+ clampVeracity(item.veracity),
1006
+ sqlBinding(item.memory_type, "unknown"),
1007
+ sqlBinding(item.author_id, null),
1008
+ sqlBinding(item.author_type, null),
1009
+ sqlBinding(item.channel_id, null),
1010
+ sqlBinding(item.trust_tier, "STATED"),
1011
+ sqlBinding(item.event_date, null),
1012
+ sqlBinding(item.event_date_precision, "unknown"),
1013
+ sqlBinding(item.temporal_tags, "[]"),
1014
+ ],
1007
1015
  );
1008
1016
  const oldRowid = Number(item.rowid);
1009
- const newRow = db.prepare("SELECT rowid FROM episodic_memory WHERE id = ?").get(id) as {
1017
+ const newRow = rowidStatement.get(id) as {
1010
1018
  rowid: number;
1011
1019
  } | null;
1012
1020
  if (Number.isFinite(oldRowid) && newRow !== null) oldToNewRowid.set(oldRowid, newRow.rowid);
@@ -1033,41 +1041,39 @@ export function importFromDict(beam: BeamMemoryState, data: Record<string, unkno
1033
1041
  const item = jsonObject(raw);
1034
1042
  const id = String(item.id ?? "");
1035
1043
  if (id.length === 0) continue;
1036
- const exists = db.prepare("SELECT 1 FROM scratchpad WHERE id = ?").get(id) !== null;
1044
+ using existsStatement = db.prepare("SELECT 1 FROM scratchpad WHERE id = ?");
1045
+ const exists = existsStatement.get(id) !== null;
1037
1046
  if (exists) {
1038
- db.prepare(
1039
- "UPDATE scratchpad SET content = ?, session_id = ?, created_at = ?, updated_at = ? WHERE id = ?",
1040
- ).run(
1047
+ db.run("UPDATE scratchpad SET content = ?, session_id = ?, created_at = ?, updated_at = ? WHERE id = ?", [
1041
1048
  sqlBinding(item.content, ""),
1042
1049
  sqlBinding(item.session_id, "default"),
1043
1050
  sqlBinding(item.created_at, null),
1044
1051
  sqlBinding(item.updated_at, null),
1045
1052
  id,
1046
- );
1053
+ ]);
1047
1054
  stats.scratchpad.updated++;
1048
1055
  } else {
1049
- db.prepare(
1050
- "INSERT INTO scratchpad (id, content, session_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
1051
- ).run(
1056
+ db.run("INSERT INTO scratchpad (id, content, session_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", [
1052
1057
  id,
1053
1058
  sqlBinding(item.content, ""),
1054
1059
  sqlBinding(item.session_id, "default"),
1055
1060
  sqlBinding(item.created_at, null),
1056
1061
  sqlBinding(item.updated_at, null),
1057
- );
1062
+ ]);
1058
1063
  stats.scratchpad.inserted++;
1059
1064
  }
1060
1065
  }
1061
1066
 
1062
1067
  for (const raw of Array.isArray(data.consolidation_log) ? data.consolidation_log : []) {
1063
1068
  const item = jsonObject(raw);
1064
- db.prepare(
1069
+ db.run(
1065
1070
  "INSERT INTO consolidation_log (session_id, items_consolidated, summary_preview, created_at) VALUES (?, ?, ?, ?)",
1066
- ).run(
1067
- sqlBinding(item.session_id, "default"),
1068
- sqlBinding(item.items_consolidated, 0),
1069
- sqlBinding(item.summary_preview, ""),
1070
- sqlBinding(item.created_at, null),
1071
+ [
1072
+ sqlBinding(item.session_id, "default"),
1073
+ sqlBinding(item.items_consolidated, 0),
1074
+ sqlBinding(item.summary_preview, ""),
1075
+ sqlBinding(item.created_at, null),
1076
+ ],
1071
1077
  );
1072
1078
  stats.consolidation_log.inserted++;
1073
1079
  }
@@ -329,7 +329,8 @@ function toRecallOptions(options: RecallFacadeOptions): BeamRecallFacadeOptions
329
329
  }
330
330
 
331
331
  function countRows(db: Database, sql: string, ...params: (string | number | null)[]): number {
332
- const row = db.prepare(sql).get(...params) as { total?: number; count?: number } | null;
332
+ using statement = db.prepare(sql);
333
+ const row = statement.get(...params) as { total?: number; count?: number } | null;
333
334
  return row?.total ?? row?.count ?? 0;
334
335
  }
335
336
 
@@ -344,9 +345,8 @@ function dataDirForDbPath(path: string): string | undefined {
344
345
 
345
346
  function sourceCounts(db: Database): Record<string, number> {
346
347
  const counts: Record<string, number> = {};
347
- for (const row of db
348
- .prepare("SELECT source, COUNT(*) AS total FROM working_memory GROUP BY source")
349
- .all() as Row[]) {
348
+ using statement = db.prepare("SELECT source, COUNT(*) AS total FROM working_memory GROUP BY source");
349
+ for (const row of statement.all() as Row[]) {
350
350
  counts[String(row.source ?? "") || "conversation"] = Number(row.total ?? 0);
351
351
  }
352
352
  return counts;
@@ -482,7 +482,8 @@ export class Mnemopi {
482
482
  const episodic = this.#withRuntimeOptions(() => this.beam.getEpisodicStats(authorId, authorType, channelId));
483
483
  const totalMemories = countRows(this.conn, "SELECT COUNT(*) AS total FROM working_memory");
484
484
  const totalSessions = countRows(this.conn, "SELECT COUNT(DISTINCT session_id) AS total FROM working_memory");
485
- const last = this.conn.prepare("SELECT timestamp FROM working_memory ORDER BY timestamp DESC LIMIT 1").get() as {
485
+ using lastStatement = this.conn.prepare("SELECT timestamp FROM working_memory ORDER BY timestamp DESC LIMIT 1");
486
+ const last = lastStatement.get() as {
486
487
  timestamp: string | null;
487
488
  } | null;
488
489
  const tripleTotal = countRows(this.conn, "SELECT COUNT(*) AS total FROM triples");
@@ -121,7 +121,7 @@ function kindCounts(rows: readonly TripleCandidateRow[]): Record<string, number>
121
121
 
122
122
  function migrateRows(db: Database, rows: readonly TripleCandidateRow[]): number {
123
123
  if (rows.length === 0) return 0;
124
- const insert = db.prepare(`
124
+ using insert = db.prepare(`
125
125
  INSERT OR IGNORE INTO annotations (memory_id, kind, value, source, confidence, created_at)
126
126
  VALUES (?, ?, ?, ?, ?, ?)
127
127
  `);