@membank/core 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import BetterSqlite3 from "better-sqlite3";
@@ -8,6 +8,20 @@ import { pipeline } from "@huggingface/transformers";
8
8
  import { createHash, randomUUID } from "node:crypto";
9
9
  import { execFile } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
+ //#region src/config/loader.ts
12
+ function loadConfig() {
13
+ const configPath = join(homedir(), ".membank", "config.json");
14
+ try {
15
+ const raw = readFileSync(configPath, "utf8");
16
+ return JSON.parse(raw);
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function isSynthesisEnabled() {
22
+ return loadConfig()?.synthesis?.enabled === true;
23
+ }
24
+ //#endregion
11
25
  //#region src/db/errors.ts
12
26
  var MembankError = class extends Error {
13
27
  constructor(message, options) {
@@ -24,7 +38,8 @@ var DatabaseError = class extends MembankError {
24
38
  //#endregion
25
39
  //#region src/db/manager.ts
26
40
  const DEFAULT_DB_PATH = join(homedir(), ".membank", "memory.db");
27
- const MIGRATIONS$1 = [[1, `
41
+ const MIGRATIONS$1 = [
42
+ [1, `
28
43
  CREATE TABLE IF NOT EXISTS memories (
29
44
  id TEXT PRIMARY KEY,
30
45
  content TEXT NOT NULL,
@@ -42,7 +57,8 @@ CREATE TABLE IF NOT EXISTS memories (
42
57
  CREATE VIRTUAL TABLE IF NOT EXISTS embeddings USING vec0(
43
58
  embedding FLOAT[384]
44
59
  );
45
- `], [2, `
60
+ `],
61
+ [2, `
46
62
  CREATE TABLE IF NOT EXISTS projects (
47
63
  id TEXT PRIMARY KEY,
48
64
  name TEXT NOT NULL,
@@ -75,7 +91,46 @@ JOIN projects p ON p.scope_hash = m.scope
75
91
  WHERE m.scope != 'global';
76
92
 
77
93
  ALTER TABLE memories DROP COLUMN scope;
78
- `]];
94
+ `],
95
+ [3, `
96
+ CREATE TABLE IF NOT EXISTS memory_review_events (
97
+ id TEXT PRIMARY KEY,
98
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
99
+ conflicting_memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
100
+ similarity REAL NOT NULL,
101
+ conflict_content_snapshot TEXT NOT NULL,
102
+ reason TEXT NOT NULL,
103
+ created_at TEXT NOT NULL,
104
+ resolved_at TEXT
105
+ );
106
+
107
+ CREATE INDEX IF NOT EXISTS idx_review_events_memory_open
108
+ ON memory_review_events(memory_id) WHERE resolved_at IS NULL;
109
+
110
+ ALTER TABLE memories DROP COLUMN needs_review;
111
+ `],
112
+ [4, `
113
+ CREATE TABLE IF NOT EXISTS syntheses (
114
+ id TEXT PRIMARY KEY,
115
+ scope TEXT NOT NULL,
116
+ content TEXT NOT NULL,
117
+ source_memory_hash TEXT NOT NULL,
118
+ synthesized_at TEXT NOT NULL,
119
+ expires_at TEXT NOT NULL,
120
+ in_flight_since TEXT,
121
+ created_at TEXT NOT NULL,
122
+ updated_at TEXT NOT NULL,
123
+ UNIQUE(scope),
124
+ CHECK(expires_at > synthesized_at)
125
+ );
126
+
127
+ CREATE INDEX IF NOT EXISTS idx_syntheses_expires_at
128
+ ON syntheses(expires_at);
129
+
130
+ CREATE INDEX IF NOT EXISTS idx_syntheses_scope_inflight
131
+ ON syntheses(scope) WHERE in_flight_since IS NOT NULL;
132
+ `]
133
+ ];
79
134
  var DatabaseManager = class DatabaseManager {
80
135
  #db;
81
136
  constructor(db) {
@@ -149,6 +204,27 @@ const ProjectSchema = z.object({
149
204
  createdAt: z.string(),
150
205
  updatedAt: z.string()
151
206
  });
207
+ const ReviewReasonSchema = z.enum(["similarity_dedup"]);
208
+ const ReviewEventSchema = z.object({
209
+ id: z.string(),
210
+ memoryId: z.string(),
211
+ conflictingMemoryId: z.string().nullable(),
212
+ similarity: z.number(),
213
+ conflictContentSnapshot: z.string(),
214
+ reason: ReviewReasonSchema,
215
+ createdAt: z.string(),
216
+ resolvedAt: z.string().nullable()
217
+ });
218
+ const ReviewEventRowSchema = z.object({
219
+ id: z.string(),
220
+ memory_id: z.string(),
221
+ conflicting_memory_id: z.string().nullable(),
222
+ similarity: z.number(),
223
+ conflict_content_snapshot: z.string(),
224
+ reason: ReviewReasonSchema,
225
+ created_at: z.string(),
226
+ resolved_at: z.string().nullable()
227
+ });
152
228
  const MemorySchema = z.object({
153
229
  id: z.string(),
154
230
  content: z.string(),
@@ -158,7 +234,7 @@ const MemorySchema = z.object({
158
234
  sourceHarness: z.string().nullable(),
159
235
  accessCount: z.number().int().nonnegative(),
160
236
  pinned: z.boolean(),
161
- needsReview: z.boolean(),
237
+ reviewEvents: z.array(ReviewEventSchema),
162
238
  createdAt: z.string(),
163
239
  updatedAt: z.string()
164
240
  });
@@ -181,12 +257,25 @@ const SaveOptionsSchema = z.object({
181
257
  });
182
258
  const MemoryPatchSchema = z.object({
183
259
  content: z.string().min(1).optional(),
184
- tags: z.array(z.string()).optional()
260
+ tags: z.array(z.string()).optional(),
261
+ type: MemoryTypeSchema.optional()
262
+ });
263
+ const SynthesisSchema = z.object({
264
+ id: z.string(),
265
+ scope: z.string(),
266
+ content: z.string(),
267
+ sourceMemoryHash: z.string(),
268
+ synthesizedAt: z.string(),
269
+ expiresAt: z.string(),
270
+ inFlightSince: z.string().nullable(),
271
+ createdAt: z.string(),
272
+ updatedAt: z.string()
185
273
  });
186
274
  const SessionContextSchema = z.object({
187
275
  stats: z.record(MemoryTypeSchema, z.number()),
188
276
  pinnedGlobal: z.array(MemorySchema),
189
- pinnedProject: z.array(MemorySchema)
277
+ pinnedProject: z.array(MemorySchema),
278
+ synthesis: z.string().optional()
190
279
  });
191
280
  const MemoryRowSchema = z.object({
192
281
  id: z.string(),
@@ -196,7 +285,6 @@ const MemoryRowSchema = z.object({
196
285
  source: z.string().nullable(),
197
286
  access_count: z.number(),
198
287
  pinned: z.number(),
199
- needs_review: z.number(),
200
288
  created_at: z.string(),
201
289
  updated_at: z.string()
202
290
  });
@@ -209,7 +297,7 @@ const ProjectRowSchema = z.object({
209
297
  });
210
298
  //#endregion
211
299
  //#region src/db/row-types.ts
212
- function rowToMemory(row, projects) {
300
+ function rowToMemory(row, projects, reviewEvents = []) {
213
301
  return {
214
302
  id: row.id,
215
303
  content: row.content,
@@ -219,11 +307,24 @@ function rowToMemory(row, projects) {
219
307
  sourceHarness: row.source,
220
308
  accessCount: row.access_count,
221
309
  pinned: row.pinned !== 0,
222
- needsReview: row.needs_review !== 0,
310
+ reviewEvents,
223
311
  createdAt: row.created_at,
224
312
  updatedAt: row.updated_at
225
313
  };
226
314
  }
315
+ function rowToReviewEvent(row) {
316
+ const parsed = ReviewEventRowSchema.parse(row);
317
+ return {
318
+ id: parsed.id,
319
+ memoryId: parsed.memory_id,
320
+ conflictingMemoryId: parsed.conflicting_memory_id,
321
+ similarity: parsed.similarity,
322
+ conflictContentSnapshot: parsed.conflict_content_snapshot,
323
+ reason: parsed.reason,
324
+ createdAt: parsed.created_at,
325
+ resolvedAt: parsed.resolved_at
326
+ };
327
+ }
227
328
  function rowToProject(row) {
228
329
  return {
229
330
  id: row.id,
@@ -260,6 +361,7 @@ var EmbeddingService = class {
260
361
  };
261
362
  //#endregion
262
363
  //#region src/memory/repository.ts
364
+ const PIN_BUDGET_THRESHOLD = 8e3;
263
365
  var MemoryRepository = class {
264
366
  #db;
265
367
  #embedding;
@@ -289,21 +391,26 @@ var MemoryRepository = class {
289
391
  if (top !== void 0 && top.similarity > .92) {
290
392
  this.#db.db.prepare(`UPDATE memories SET content = ?, updated_at = ? WHERE id = ?`).run(content, now, top.id);
291
393
  this.#db.db.prepare(`UPDATE embeddings SET embedding = ? WHERE rowid = ?`).run(embeddingBlob, top.rowid);
292
- return rowToMemory(MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(top.id)), this.#projects.getProjectsForMemories([top.id]).get(top.id) ?? []);
394
+ const updated = MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(top.id));
395
+ const projectMap = this.#projects.getProjectsForMemories([top.id]);
396
+ const events = this.#getEventsForMemories([top.id]);
397
+ return rowToMemory(updated, projectMap.get(top.id) ?? [], events.get(top.id) ?? []);
293
398
  }
294
- if (top !== void 0 && top.similarity >= .75) this.#db.db.prepare(`UPDATE memories SET needs_review = 1 WHERE id = ?`).run(top.id);
295
399
  const id = randomUUID();
296
- this.#db.db.prepare(`INSERT INTO memories (id, content, type, tags, source, access_count, pinned, needs_review, created_at, updated_at)
297
- VALUES (?, ?, ?, ?, ?, 0, 0, 0, ?, ?)`).run(id, content, type, JSON.stringify(tags), sourceHarness ?? null, now, now);
400
+ this.#db.db.prepare(`INSERT INTO memories (id, content, type, tags, source, access_count, pinned, created_at, updated_at)
401
+ VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)`).run(id, content, type, JSON.stringify(tags), sourceHarness ?? null, now, now);
402
+ if (top !== void 0 && top.similarity >= .75) this.#db.db.prepare(`INSERT INTO memory_review_events
403
+ (id, memory_id, conflicting_memory_id, similarity, conflict_content_snapshot, reason, created_at)
404
+ VALUES (?, ?, ?, ?, ?, 'similarity_dedup', ?)`).run(randomUUID(), top.id, id, top.similarity, content, now);
298
405
  this.#db.db.prepare(`INSERT INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`).run(embeddingBlob, id);
299
406
  if (projectScope !== void 0) {
300
407
  const project = this.#projects.upsertByHash(projectScope.hash, projectScope.name);
301
408
  this.#projects.addAssociation(id, project.id);
302
409
  }
303
- return rowToMemory(MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id)), this.#projects.getProjectsForMemories([id]).get(id) ?? []);
410
+ return rowToMemory(MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id)), this.#projects.getProjectsForMemories([id]).get(id) ?? [], []);
304
411
  }
305
412
  async update(id, patch) {
306
- const { content, tags } = MemoryPatchSchema.parse(patch);
413
+ const { content, tags, type } = MemoryPatchSchema.parse(patch);
307
414
  const existing = this.#db.db.prepare(`SELECT m.rowid, m.* FROM memories m WHERE m.id = ?`).get(id);
308
415
  if (existing === void 0) throw new Error(`Memory not found: ${id}`);
309
416
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -317,6 +424,10 @@ var MemoryRepository = class {
317
424
  sets.push("tags = ?");
318
425
  values.push(JSON.stringify(tags));
319
426
  }
427
+ if (type !== void 0) {
428
+ sets.push("type = ?");
429
+ values.push(type);
430
+ }
320
431
  values.push(id);
321
432
  this.#db.db.prepare(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`).run(...values);
322
433
  if (content !== void 0) {
@@ -324,7 +435,10 @@ var MemoryRepository = class {
324
435
  const embeddingBlob = Buffer.from(embedding.buffer);
325
436
  this.#db.db.prepare(`UPDATE embeddings SET embedding = ? WHERE rowid = ?`).run(embeddingBlob, existing.rowid);
326
437
  }
327
- return rowToMemory(MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id)), this.#projects.getProjectsForMemories([id]).get(id) ?? []);
438
+ const updated = MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id));
439
+ const projectMap = this.#projects.getProjectsForMemories([id]);
440
+ const events = this.#getEventsForMemories([id]);
441
+ return rowToMemory(updated, projectMap.get(id) ?? [], events.get(id) ?? []);
328
442
  }
329
443
  delete(id) {
330
444
  const row = this.#db.db.prepare(`SELECT rowid FROM memories WHERE id = ?`).get(id);
@@ -346,7 +460,48 @@ var MemoryRepository = class {
346
460
  if (rows.length === 0) return [];
347
461
  const ids = rows.map((r) => r.id);
348
462
  const projectMap = this.#projects.getProjectsForMemories(ids);
349
- return rows.map((row) => rowToMemory(row, projectMap.get(row.id) ?? []));
463
+ const eventMap = this.#getEventsForMemories(ids);
464
+ return rows.map((row) => rowToMemory(row, projectMap.get(row.id) ?? [], eventMap.get(row.id) ?? []));
465
+ }
466
+ listFlagged() {
467
+ const rows = this.#db.db.prepare(`SELECT * FROM memories
468
+ WHERE EXISTS (
469
+ SELECT 1 FROM memory_review_events e
470
+ WHERE e.memory_id = memories.id AND e.resolved_at IS NULL
471
+ )
472
+ ORDER BY created_at DESC`).all();
473
+ if (rows.length === 0) return [];
474
+ const ids = rows.map((r) => r.id);
475
+ const projectMap = this.#projects.getProjectsForMemories(ids);
476
+ const eventMap = this.#getEventsForMemories(ids, { unresolvedOnly: true });
477
+ return rows.map((row) => rowToMemory(row, projectMap.get(row.id) ?? [], eventMap.get(row.id) ?? []));
478
+ }
479
+ listReviewEvents(memoryId, opts) {
480
+ const where = opts?.unresolvedOnly === true ? "WHERE memory_id = ? AND resolved_at IS NULL" : "WHERE memory_id = ?";
481
+ return this.#db.db.prepare(`SELECT * FROM memory_review_events ${where} ORDER BY created_at DESC`).all(memoryId).map((r) => rowToReviewEvent(ReviewEventRowSchema.parse(r)));
482
+ }
483
+ resolveReviewEvents(memoryId) {
484
+ const now = (/* @__PURE__ */ new Date()).toISOString();
485
+ this.#db.db.prepare(`UPDATE memory_review_events SET resolved_at = ? WHERE memory_id = ? AND resolved_at IS NULL`).run(now, memoryId);
486
+ }
487
+ #getEventsForMemories(ids, opts) {
488
+ if (ids.length === 0) return /* @__PURE__ */ new Map();
489
+ const placeholders = ids.map(() => "?").join(", ");
490
+ const unresolvedClause = opts?.unresolvedOnly === true ? "AND resolved_at IS NULL" : "";
491
+ const rows = this.#db.db.prepare(`SELECT * FROM memory_review_events
492
+ WHERE memory_id IN (${placeholders}) ${unresolvedClause}
493
+ ORDER BY created_at DESC`).all(...ids);
494
+ const map = /* @__PURE__ */ new Map();
495
+ for (const row of rows) {
496
+ const event = rowToReviewEvent(ReviewEventRowSchema.parse(row));
497
+ const existing = map.get(event.memoryId) ?? [];
498
+ existing.push(event);
499
+ map.set(event.memoryId, existing);
500
+ }
501
+ return map;
502
+ }
503
+ getPinnedCharCount() {
504
+ return (this.#db.db.prepare(`SELECT COALESCE(SUM(LENGTH(content)), 0) as total FROM memories WHERE pinned = 1`).get() ?? { total: 0 }).total;
350
505
  }
351
506
  stats() {
352
507
  const byType = Object.fromEntries(MEMORY_TYPE_VALUES.map((t) => [t, 0]));
@@ -355,21 +510,27 @@ var MemoryRepository = class {
355
510
  const parsed = MemoryTypeSchema.safeParse(row.type);
356
511
  if (parsed.success) byType[parsed.data] = row.count;
357
512
  }
358
- const totals = this.#db.db.prepare(`SELECT COUNT(*) as total, SUM(needs_review) as needsReview FROM memories`).get() ?? {
513
+ const aggregates = this.#db.db.prepare(`SELECT COUNT(*) as total, SUM(pinned) as pinned FROM memories`).get() ?? {
359
514
  total: 0,
360
- needsReview: 0
515
+ pinned: 0
361
516
  };
517
+ const reviewRow = this.#db.db.prepare(`SELECT COUNT(DISTINCT memory_id) as needsReview FROM memory_review_events WHERE resolved_at IS NULL`).get() ?? { needsReview: 0 };
362
518
  return {
363
519
  byType,
364
- total: totals.total,
365
- needsReview: totals.needsReview ?? 0
520
+ total: aggregates.total,
521
+ pinned: aggregates.pinned ?? 0,
522
+ needsReview: reviewRow.needsReview,
523
+ pinBudgetChars: this.getPinnedCharCount()
366
524
  };
367
525
  }
368
526
  setPin(id, pinned) {
369
527
  if (this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id) === void 0) throw new Error(`Memory not found: ${id}`);
370
528
  const now = (/* @__PURE__ */ new Date()).toISOString();
371
529
  this.#db.db.prepare(`UPDATE memories SET pinned = ?, updated_at = ? WHERE id = ?`).run(pinned ? 1 : 0, now, id);
372
- return rowToMemory(MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id)), this.#projects.getProjectsForMemories([id]).get(id) ?? []);
530
+ const updated = MemoryRowSchema.parse(this.#db.db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id));
531
+ const projectMap = this.#projects.getProjectsForMemories([id]);
532
+ const events = this.#getEventsForMemories([id]);
533
+ return rowToMemory(updated, projectMap.get(id) ?? [], events.get(id) ?? []);
373
534
  }
374
535
  incrementAccessCount(id) {
375
536
  this.#db.db.prepare(`UPDATE memories SET access_count = access_count + 1 WHERE id = ?`).run(id);
@@ -533,7 +694,7 @@ var QueryEngine = class {
533
694
  const now = Date.now();
534
695
  const scored = rows.filter((row) => row.cosine_sim > 0).map((row) => {
535
696
  const memory = rowToMemory(row, []);
536
- const score = this.#computeScore(memory, now);
697
+ const score = this.#computeScore(memory, row.cosine_sim, now);
537
698
  return {
538
699
  ...memory,
539
700
  score
@@ -544,12 +705,12 @@ var QueryEngine = class {
544
705
  for (const result of results) this.#repo.incrementAccessCount(result.id);
545
706
  return results;
546
707
  }
547
- #computeScore(memory, now) {
708
+ #computeScore(memory, cosine_sim, now) {
548
709
  const typeWeight = TYPE_WEIGHTS[memory.type];
549
710
  const accessCountNorm = memory.accessCount / (memory.accessCount + 10);
550
711
  const recencyNorm = 1 / (1 + (now - new Date(memory.updatedAt).getTime()) / 864e5);
551
712
  const pinned = memory.pinned ? 1 : 0;
552
- return typeWeight * .4 + accessCountNorm * .3 + recencyNorm * .2 + pinned * .1;
713
+ return cosine_sim * .4 + typeWeight * .25 + accessCountNorm * .2 + recencyNorm * .1 + pinned * .05;
553
714
  }
554
715
  };
555
716
  //#endregion
@@ -562,28 +723,139 @@ var SessionContextBuilder = class {
562
723
  constructor(db) {
563
724
  this.#db = db;
564
725
  }
565
- getSessionContext(projectHash) {
566
- const pinnedGlobal = this.#db.db.prepare(`SELECT * FROM memories
567
- WHERE id NOT IN (SELECT memory_id FROM memory_projects)
568
- AND pinned = 1`).all().map((row) => rowToMemory(row, []));
569
- const pinnedProject = this.#db.db.prepare(`SELECT m.* FROM memories m
570
- JOIN memory_projects mp ON mp.memory_id = m.id
571
- JOIN projects p ON p.id = mp.project_id
572
- WHERE p.scope_hash = ? AND m.pinned = 1`).all(projectHash).map((row) => rowToMemory(row, []));
726
+ getSessionContext(projectHash, synthesis) {
573
727
  const typeCounts = this.#db.db.prepare("SELECT type, COUNT(*) as count FROM memories GROUP BY type").all();
574
728
  const stats = Object.fromEntries(MEMORY_TYPE_VALUES.map((t) => [t, 0]));
575
729
  for (const row of typeCounts) {
576
730
  const parsed = MemoryTypeSchema.safeParse(row.type);
577
731
  if (parsed.success) stats[parsed.data] = row.count;
578
732
  }
733
+ if (synthesis !== void 0 && synthesis.length > 0) return {
734
+ stats,
735
+ pinnedGlobal: [],
736
+ pinnedProject: [],
737
+ synthesis
738
+ };
579
739
  return {
580
740
  stats,
581
- pinnedGlobal,
582
- pinnedProject
741
+ pinnedGlobal: this.#db.db.prepare(`SELECT * FROM memories
742
+ WHERE id NOT IN (SELECT memory_id FROM memory_projects)
743
+ AND pinned = 1`).all().map((row) => rowToMemory(row, [])),
744
+ pinnedProject: this.#db.db.prepare(`SELECT m.* FROM memories m
745
+ JOIN memory_projects mp ON mp.memory_id = m.id
746
+ JOIN projects p ON p.id = mp.project_id
747
+ WHERE p.scope_hash = ? AND m.pinned = 1`).all(projectHash).map((row) => rowToMemory(row, []))
583
748
  };
584
749
  }
585
750
  };
586
751
  //#endregion
587
- export { DatabaseError, DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MIGRATIONS, MembankError, MemoryPatchSchema, MemoryRepository, MemoryRowSchema, MemorySchema, MemoryTypeSchema, ProjectRepository, ProjectRowSchema, ProjectSchema, QueryEngine, QueryOptionsSchema, SaveOptionsSchema, SessionContextBuilder, SessionContextSchema, TagsJsonSchema, listMemoryTypes, resolveProject, resolveScope, rowToMemory, rowToProject, runScopeToProjectsMigration };
752
+ //#region src/synthesis/repository.ts
753
+ function rowToSynthesis(row) {
754
+ return SynthesisSchema.parse({
755
+ id: row.id,
756
+ scope: row.scope,
757
+ content: row.content,
758
+ sourceMemoryHash: row.source_memory_hash,
759
+ synthesizedAt: row.synthesized_at,
760
+ expiresAt: row.expires_at,
761
+ inFlightSince: row.in_flight_since,
762
+ createdAt: row.created_at,
763
+ updatedAt: row.updated_at
764
+ });
765
+ }
766
+ const STALENESS_DAYS = 30;
767
+ var SynthesisRepository = class {
768
+ #db;
769
+ constructor(db) {
770
+ this.#db = db;
771
+ }
772
+ saveSynthesis(scope, content, sourceHash) {
773
+ const now = (/* @__PURE__ */ new Date()).toISOString();
774
+ const expiresAt = new Date(Date.now() + STALENESS_DAYS * 24 * 60 * 60 * 1e3).toISOString();
775
+ if (this.#db.db.prepare("SELECT id FROM syntheses WHERE scope = ?").get(scope) !== void 0) this.#db.db.prepare(`UPDATE syntheses
776
+ SET content = ?, source_memory_hash = ?, synthesized_at = ?, expires_at = ?,
777
+ in_flight_since = NULL, updated_at = ?
778
+ WHERE scope = ?`).run(content, sourceHash, now, expiresAt, now, scope);
779
+ else {
780
+ const id = randomUUID();
781
+ this.#db.db.prepare(`INSERT INTO syntheses
782
+ (id, scope, content, source_memory_hash, synthesized_at, expires_at,
783
+ in_flight_since, created_at, updated_at)
784
+ VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(id, scope, content, sourceHash, now, expiresAt, now, now);
785
+ }
786
+ const row = this.#db.db.prepare("SELECT * FROM syntheses WHERE scope = ?").get(scope);
787
+ if (row === void 0) throw new Error(`Failed to save synthesis for scope: ${scope}`);
788
+ return rowToSynthesis(row);
789
+ }
790
+ getSynthesis(scope) {
791
+ const row = this.#db.db.prepare("SELECT * FROM syntheses WHERE scope = ?").get(scope);
792
+ return row !== void 0 ? rowToSynthesis(row) : void 0;
793
+ }
794
+ markInFlight(scope) {
795
+ const now = (/* @__PURE__ */ new Date()).toISOString();
796
+ if (this.#db.db.prepare("SELECT id FROM syntheses WHERE scope = ?").get(scope) !== void 0) this.#db.db.prepare("UPDATE syntheses SET in_flight_since = ?, updated_at = ? WHERE scope = ?").run(now, now, scope);
797
+ else {
798
+ const id = randomUUID();
799
+ const placeholder = "pending";
800
+ const future = new Date(Date.now() + STALENESS_DAYS * 24 * 60 * 60 * 1e3).toISOString();
801
+ this.#db.db.prepare(`INSERT INTO syntheses
802
+ (id, scope, content, source_memory_hash, synthesized_at, expires_at,
803
+ in_flight_since, created_at, updated_at)
804
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, scope, placeholder, "", now, future, now, now, now);
805
+ }
806
+ }
807
+ clearInFlight(scope) {
808
+ const now = (/* @__PURE__ */ new Date()).toISOString();
809
+ this.#db.db.prepare("UPDATE syntheses SET in_flight_since = NULL, updated_at = ? WHERE scope = ?").run(now, scope);
810
+ }
811
+ computeSourceMemoryHash(scope) {
812
+ let contents;
813
+ if (scope === "global") contents = this.#db.db.prepare(`SELECT content FROM memories
814
+ WHERE id NOT IN (SELECT memory_id FROM memory_projects)
815
+ ORDER BY id`).all();
816
+ else contents = this.#db.db.prepare(`SELECT m.content FROM memories m
817
+ JOIN memory_projects mp ON mp.memory_id = m.id
818
+ JOIN projects p ON p.id = mp.project_id
819
+ WHERE p.scope_hash = ?
820
+ ORDER BY m.id`).all(scope);
821
+ return createHash("sha256").update(JSON.stringify(contents.map((r) => r.content))).digest("hex");
822
+ }
823
+ getExpiredOrDirtyScopes() {
824
+ const allScopes = this.getAllActiveScopes();
825
+ const now = (/* @__PURE__ */ new Date()).toISOString();
826
+ const results = [];
827
+ for (const scope of allScopes) {
828
+ const row = this.#db.db.prepare("SELECT * FROM syntheses WHERE scope = ?").get(scope);
829
+ if (row === void 0 || row.content === "pending" && row.source_memory_hash === "") {
830
+ results.push({
831
+ scope,
832
+ reason: "missing"
833
+ });
834
+ continue;
835
+ }
836
+ if (row.expires_at <= now) {
837
+ results.push({
838
+ scope,
839
+ reason: "expired"
840
+ });
841
+ continue;
842
+ }
843
+ if (this.computeSourceMemoryHash(scope) !== row.source_memory_hash) results.push({
844
+ scope,
845
+ reason: "dirty"
846
+ });
847
+ }
848
+ return results;
849
+ }
850
+ getAllActiveScopes() {
851
+ return ["global", ...this.#db.db.prepare("SELECT DISTINCT scope_hash FROM projects").all().map((r) => r.scope_hash)];
852
+ }
853
+ expireStale() {
854
+ const now = (/* @__PURE__ */ new Date()).toISOString();
855
+ this.#db.db.prepare("DELETE FROM syntheses WHERE expires_at < ?").run(now);
856
+ }
857
+ };
858
+ //#endregion
859
+ export { DatabaseError, DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MIGRATIONS, MembankError, MemoryPatchSchema, MemoryRepository, MemoryRowSchema, MemorySchema, MemoryTypeSchema, PIN_BUDGET_THRESHOLD, ProjectRepository, ProjectRowSchema, ProjectSchema, QueryEngine, QueryOptionsSchema, ReviewEventRowSchema, ReviewEventSchema, ReviewReasonSchema, SaveOptionsSchema, SessionContextBuilder, SessionContextSchema, SynthesisRepository, SynthesisSchema, TagsJsonSchema, isSynthesisEnabled, listMemoryTypes, resolveProject, resolveScope, rowToMemory, rowToProject, runScopeToProjectsMigration };
588
860
 
589
861
  //# sourceMappingURL=index.mjs.map