@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 +6 -0
- package/package.json +5 -5
- package/src/core/annotations.ts +25 -26
- package/src/core/beam/consolidate.ts +2 -2
- package/src/core/beam/helpers.ts +1 -1
- package/src/core/beam/store.ts +231 -225
- package/src/core/memory.ts +6 -5
- package/src/core/migrations/e6-triplestore-split.ts +1 -1
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.
|
|
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.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
44
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
45
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
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": {
|
package/src/core/annotations.ts
CHANGED
|
@@ -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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
832
|
-
beam.db.
|
|
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> {
|
package/src/core/beam/helpers.ts
CHANGED
|
@@ -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
|
-
|
|
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[]) => {
|
package/src/core/beam/store.ts
CHANGED
|
@@ -157,18 +157,16 @@ function invalidateCaches(beam: BeamMemoryState): void {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function findDuplicate(beam: BeamMemoryState, content: string): string | null {
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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.
|
|
203
|
+
db.run(`DELETE FROM facts WHERE source_msg_id IN (${placeholders})`, [...ids]);
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
db.
|
|
208
|
-
db.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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.
|
|
400
|
-
beam.db.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
+
`,
|
|
652
|
+
[now, replacementId, memoryId, beam.sessionId],
|
|
653
|
+
);
|
|
655
654
|
if (working.changes > 0) return true;
|
|
656
|
-
const episodic = beam.db
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
720
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
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
|
-
|
|
794
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
845
|
-
|
|
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:
|
|
869
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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 =
|
|
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
|
-
|
|
1044
|
+
using existsStatement = db.prepare("SELECT 1 FROM scratchpad WHERE id = ?");
|
|
1045
|
+
const exists = existsStatement.get(id) !== null;
|
|
1037
1046
|
if (exists) {
|
|
1038
|
-
db.
|
|
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.
|
|
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.
|
|
1069
|
+
db.run(
|
|
1065
1070
|
"INSERT INTO consolidation_log (session_id, items_consolidated, summary_preview, created_at) VALUES (?, ?, ?, ?)",
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
}
|
package/src/core/memory.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
`);
|