kiro-memory 1.7.1 → 1.8.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.
@@ -58,7 +58,7 @@ function searchObservationsFTS(db, query, filters = {}) {
58
58
  sql += " AND o.created_at_epoch <= ?";
59
59
  params.push(filters.dateEnd);
60
60
  }
61
- sql += " ORDER BY rank LIMIT ?";
61
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
62
62
  params.push(limit);
63
63
  const stmt = db.query(sql);
64
64
  return stmt.all(...params);
@@ -72,7 +72,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
72
72
  const safeQuery = sanitizeFTS5Query(query);
73
73
  if (!safeQuery) return [];
74
74
  let sql = `
75
- SELECT o.*, rank as fts5_rank FROM observations o
75
+ SELECT o.*, bm25(observations_fts, ${BM25_WEIGHTS}) as fts5_rank FROM observations o
76
76
  JOIN observations_fts fts ON o.id = fts.rowid
77
77
  WHERE observations_fts MATCH ?
78
78
  `;
@@ -93,7 +93,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
93
93
  sql += " AND o.created_at_epoch <= ?";
94
94
  params.push(filters.dateEnd);
95
95
  }
96
- sql += " ORDER BY rank LIMIT ?";
96
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
97
97
  params.push(limit);
98
98
  const stmt = db.query(sql);
99
99
  return stmt.all(...params);
@@ -197,11 +197,23 @@ function getProjectStats(db, project) {
197
197
  const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
198
198
  const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
199
199
  const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
200
+ const discoveryStmt = db.query(
201
+ "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
202
+ );
203
+ const discoveryTokens = discoveryStmt.get(project)?.total || 0;
204
+ const readStmt = db.query(
205
+ `SELECT COALESCE(SUM(
206
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
207
+ ), 0) as total FROM observations WHERE project = ?`
208
+ );
209
+ const readTokens = readStmt.get(project)?.total || 0;
210
+ const savings = Math.max(0, discoveryTokens - readTokens);
200
211
  return {
201
212
  observations: obsStmt.get(project)?.count || 0,
202
213
  summaries: sumStmt.get(project)?.count || 0,
203
214
  sessions: sesStmt.get(project)?.count || 0,
204
- prompts: prmStmt.get(project)?.count || 0
215
+ prompts: prmStmt.get(project)?.count || 0,
216
+ tokenEconomics: { discoveryTokens, readTokens, savings }
205
217
  };
206
218
  }
207
219
  function getStaleObservations(db, project) {
@@ -243,9 +255,11 @@ function markObservationsStale(db, ids, stale) {
243
255
  [stale ? 1 : 0, ...validIds]
244
256
  );
245
257
  }
258
+ var BM25_WEIGHTS;
246
259
  var init_Search = __esm({
247
260
  "src/services/sqlite/Search.ts"() {
248
261
  "use strict";
262
+ BM25_WEIGHTS = "10.0, 1.0, 5.0, 3.0";
249
263
  }
250
264
  });
251
265
 
@@ -257,19 +271,28 @@ __export(Observations_exports, {
257
271
  deleteObservation: () => deleteObservation,
258
272
  getObservationsByProject: () => getObservationsByProject,
259
273
  getObservationsBySession: () => getObservationsBySession,
274
+ isDuplicateObservation: () => isDuplicateObservation,
260
275
  searchObservations: () => searchObservations,
261
276
  updateLastAccessed: () => updateLastAccessed
262
277
  });
263
278
  function escapeLikePattern2(input) {
264
279
  return input.replace(/[%_\\]/g, "\\$&");
265
280
  }
266
- function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
281
+ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
282
+ if (!contentHash) return false;
283
+ const threshold = Date.now() - windowMs;
284
+ const result = db.query(
285
+ "SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch > ? LIMIT 1"
286
+ ).get(contentHash, threshold);
287
+ return !!result;
288
+ }
289
+ function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
267
290
  const now = /* @__PURE__ */ new Date();
268
291
  const result = db.run(
269
- `INSERT INTO observations
270
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
271
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
272
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
292
+ `INSERT INTO observations
293
+ (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch, content_hash, discovery_tokens)
294
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
295
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
273
296
  );
274
297
  return Number(result.lastInsertRowid);
275
298
  }
@@ -325,39 +348,42 @@ function consolidateObservations(db, project, options = {}) {
325
348
  if (groups.length === 0) return { merged: 0, removed: 0 };
326
349
  let totalMerged = 0;
327
350
  let totalRemoved = 0;
328
- for (const group of groups) {
329
- const obsIds = group.ids.split(",").map(Number);
330
- const placeholders = obsIds.map(() => "?").join(",");
331
- const observations = db.query(
332
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
333
- ).all(...obsIds);
334
- if (observations.length < minGroupSize) continue;
335
- if (options.dryRun) {
336
- totalMerged += 1;
337
- totalRemoved += observations.length - 1;
338
- continue;
339
- }
340
- const keeper = observations[0];
341
- const others = observations.slice(1);
342
- const uniqueTexts = /* @__PURE__ */ new Set();
343
- if (keeper.text) uniqueTexts.add(keeper.text);
344
- for (const obs of others) {
345
- if (obs.text && !uniqueTexts.has(obs.text)) {
346
- uniqueTexts.add(obs.text);
351
+ const runConsolidation = db.transaction(() => {
352
+ for (const group of groups) {
353
+ const obsIds = group.ids.split(",").map(Number);
354
+ const placeholders = obsIds.map(() => "?").join(",");
355
+ const observations = db.query(
356
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
357
+ ).all(...obsIds);
358
+ if (observations.length < minGroupSize) continue;
359
+ if (options.dryRun) {
360
+ totalMerged += 1;
361
+ totalRemoved += observations.length - 1;
362
+ continue;
347
363
  }
364
+ const keeper = observations[0];
365
+ const others = observations.slice(1);
366
+ const uniqueTexts = /* @__PURE__ */ new Set();
367
+ if (keeper.text) uniqueTexts.add(keeper.text);
368
+ for (const obs of others) {
369
+ if (obs.text && !uniqueTexts.has(obs.text)) {
370
+ uniqueTexts.add(obs.text);
371
+ }
372
+ }
373
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
374
+ db.run(
375
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
376
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
377
+ );
378
+ const removeIds = others.map((o) => o.id);
379
+ const removePlaceholders = removeIds.map(() => "?").join(",");
380
+ db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
381
+ db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
382
+ totalMerged += 1;
383
+ totalRemoved += removeIds.length;
348
384
  }
349
- const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
350
- db.run(
351
- "UPDATE observations SET text = ?, title = ? WHERE id = ?",
352
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
353
- );
354
- const removeIds = others.map((o) => o.id);
355
- const removePlaceholders = removeIds.map(() => "?").join(",");
356
- db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
357
- db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
358
- totalMerged += 1;
359
- totalRemoved += removeIds.length;
360
- }
385
+ });
386
+ runConsolidation();
361
387
  return { merged: totalMerged, removed: totalRemoved };
362
388
  }
363
389
  var init_Observations = __esm({
@@ -685,6 +685,29 @@ var MigrationRunner = class {
685
685
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
686
686
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
687
687
  }
688
+ },
689
+ {
690
+ version: 7,
691
+ up: (db) => {
692
+ db.run("ALTER TABLE observations ADD COLUMN content_hash TEXT");
693
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_hash ON observations(content_hash)");
694
+ }
695
+ },
696
+ {
697
+ version: 8,
698
+ up: (db) => {
699
+ db.run("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
700
+ db.run("ALTER TABLE summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
701
+ }
702
+ },
703
+ {
704
+ version: 9,
705
+ up: (db) => {
706
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_epoch ON observations(project, created_at_epoch DESC)");
707
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_type ON observations(project, type)");
708
+ db.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_epoch ON summaries(project, created_at_epoch DESC)");
709
+ db.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_epoch ON prompts(project, created_at_epoch DESC)");
710
+ }
688
711
  }
689
712
  ];
690
713
  }
@@ -4,13 +4,21 @@ import { createRequire } from 'module';const require = createRequire(import.meta
4
4
  function escapeLikePattern(input) {
5
5
  return input.replace(/[%_\\]/g, "\\$&");
6
6
  }
7
- function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
7
+ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
8
+ if (!contentHash) return false;
9
+ const threshold = Date.now() - windowMs;
10
+ const result = db.query(
11
+ "SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch > ? LIMIT 1"
12
+ ).get(contentHash, threshold);
13
+ return !!result;
14
+ }
15
+ function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
8
16
  const now = /* @__PURE__ */ new Date();
9
17
  const result = db.run(
10
- `INSERT INTO observations
11
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
12
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
13
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
18
+ `INSERT INTO observations
19
+ (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch, content_hash, discovery_tokens)
20
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
21
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
14
22
  );
15
23
  return Number(result.lastInsertRowid);
16
24
  }
@@ -66,39 +74,42 @@ function consolidateObservations(db, project, options = {}) {
66
74
  if (groups.length === 0) return { merged: 0, removed: 0 };
67
75
  let totalMerged = 0;
68
76
  let totalRemoved = 0;
69
- for (const group of groups) {
70
- const obsIds = group.ids.split(",").map(Number);
71
- const placeholders = obsIds.map(() => "?").join(",");
72
- const observations = db.query(
73
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
74
- ).all(...obsIds);
75
- if (observations.length < minGroupSize) continue;
76
- if (options.dryRun) {
77
- totalMerged += 1;
78
- totalRemoved += observations.length - 1;
79
- continue;
80
- }
81
- const keeper = observations[0];
82
- const others = observations.slice(1);
83
- const uniqueTexts = /* @__PURE__ */ new Set();
84
- if (keeper.text) uniqueTexts.add(keeper.text);
85
- for (const obs of others) {
86
- if (obs.text && !uniqueTexts.has(obs.text)) {
87
- uniqueTexts.add(obs.text);
77
+ const runConsolidation = db.transaction(() => {
78
+ for (const group of groups) {
79
+ const obsIds = group.ids.split(",").map(Number);
80
+ const placeholders = obsIds.map(() => "?").join(",");
81
+ const observations = db.query(
82
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
83
+ ).all(...obsIds);
84
+ if (observations.length < minGroupSize) continue;
85
+ if (options.dryRun) {
86
+ totalMerged += 1;
87
+ totalRemoved += observations.length - 1;
88
+ continue;
88
89
  }
90
+ const keeper = observations[0];
91
+ const others = observations.slice(1);
92
+ const uniqueTexts = /* @__PURE__ */ new Set();
93
+ if (keeper.text) uniqueTexts.add(keeper.text);
94
+ for (const obs of others) {
95
+ if (obs.text && !uniqueTexts.has(obs.text)) {
96
+ uniqueTexts.add(obs.text);
97
+ }
98
+ }
99
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
100
+ db.run(
101
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
102
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
103
+ );
104
+ const removeIds = others.map((o) => o.id);
105
+ const removePlaceholders = removeIds.map(() => "?").join(",");
106
+ db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
107
+ db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
108
+ totalMerged += 1;
109
+ totalRemoved += removeIds.length;
89
110
  }
90
- const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
91
- db.run(
92
- "UPDATE observations SET text = ?, title = ? WHERE id = ?",
93
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
94
- );
95
- const removeIds = others.map((o) => o.id);
96
- const removePlaceholders = removeIds.map(() => "?").join(",");
97
- db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
98
- db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
99
- totalMerged += 1;
100
- totalRemoved += removeIds.length;
101
- }
111
+ });
112
+ runConsolidation();
102
113
  return { merged: totalMerged, removed: totalRemoved };
103
114
  }
104
115
  export {
@@ -107,6 +118,7 @@ export {
107
118
  deleteObservation,
108
119
  getObservationsByProject,
109
120
  getObservationsBySession,
121
+ isDuplicateObservation,
110
122
  searchObservations,
111
123
  updateLastAccessed
112
124
  };
@@ -2,6 +2,7 @@ import { createRequire } from 'module';const require = createRequire(import.meta
2
2
 
3
3
  // src/services/sqlite/Search.ts
4
4
  import { existsSync, statSync } from "fs";
5
+ var BM25_WEIGHTS = "10.0, 1.0, 5.0, 3.0";
5
6
  function escapeLikePattern(input) {
6
7
  return input.replace(/[%_\\]/g, "\\$&");
7
8
  }
@@ -37,7 +38,7 @@ function searchObservationsFTS(db, query, filters = {}) {
37
38
  sql += " AND o.created_at_epoch <= ?";
38
39
  params.push(filters.dateEnd);
39
40
  }
40
- sql += " ORDER BY rank LIMIT ?";
41
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
41
42
  params.push(limit);
42
43
  const stmt = db.query(sql);
43
44
  return stmt.all(...params);
@@ -51,7 +52,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
51
52
  const safeQuery = sanitizeFTS5Query(query);
52
53
  if (!safeQuery) return [];
53
54
  let sql = `
54
- SELECT o.*, rank as fts5_rank FROM observations o
55
+ SELECT o.*, bm25(observations_fts, ${BM25_WEIGHTS}) as fts5_rank FROM observations o
55
56
  JOIN observations_fts fts ON o.id = fts.rowid
56
57
  WHERE observations_fts MATCH ?
57
58
  `;
@@ -72,7 +73,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
72
73
  sql += " AND o.created_at_epoch <= ?";
73
74
  params.push(filters.dateEnd);
74
75
  }
75
- sql += " ORDER BY rank LIMIT ?";
76
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
76
77
  params.push(limit);
77
78
  const stmt = db.query(sql);
78
79
  return stmt.all(...params);
@@ -176,11 +177,23 @@ function getProjectStats(db, project) {
176
177
  const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
177
178
  const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
178
179
  const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
180
+ const discoveryStmt = db.query(
181
+ "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
182
+ );
183
+ const discoveryTokens = discoveryStmt.get(project)?.total || 0;
184
+ const readStmt = db.query(
185
+ `SELECT COALESCE(SUM(
186
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
187
+ ), 0) as total FROM observations WHERE project = ?`
188
+ );
189
+ const readTokens = readStmt.get(project)?.total || 0;
190
+ const savings = Math.max(0, discoveryTokens - readTokens);
179
191
  return {
180
192
  observations: obsStmt.get(project)?.count || 0,
181
193
  summaries: sumStmt.get(project)?.count || 0,
182
194
  sessions: sesStmt.get(project)?.count || 0,
183
- prompts: prmStmt.get(project)?.count || 0
195
+ prompts: prmStmt.get(project)?.count || 0,
196
+ tokenEconomics: { discoveryTokens, readTokens, savings }
184
197
  };
185
198
  }
186
199
  function getStaleObservations(db, project) {
@@ -685,6 +685,29 @@ var MigrationRunner = class {
685
685
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
686
686
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
687
687
  }
688
+ },
689
+ {
690
+ version: 7,
691
+ up: (db) => {
692
+ db.run("ALTER TABLE observations ADD COLUMN content_hash TEXT");
693
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_hash ON observations(content_hash)");
694
+ }
695
+ },
696
+ {
697
+ version: 8,
698
+ up: (db) => {
699
+ db.run("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
700
+ db.run("ALTER TABLE summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
701
+ }
702
+ },
703
+ {
704
+ version: 9,
705
+ up: (db) => {
706
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_epoch ON observations(project, created_at_epoch DESC)");
707
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_type ON observations(project, type)");
708
+ db.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_epoch ON summaries(project, created_at_epoch DESC)");
709
+ db.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_epoch ON prompts(project, created_at_epoch DESC)");
710
+ }
688
711
  }
689
712
  ];
690
713
  }
@@ -755,13 +778,21 @@ function getSessionsByProject(db, project, limit = 100) {
755
778
  function escapeLikePattern(input) {
756
779
  return input.replace(/[%_\\]/g, "\\$&");
757
780
  }
758
- function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
781
+ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
782
+ if (!contentHash) return false;
783
+ const threshold = Date.now() - windowMs;
784
+ const result = db.query(
785
+ "SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch > ? LIMIT 1"
786
+ ).get(contentHash, threshold);
787
+ return !!result;
788
+ }
789
+ function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
759
790
  const now = /* @__PURE__ */ new Date();
760
791
  const result = db.run(
761
- `INSERT INTO observations
762
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
763
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
764
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
792
+ `INSERT INTO observations
793
+ (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch, content_hash, discovery_tokens)
794
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
795
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
765
796
  );
766
797
  return Number(result.lastInsertRowid);
767
798
  }
@@ -817,39 +848,42 @@ function consolidateObservations(db, project, options = {}) {
817
848
  if (groups.length === 0) return { merged: 0, removed: 0 };
818
849
  let totalMerged = 0;
819
850
  let totalRemoved = 0;
820
- for (const group of groups) {
821
- const obsIds = group.ids.split(",").map(Number);
822
- const placeholders = obsIds.map(() => "?").join(",");
823
- const observations = db.query(
824
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
825
- ).all(...obsIds);
826
- if (observations.length < minGroupSize) continue;
827
- if (options.dryRun) {
828
- totalMerged += 1;
829
- totalRemoved += observations.length - 1;
830
- continue;
831
- }
832
- const keeper = observations[0];
833
- const others = observations.slice(1);
834
- const uniqueTexts = /* @__PURE__ */ new Set();
835
- if (keeper.text) uniqueTexts.add(keeper.text);
836
- for (const obs of others) {
837
- if (obs.text && !uniqueTexts.has(obs.text)) {
838
- uniqueTexts.add(obs.text);
851
+ const runConsolidation = db.transaction(() => {
852
+ for (const group of groups) {
853
+ const obsIds = group.ids.split(",").map(Number);
854
+ const placeholders = obsIds.map(() => "?").join(",");
855
+ const observations = db.query(
856
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
857
+ ).all(...obsIds);
858
+ if (observations.length < minGroupSize) continue;
859
+ if (options.dryRun) {
860
+ totalMerged += 1;
861
+ totalRemoved += observations.length - 1;
862
+ continue;
863
+ }
864
+ const keeper = observations[0];
865
+ const others = observations.slice(1);
866
+ const uniqueTexts = /* @__PURE__ */ new Set();
867
+ if (keeper.text) uniqueTexts.add(keeper.text);
868
+ for (const obs of others) {
869
+ if (obs.text && !uniqueTexts.has(obs.text)) {
870
+ uniqueTexts.add(obs.text);
871
+ }
839
872
  }
873
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
874
+ db.run(
875
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
876
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
877
+ );
878
+ const removeIds = others.map((o) => o.id);
879
+ const removePlaceholders = removeIds.map(() => "?").join(",");
880
+ db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
881
+ db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
882
+ totalMerged += 1;
883
+ totalRemoved += removeIds.length;
840
884
  }
841
- const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
842
- db.run(
843
- "UPDATE observations SET text = ?, title = ? WHERE id = ?",
844
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
845
- );
846
- const removeIds = others.map((o) => o.id);
847
- const removePlaceholders = removeIds.map(() => "?").join(",");
848
- db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
849
- db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
850
- totalMerged += 1;
851
- totalRemoved += removeIds.length;
852
- }
885
+ });
886
+ runConsolidation();
853
887
  return { merged: totalMerged, removed: totalRemoved };
854
888
  }
855
889
 
@@ -1090,6 +1124,7 @@ function getReportData(db, project, startEpoch, endEpoch) {
1090
1124
 
1091
1125
  // src/services/sqlite/Search.ts
1092
1126
  import { existsSync as existsSync3, statSync } from "fs";
1127
+ var BM25_WEIGHTS = "10.0, 1.0, 5.0, 3.0";
1093
1128
  function escapeLikePattern3(input) {
1094
1129
  return input.replace(/[%_\\]/g, "\\$&");
1095
1130
  }
@@ -1125,7 +1160,7 @@ function searchObservationsFTS(db, query, filters = {}) {
1125
1160
  sql += " AND o.created_at_epoch <= ?";
1126
1161
  params.push(filters.dateEnd);
1127
1162
  }
1128
- sql += " ORDER BY rank LIMIT ?";
1163
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
1129
1164
  params.push(limit);
1130
1165
  const stmt = db.query(sql);
1131
1166
  return stmt.all(...params);
@@ -1139,7 +1174,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
1139
1174
  const safeQuery = sanitizeFTS5Query(query);
1140
1175
  if (!safeQuery) return [];
1141
1176
  let sql = `
1142
- SELECT o.*, rank as fts5_rank FROM observations o
1177
+ SELECT o.*, bm25(observations_fts, ${BM25_WEIGHTS}) as fts5_rank FROM observations o
1143
1178
  JOIN observations_fts fts ON o.id = fts.rowid
1144
1179
  WHERE observations_fts MATCH ?
1145
1180
  `;
@@ -1160,7 +1195,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
1160
1195
  sql += " AND o.created_at_epoch <= ?";
1161
1196
  params.push(filters.dateEnd);
1162
1197
  }
1163
- sql += " ORDER BY rank LIMIT ?";
1198
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
1164
1199
  params.push(limit);
1165
1200
  const stmt = db.query(sql);
1166
1201
  return stmt.all(...params);
@@ -1264,11 +1299,23 @@ function getProjectStats(db, project) {
1264
1299
  const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
1265
1300
  const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
1266
1301
  const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
1302
+ const discoveryStmt = db.query(
1303
+ "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
1304
+ );
1305
+ const discoveryTokens = discoveryStmt.get(project)?.total || 0;
1306
+ const readStmt = db.query(
1307
+ `SELECT COALESCE(SUM(
1308
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
1309
+ ), 0) as total FROM observations WHERE project = ?`
1310
+ );
1311
+ const readTokens = readStmt.get(project)?.total || 0;
1312
+ const savings = Math.max(0, discoveryTokens - readTokens);
1267
1313
  return {
1268
1314
  observations: obsStmt.get(project)?.count || 0,
1269
1315
  summaries: sumStmt.get(project)?.count || 0,
1270
1316
  sessions: sesStmt.get(project)?.count || 0,
1271
- prompts: prmStmt.get(project)?.count || 0
1317
+ prompts: prmStmt.get(project)?.count || 0,
1318
+ tokenEconomics: { discoveryTokens, readTokens, savings }
1272
1319
  };
1273
1320
  }
1274
1321
  function getStaleObservations(db, project) {
@@ -1346,6 +1393,7 @@ export {
1346
1393
  getSummaryBySession,
1347
1394
  getTimeline,
1348
1395
  initializeDatabase,
1396
+ isDuplicateObservation,
1349
1397
  markObservationsStale,
1350
1398
  searchObservations,
1351
1399
  searchObservationsFTS,
@@ -75,6 +75,10 @@
75
75
  '0%, 100%': { opacity: '1' },
76
76
  '50%': { opacity: '0.5' },
77
77
  },
78
+ 'slide-in-left': {
79
+ '0%': { opacity: '0', transform: 'translateX(-100%)' },
80
+ '100%': { opacity: '1', transform: 'translateX(0)' },
81
+ },
78
82
  },
79
83
  animation: {
80
84
  'slide-up': 'slide-up 0.35s ease-out forwards',
@@ -82,6 +86,7 @@
82
86
  'scale-in': 'scale-in 0.2s ease-out forwards',
83
87
  'spin': 'spin 0.8s linear infinite',
84
88
  'pulse-dot': 'pulse-dot 2s ease-in-out infinite',
89
+ 'slide-in-left': 'slide-in-left 0.25s ease-out forwards',
85
90
  },
86
91
  },
87
92
  },