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.
- package/README.md +26 -7
- package/package.json +3 -5
- package/plugin/dist/cli/contextkit.js +177 -65
- package/plugin/dist/hooks/agentSpawn.js +177 -65
- package/plugin/dist/hooks/kiro-hooks.js +177 -65
- package/plugin/dist/hooks/postToolUse.js +415 -106
- package/plugin/dist/hooks/stop.js +213 -74
- package/plugin/dist/hooks/userPromptSubmit.js +177 -65
- package/plugin/dist/index.js +177 -65
- package/plugin/dist/sdk/index.js +177 -65
- package/plugin/dist/servers/mcp-server.js +28 -0
- package/plugin/dist/services/search/HybridSearch.js +66 -40
- package/plugin/dist/services/search/index.js +66 -40
- package/plugin/dist/services/sqlite/Database.js +23 -0
- package/plugin/dist/services/sqlite/Observations.js +48 -36
- package/plugin/dist/services/sqlite/Search.js +17 -4
- package/plugin/dist/services/sqlite/index.js +88 -40
- package/plugin/dist/viewer.html +5 -0
- package/plugin/dist/viewer.js +529 -250
- package/plugin/dist/worker-service.js +263 -44
|
@@ -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 +=
|
|
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.*,
|
|
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 +=
|
|
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
|
|
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
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
|
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
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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 +=
|
|
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.*,
|
|
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 +=
|
|
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
|
|
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
|
-
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
uniqueTexts.
|
|
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
|
-
|
|
842
|
-
|
|
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 +=
|
|
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.*,
|
|
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 +=
|
|
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,
|
package/plugin/dist/viewer.html
CHANGED
|
@@ -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
|
},
|