kiro-memory 1.7.0 → 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.
@@ -24,19 +24,28 @@ __export(Observations_exports, {
24
24
  deleteObservation: () => deleteObservation,
25
25
  getObservationsByProject: () => getObservationsByProject,
26
26
  getObservationsBySession: () => getObservationsBySession,
27
+ isDuplicateObservation: () => isDuplicateObservation,
27
28
  searchObservations: () => searchObservations,
28
29
  updateLastAccessed: () => updateLastAccessed
29
30
  });
30
31
  function escapeLikePattern(input) {
31
32
  return input.replace(/[%_\\]/g, "\\$&");
32
33
  }
33
- function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
34
+ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
35
+ if (!contentHash) return false;
36
+ const threshold = Date.now() - windowMs;
37
+ const result = db.query(
38
+ "SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch > ? LIMIT 1"
39
+ ).get(contentHash, threshold);
40
+ return !!result;
41
+ }
42
+ function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
34
43
  const now = /* @__PURE__ */ new Date();
35
44
  const result = db.run(
36
- `INSERT INTO observations
37
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
38
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
39
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
45
+ `INSERT INTO observations
46
+ (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)
47
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
48
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
40
49
  );
41
50
  return Number(result.lastInsertRowid);
42
51
  }
@@ -92,39 +101,42 @@ function consolidateObservations(db, project, options = {}) {
92
101
  if (groups.length === 0) return { merged: 0, removed: 0 };
93
102
  let totalMerged = 0;
94
103
  let totalRemoved = 0;
95
- for (const group of groups) {
96
- const obsIds = group.ids.split(",").map(Number);
97
- const placeholders = obsIds.map(() => "?").join(",");
98
- const observations = db.query(
99
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
100
- ).all(...obsIds);
101
- if (observations.length < minGroupSize) continue;
102
- if (options.dryRun) {
103
- totalMerged += 1;
104
- totalRemoved += observations.length - 1;
105
- continue;
106
- }
107
- const keeper = observations[0];
108
- const others = observations.slice(1);
109
- const uniqueTexts = /* @__PURE__ */ new Set();
110
- if (keeper.text) uniqueTexts.add(keeper.text);
111
- for (const obs of others) {
112
- if (obs.text && !uniqueTexts.has(obs.text)) {
113
- uniqueTexts.add(obs.text);
104
+ const runConsolidation = db.transaction(() => {
105
+ for (const group of groups) {
106
+ const obsIds = group.ids.split(",").map(Number);
107
+ const placeholders = obsIds.map(() => "?").join(",");
108
+ const observations = db.query(
109
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
110
+ ).all(...obsIds);
111
+ if (observations.length < minGroupSize) continue;
112
+ if (options.dryRun) {
113
+ totalMerged += 1;
114
+ totalRemoved += observations.length - 1;
115
+ continue;
114
116
  }
117
+ const keeper = observations[0];
118
+ const others = observations.slice(1);
119
+ const uniqueTexts = /* @__PURE__ */ new Set();
120
+ if (keeper.text) uniqueTexts.add(keeper.text);
121
+ for (const obs of others) {
122
+ if (obs.text && !uniqueTexts.has(obs.text)) {
123
+ uniqueTexts.add(obs.text);
124
+ }
125
+ }
126
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
127
+ db.run(
128
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
129
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
130
+ );
131
+ const removeIds = others.map((o) => o.id);
132
+ const removePlaceholders = removeIds.map(() => "?").join(",");
133
+ db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
134
+ db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
135
+ totalMerged += 1;
136
+ totalRemoved += removeIds.length;
115
137
  }
116
- const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
117
- db.run(
118
- "UPDATE observations SET text = ?, title = ? WHERE id = ?",
119
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
120
- );
121
- const removeIds = others.map((o) => o.id);
122
- const removePlaceholders = removeIds.map(() => "?").join(",");
123
- db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
124
- db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
125
- totalMerged += 1;
126
- totalRemoved += removeIds.length;
127
- }
138
+ });
139
+ runConsolidation();
128
140
  return { merged: totalMerged, removed: totalRemoved };
129
141
  }
130
142
  var init_Observations = __esm({
@@ -182,7 +194,7 @@ function searchObservationsFTS(db, query, filters = {}) {
182
194
  sql += " AND o.created_at_epoch <= ?";
183
195
  params.push(filters.dateEnd);
184
196
  }
185
- sql += " ORDER BY rank LIMIT ?";
197
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
186
198
  params.push(limit);
187
199
  const stmt = db.query(sql);
188
200
  return stmt.all(...params);
@@ -196,7 +208,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
196
208
  const safeQuery = sanitizeFTS5Query(query);
197
209
  if (!safeQuery) return [];
198
210
  let sql = `
199
- SELECT o.*, rank as fts5_rank FROM observations o
211
+ SELECT o.*, bm25(observations_fts, ${BM25_WEIGHTS}) as fts5_rank FROM observations o
200
212
  JOIN observations_fts fts ON o.id = fts.rowid
201
213
  WHERE observations_fts MATCH ?
202
214
  `;
@@ -217,7 +229,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
217
229
  sql += " AND o.created_at_epoch <= ?";
218
230
  params.push(filters.dateEnd);
219
231
  }
220
- sql += " ORDER BY rank LIMIT ?";
232
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
221
233
  params.push(limit);
222
234
  const stmt = db.query(sql);
223
235
  return stmt.all(...params);
@@ -321,11 +333,23 @@ function getProjectStats(db, project) {
321
333
  const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
322
334
  const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
323
335
  const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
336
+ const discoveryStmt = db.query(
337
+ "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
338
+ );
339
+ const discoveryTokens = discoveryStmt.get(project)?.total || 0;
340
+ const readStmt = db.query(
341
+ `SELECT COALESCE(SUM(
342
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
343
+ ), 0) as total FROM observations WHERE project = ?`
344
+ );
345
+ const readTokens = readStmt.get(project)?.total || 0;
346
+ const savings = Math.max(0, discoveryTokens - readTokens);
324
347
  return {
325
348
  observations: obsStmt.get(project)?.count || 0,
326
349
  summaries: sumStmt.get(project)?.count || 0,
327
350
  sessions: sesStmt.get(project)?.count || 0,
328
- prompts: prmStmt.get(project)?.count || 0
351
+ prompts: prmStmt.get(project)?.count || 0,
352
+ tokenEconomics: { discoveryTokens, readTokens, savings }
329
353
  };
330
354
  }
331
355
  function getStaleObservations(db, project) {
@@ -367,9 +391,11 @@ function markObservationsStale(db, ids, stale) {
367
391
  [stale ? 1 : 0, ...validIds]
368
392
  );
369
393
  }
394
+ var BM25_WEIGHTS;
370
395
  var init_Search = __esm({
371
396
  "src/services/sqlite/Search.ts"() {
372
397
  "use strict";
398
+ BM25_WEIGHTS = "10.0, 1.0, 5.0, 3.0";
373
399
  }
374
400
  });
375
401
 
@@ -1136,6 +1162,29 @@ var MigrationRunner = class {
1136
1162
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
1137
1163
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
1138
1164
  }
1165
+ },
1166
+ {
1167
+ version: 7,
1168
+ up: (db) => {
1169
+ db.run("ALTER TABLE observations ADD COLUMN content_hash TEXT");
1170
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_hash ON observations(content_hash)");
1171
+ }
1172
+ },
1173
+ {
1174
+ version: 8,
1175
+ up: (db) => {
1176
+ db.run("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
1177
+ db.run("ALTER TABLE summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
1178
+ }
1179
+ },
1180
+ {
1181
+ version: 9,
1182
+ up: (db) => {
1183
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_epoch ON observations(project, created_at_epoch DESC)");
1184
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_type ON observations(project, type)");
1185
+ db.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_epoch ON summaries(project, created_at_epoch DESC)");
1186
+ db.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_epoch ON prompts(project, created_at_epoch DESC)");
1187
+ }
1139
1188
  }
1140
1189
  ];
1141
1190
  }
@@ -1380,6 +1429,7 @@ init_Search();
1380
1429
 
1381
1430
  // src/sdk/index.ts
1382
1431
  init_Observations();
1432
+ import { createHash } from "crypto";
1383
1433
  init_Search();
1384
1434
 
1385
1435
  // src/services/search/EmbeddingService.ts
@@ -1892,31 +1942,71 @@ var KiroMemorySDK = class {
1892
1942
  logger.debug("SDK", `Embedding generation fallita per obs ${observationId}: ${error}`);
1893
1943
  }
1894
1944
  }
1945
+ /**
1946
+ * Genera content hash SHA256 per deduplicazione basata su contenuto.
1947
+ * Usa (project + type + title + narrative) come tupla di identità semantica.
1948
+ * NON include sessionId perché è unico ad ogni invocazione.
1949
+ */
1950
+ generateContentHash(type, title, narrative) {
1951
+ const payload = `${this.project}|${type}|${title}|${narrative || ""}`;
1952
+ return createHash("sha256").update(payload).digest("hex");
1953
+ }
1954
+ /**
1955
+ * Finestre di deduplicazione per tipo (ms).
1956
+ * Tipi con molte ripetizioni hanno finestre più ampie.
1957
+ */
1958
+ getDeduplicationWindow(type) {
1959
+ switch (type) {
1960
+ case "file-read":
1961
+ return 6e4;
1962
+ // 60s — letture frequenti sugli stessi file
1963
+ case "file-write":
1964
+ return 1e4;
1965
+ // 10s — scritture rapide consecutive
1966
+ case "command":
1967
+ return 3e4;
1968
+ // 30s — standard
1969
+ case "research":
1970
+ return 12e4;
1971
+ // 120s — web search e fetch ripetuti
1972
+ case "delegation":
1973
+ return 6e4;
1974
+ // 60s — delegazioni rapide
1975
+ default:
1976
+ return 3e4;
1977
+ }
1978
+ }
1895
1979
  /**
1896
1980
  * Store a new observation
1897
1981
  */
1898
1982
  async storeObservation(data) {
1899
1983
  this.validateObservationInput(data);
1984
+ const sessionId = "sdk-" + Date.now();
1985
+ const contentHash = this.generateContentHash(data.type, data.title, data.narrative);
1986
+ const dedupWindow = this.getDeduplicationWindow(data.type);
1987
+ if (isDuplicateObservation(this.db.db, contentHash, dedupWindow)) {
1988
+ logger.debug("SDK", `Osservazione duplicata scartata (${data.type}, ${dedupWindow}ms): ${data.title}`);
1989
+ return -1;
1990
+ }
1991
+ const filesRead = data.filesRead || (data.type === "file-read" ? data.files : void 0);
1992
+ const filesModified = data.filesModified || (data.type === "file-write" ? data.files : void 0);
1993
+ const discoveryTokens = Math.ceil(data.content.length / 4);
1900
1994
  const observationId = createObservation(
1901
1995
  this.db.db,
1902
- "sdk-" + Date.now(),
1996
+ sessionId,
1903
1997
  this.project,
1904
1998
  data.type,
1905
1999
  data.title,
1906
- null,
1907
- // subtitle
2000
+ data.subtitle || null,
1908
2001
  data.content,
1909
- null,
1910
- // narrative
1911
- null,
1912
- // facts
2002
+ data.narrative || null,
2003
+ data.facts || null,
1913
2004
  data.concepts?.join(", ") || null,
1914
- data.files?.join(", ") || null,
1915
- // files_read
1916
- data.files?.join(", ") || null,
1917
- // files_modified
1918
- 0
1919
- // prompt_number
2005
+ filesRead?.join(", ") || null,
2006
+ filesModified?.join(", ") || null,
2007
+ 0,
2008
+ contentHash,
2009
+ discoveryTokens
1920
2010
  );
1921
2011
  this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1922
2012
  });
@@ -1959,9 +2049,16 @@ var KiroMemorySDK = class {
1959
2049
  };
1960
2050
  }
1961
2051
  })();
2052
+ const sessionId = "sdk-" + Date.now();
2053
+ const contentHash = this.generateContentHash(data.type, data.title);
2054
+ if (isDuplicateObservation(this.db.db, contentHash)) {
2055
+ logger.debug("SDK", `Knowledge duplicata scartata: ${data.title}`);
2056
+ return -1;
2057
+ }
2058
+ const discoveryTokens = Math.ceil(data.content.length / 4);
1962
2059
  const observationId = createObservation(
1963
2060
  this.db.db,
1964
- "sdk-" + Date.now(),
2061
+ sessionId,
1965
2062
  data.project || this.project,
1966
2063
  data.knowledgeType,
1967
2064
  // type = knowledgeType
@@ -1975,9 +2072,12 @@ var KiroMemorySDK = class {
1975
2072
  // facts = metadati JSON
1976
2073
  data.concepts?.join(", ") || null,
1977
2074
  data.files?.join(", ") || null,
1978
- data.files?.join(", ") || null,
1979
- 0
2075
+ null,
2076
+ // filesModified: knowledge non modifica file
2077
+ 0,
1980
2078
  // prompt_number
2079
+ contentHash,
2080
+ discoveryTokens
1981
2081
  );
1982
2082
  this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1983
2083
  });
@@ -1993,11 +2093,11 @@ var KiroMemorySDK = class {
1993
2093
  "sdk-" + Date.now(),
1994
2094
  this.project,
1995
2095
  data.request || null,
1996
- null,
2096
+ data.investigated || null,
1997
2097
  data.learned || null,
1998
2098
  data.completed || null,
1999
2099
  data.nextSteps || null,
2000
- null
2100
+ data.notes || null
2001
2101
  );
2002
2102
  }
2003
2103
  /**
@@ -2181,7 +2281,14 @@ var KiroMemorySDK = class {
2181
2281
  }));
2182
2282
  } else {
2183
2283
  const observations = getObservationsByProject(this.db.db, this.project, 30);
2184
- items = observations.map((obs) => {
2284
+ const knowledgeTypes = new Set(KNOWLEDGE_TYPES);
2285
+ const knowledgeObs = [];
2286
+ const normalObs = [];
2287
+ for (const obs of observations) {
2288
+ if (knowledgeTypes.has(obs.type)) knowledgeObs.push(obs);
2289
+ else normalObs.push(obs);
2290
+ }
2291
+ const scoreObs = (obs) => {
2185
2292
  const signals = {
2186
2293
  semantic: 0,
2187
2294
  fts5: 0,
@@ -2189,7 +2296,6 @@ var KiroMemorySDK = class {
2189
2296
  projectMatch: projectMatchScore(obs.project, this.project)
2190
2297
  };
2191
2298
  const baseScore = computeCompositeScore(signals, CONTEXT_WEIGHTS);
2192
- const boostedScore = Math.min(1, baseScore * knowledgeTypeBoost(obs.type));
2193
2299
  return {
2194
2300
  id: obs.id,
2195
2301
  title: obs.title,
@@ -2198,17 +2304,23 @@ var KiroMemorySDK = class {
2198
2304
  project: obs.project,
2199
2305
  created_at: obs.created_at,
2200
2306
  created_at_epoch: obs.created_at_epoch,
2201
- score: boostedScore,
2307
+ score: Math.min(1, baseScore * knowledgeTypeBoost(obs.type)),
2202
2308
  signals
2203
2309
  };
2204
- });
2205
- items.sort((a, b) => b.score - a.score);
2310
+ };
2311
+ const scoredKnowledge = knowledgeObs.map(scoreObs).sort((a, b) => b.score - a.score);
2312
+ const scoredNormal = normalObs.map(scoreObs).sort((a, b) => b.score - a.score);
2313
+ items = [...scoredKnowledge, ...scoredNormal];
2206
2314
  }
2207
2315
  let tokensUsed = 0;
2316
+ const budgetItems = [];
2208
2317
  for (const item of items) {
2209
- tokensUsed += Math.ceil((item.title.length + item.content.length) / 4);
2210
- if (tokensUsed > tokenBudget) break;
2318
+ const itemTokens = Math.ceil((item.title.length + item.content.length) / 4);
2319
+ if (tokensUsed + itemTokens > tokenBudget) break;
2320
+ tokensUsed += itemTokens;
2321
+ budgetItems.push(item);
2211
2322
  }
2323
+ items = budgetItems;
2212
2324
  return {
2213
2325
  project: this.project,
2214
2326
  items,