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.
@@ -23,19 +23,28 @@ __export(Observations_exports, {
23
23
  deleteObservation: () => deleteObservation,
24
24
  getObservationsByProject: () => getObservationsByProject,
25
25
  getObservationsBySession: () => getObservationsBySession,
26
+ isDuplicateObservation: () => isDuplicateObservation,
26
27
  searchObservations: () => searchObservations,
27
28
  updateLastAccessed: () => updateLastAccessed
28
29
  });
29
30
  function escapeLikePattern(input) {
30
31
  return input.replace(/[%_\\]/g, "\\$&");
31
32
  }
32
- function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
33
+ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
34
+ if (!contentHash) return false;
35
+ const threshold = Date.now() - windowMs;
36
+ const result = db.query(
37
+ "SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch > ? LIMIT 1"
38
+ ).get(contentHash, threshold);
39
+ return !!result;
40
+ }
41
+ function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
33
42
  const now = /* @__PURE__ */ new Date();
34
43
  const result = db.run(
35
- `INSERT INTO observations
36
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
37
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
38
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
44
+ `INSERT INTO observations
45
+ (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)
46
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
47
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
39
48
  );
40
49
  return Number(result.lastInsertRowid);
41
50
  }
@@ -91,39 +100,42 @@ function consolidateObservations(db, project, options = {}) {
91
100
  if (groups.length === 0) return { merged: 0, removed: 0 };
92
101
  let totalMerged = 0;
93
102
  let totalRemoved = 0;
94
- for (const group of groups) {
95
- const obsIds = group.ids.split(",").map(Number);
96
- const placeholders = obsIds.map(() => "?").join(",");
97
- const observations = db.query(
98
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
99
- ).all(...obsIds);
100
- if (observations.length < minGroupSize) continue;
101
- if (options.dryRun) {
102
- totalMerged += 1;
103
- totalRemoved += observations.length - 1;
104
- continue;
105
- }
106
- const keeper = observations[0];
107
- const others = observations.slice(1);
108
- const uniqueTexts = /* @__PURE__ */ new Set();
109
- if (keeper.text) uniqueTexts.add(keeper.text);
110
- for (const obs of others) {
111
- if (obs.text && !uniqueTexts.has(obs.text)) {
112
- uniqueTexts.add(obs.text);
103
+ const runConsolidation = db.transaction(() => {
104
+ for (const group of groups) {
105
+ const obsIds = group.ids.split(",").map(Number);
106
+ const placeholders = obsIds.map(() => "?").join(",");
107
+ const observations = db.query(
108
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
109
+ ).all(...obsIds);
110
+ if (observations.length < minGroupSize) continue;
111
+ if (options.dryRun) {
112
+ totalMerged += 1;
113
+ totalRemoved += observations.length - 1;
114
+ continue;
115
+ }
116
+ const keeper = observations[0];
117
+ const others = observations.slice(1);
118
+ const uniqueTexts = /* @__PURE__ */ new Set();
119
+ if (keeper.text) uniqueTexts.add(keeper.text);
120
+ for (const obs of others) {
121
+ if (obs.text && !uniqueTexts.has(obs.text)) {
122
+ uniqueTexts.add(obs.text);
123
+ }
113
124
  }
125
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
126
+ db.run(
127
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
128
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
129
+ );
130
+ const removeIds = others.map((o) => o.id);
131
+ const removePlaceholders = removeIds.map(() => "?").join(",");
132
+ db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
133
+ db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
134
+ totalMerged += 1;
135
+ totalRemoved += removeIds.length;
114
136
  }
115
- const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
116
- db.run(
117
- "UPDATE observations SET text = ?, title = ? WHERE id = ?",
118
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
119
- );
120
- const removeIds = others.map((o) => o.id);
121
- const removePlaceholders = removeIds.map(() => "?").join(",");
122
- db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
123
- db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
124
- totalMerged += 1;
125
- totalRemoved += removeIds.length;
126
- }
137
+ });
138
+ runConsolidation();
127
139
  return { merged: totalMerged, removed: totalRemoved };
128
140
  }
129
141
  var init_Observations = __esm({
@@ -181,7 +193,7 @@ function searchObservationsFTS(db, query, filters = {}) {
181
193
  sql += " AND o.created_at_epoch <= ?";
182
194
  params.push(filters.dateEnd);
183
195
  }
184
- sql += " ORDER BY rank LIMIT ?";
196
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
185
197
  params.push(limit);
186
198
  const stmt = db.query(sql);
187
199
  return stmt.all(...params);
@@ -195,7 +207,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
195
207
  const safeQuery = sanitizeFTS5Query(query);
196
208
  if (!safeQuery) return [];
197
209
  let sql = `
198
- SELECT o.*, rank as fts5_rank FROM observations o
210
+ SELECT o.*, bm25(observations_fts, ${BM25_WEIGHTS}) as fts5_rank FROM observations o
199
211
  JOIN observations_fts fts ON o.id = fts.rowid
200
212
  WHERE observations_fts MATCH ?
201
213
  `;
@@ -216,7 +228,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
216
228
  sql += " AND o.created_at_epoch <= ?";
217
229
  params.push(filters.dateEnd);
218
230
  }
219
- sql += " ORDER BY rank LIMIT ?";
231
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
220
232
  params.push(limit);
221
233
  const stmt = db.query(sql);
222
234
  return stmt.all(...params);
@@ -320,11 +332,23 @@ function getProjectStats(db, project) {
320
332
  const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
321
333
  const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
322
334
  const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
335
+ const discoveryStmt = db.query(
336
+ "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
337
+ );
338
+ const discoveryTokens = discoveryStmt.get(project)?.total || 0;
339
+ const readStmt = db.query(
340
+ `SELECT COALESCE(SUM(
341
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
342
+ ), 0) as total FROM observations WHERE project = ?`
343
+ );
344
+ const readTokens = readStmt.get(project)?.total || 0;
345
+ const savings = Math.max(0, discoveryTokens - readTokens);
323
346
  return {
324
347
  observations: obsStmt.get(project)?.count || 0,
325
348
  summaries: sumStmt.get(project)?.count || 0,
326
349
  sessions: sesStmt.get(project)?.count || 0,
327
- prompts: prmStmt.get(project)?.count || 0
350
+ prompts: prmStmt.get(project)?.count || 0,
351
+ tokenEconomics: { discoveryTokens, readTokens, savings }
328
352
  };
329
353
  }
330
354
  function getStaleObservations(db, project) {
@@ -366,9 +390,11 @@ function markObservationsStale(db, ids, stale) {
366
390
  [stale ? 1 : 0, ...validIds]
367
391
  );
368
392
  }
393
+ var BM25_WEIGHTS;
369
394
  var init_Search = __esm({
370
395
  "src/services/sqlite/Search.ts"() {
371
396
  "use strict";
397
+ BM25_WEIGHTS = "10.0, 1.0, 5.0, 3.0";
372
398
  }
373
399
  });
374
400
 
@@ -1057,6 +1083,29 @@ var MigrationRunner = class {
1057
1083
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
1058
1084
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
1059
1085
  }
1086
+ },
1087
+ {
1088
+ version: 7,
1089
+ up: (db) => {
1090
+ db.run("ALTER TABLE observations ADD COLUMN content_hash TEXT");
1091
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_hash ON observations(content_hash)");
1092
+ }
1093
+ },
1094
+ {
1095
+ version: 8,
1096
+ up: (db) => {
1097
+ db.run("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
1098
+ db.run("ALTER TABLE summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
1099
+ }
1100
+ },
1101
+ {
1102
+ version: 9,
1103
+ up: (db) => {
1104
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_epoch ON observations(project, created_at_epoch DESC)");
1105
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_type ON observations(project, type)");
1106
+ db.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_epoch ON summaries(project, created_at_epoch DESC)");
1107
+ db.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_epoch ON prompts(project, created_at_epoch DESC)");
1108
+ }
1060
1109
  }
1061
1110
  ];
1062
1111
  }
@@ -1311,6 +1360,7 @@ init_Search();
1311
1360
 
1312
1361
  // src/sdk/index.ts
1313
1362
  init_Observations();
1363
+ import { createHash } from "crypto";
1314
1364
  init_Search();
1315
1365
 
1316
1366
  // src/services/search/EmbeddingService.ts
@@ -1869,31 +1919,71 @@ var KiroMemorySDK = class {
1869
1919
  logger.debug("SDK", `Embedding generation fallita per obs ${observationId}: ${error}`);
1870
1920
  }
1871
1921
  }
1922
+ /**
1923
+ * Genera content hash SHA256 per deduplicazione basata su contenuto.
1924
+ * Usa (project + type + title + narrative) come tupla di identità semantica.
1925
+ * NON include sessionId perché è unico ad ogni invocazione.
1926
+ */
1927
+ generateContentHash(type, title, narrative) {
1928
+ const payload = `${this.project}|${type}|${title}|${narrative || ""}`;
1929
+ return createHash("sha256").update(payload).digest("hex");
1930
+ }
1931
+ /**
1932
+ * Finestre di deduplicazione per tipo (ms).
1933
+ * Tipi con molte ripetizioni hanno finestre più ampie.
1934
+ */
1935
+ getDeduplicationWindow(type) {
1936
+ switch (type) {
1937
+ case "file-read":
1938
+ return 6e4;
1939
+ // 60s — letture frequenti sugli stessi file
1940
+ case "file-write":
1941
+ return 1e4;
1942
+ // 10s — scritture rapide consecutive
1943
+ case "command":
1944
+ return 3e4;
1945
+ // 30s — standard
1946
+ case "research":
1947
+ return 12e4;
1948
+ // 120s — web search e fetch ripetuti
1949
+ case "delegation":
1950
+ return 6e4;
1951
+ // 60s — delegazioni rapide
1952
+ default:
1953
+ return 3e4;
1954
+ }
1955
+ }
1872
1956
  /**
1873
1957
  * Store a new observation
1874
1958
  */
1875
1959
  async storeObservation(data) {
1876
1960
  this.validateObservationInput(data);
1961
+ const sessionId = "sdk-" + Date.now();
1962
+ const contentHash = this.generateContentHash(data.type, data.title, data.narrative);
1963
+ const dedupWindow = this.getDeduplicationWindow(data.type);
1964
+ if (isDuplicateObservation(this.db.db, contentHash, dedupWindow)) {
1965
+ logger.debug("SDK", `Osservazione duplicata scartata (${data.type}, ${dedupWindow}ms): ${data.title}`);
1966
+ return -1;
1967
+ }
1968
+ const filesRead = data.filesRead || (data.type === "file-read" ? data.files : void 0);
1969
+ const filesModified = data.filesModified || (data.type === "file-write" ? data.files : void 0);
1970
+ const discoveryTokens = Math.ceil(data.content.length / 4);
1877
1971
  const observationId = createObservation(
1878
1972
  this.db.db,
1879
- "sdk-" + Date.now(),
1973
+ sessionId,
1880
1974
  this.project,
1881
1975
  data.type,
1882
1976
  data.title,
1883
- null,
1884
- // subtitle
1977
+ data.subtitle || null,
1885
1978
  data.content,
1886
- null,
1887
- // narrative
1888
- null,
1889
- // facts
1979
+ data.narrative || null,
1980
+ data.facts || null,
1890
1981
  data.concepts?.join(", ") || null,
1891
- data.files?.join(", ") || null,
1892
- // files_read
1893
- data.files?.join(", ") || null,
1894
- // files_modified
1895
- 0
1896
- // prompt_number
1982
+ filesRead?.join(", ") || null,
1983
+ filesModified?.join(", ") || null,
1984
+ 0,
1985
+ contentHash,
1986
+ discoveryTokens
1897
1987
  );
1898
1988
  this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1899
1989
  });
@@ -1936,9 +2026,16 @@ var KiroMemorySDK = class {
1936
2026
  };
1937
2027
  }
1938
2028
  })();
2029
+ const sessionId = "sdk-" + Date.now();
2030
+ const contentHash = this.generateContentHash(data.type, data.title);
2031
+ if (isDuplicateObservation(this.db.db, contentHash)) {
2032
+ logger.debug("SDK", `Knowledge duplicata scartata: ${data.title}`);
2033
+ return -1;
2034
+ }
2035
+ const discoveryTokens = Math.ceil(data.content.length / 4);
1939
2036
  const observationId = createObservation(
1940
2037
  this.db.db,
1941
- "sdk-" + Date.now(),
2038
+ sessionId,
1942
2039
  data.project || this.project,
1943
2040
  data.knowledgeType,
1944
2041
  // type = knowledgeType
@@ -1952,9 +2049,12 @@ var KiroMemorySDK = class {
1952
2049
  // facts = metadati JSON
1953
2050
  data.concepts?.join(", ") || null,
1954
2051
  data.files?.join(", ") || null,
1955
- data.files?.join(", ") || null,
1956
- 0
2052
+ null,
2053
+ // filesModified: knowledge non modifica file
2054
+ 0,
1957
2055
  // prompt_number
2056
+ contentHash,
2057
+ discoveryTokens
1958
2058
  );
1959
2059
  this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1960
2060
  });
@@ -1970,11 +2070,11 @@ var KiroMemorySDK = class {
1970
2070
  "sdk-" + Date.now(),
1971
2071
  this.project,
1972
2072
  data.request || null,
1973
- null,
2073
+ data.investigated || null,
1974
2074
  data.learned || null,
1975
2075
  data.completed || null,
1976
2076
  data.nextSteps || null,
1977
- null
2077
+ data.notes || null
1978
2078
  );
1979
2079
  }
1980
2080
  /**
@@ -2158,7 +2258,14 @@ var KiroMemorySDK = class {
2158
2258
  }));
2159
2259
  } else {
2160
2260
  const observations = getObservationsByProject(this.db.db, this.project, 30);
2161
- items = observations.map((obs) => {
2261
+ const knowledgeTypes = new Set(KNOWLEDGE_TYPES);
2262
+ const knowledgeObs = [];
2263
+ const normalObs = [];
2264
+ for (const obs of observations) {
2265
+ if (knowledgeTypes.has(obs.type)) knowledgeObs.push(obs);
2266
+ else normalObs.push(obs);
2267
+ }
2268
+ const scoreObs = (obs) => {
2162
2269
  const signals = {
2163
2270
  semantic: 0,
2164
2271
  fts5: 0,
@@ -2166,7 +2273,6 @@ var KiroMemorySDK = class {
2166
2273
  projectMatch: projectMatchScore(obs.project, this.project)
2167
2274
  };
2168
2275
  const baseScore = computeCompositeScore(signals, CONTEXT_WEIGHTS);
2169
- const boostedScore = Math.min(1, baseScore * knowledgeTypeBoost(obs.type));
2170
2276
  return {
2171
2277
  id: obs.id,
2172
2278
  title: obs.title,
@@ -2175,17 +2281,23 @@ var KiroMemorySDK = class {
2175
2281
  project: obs.project,
2176
2282
  created_at: obs.created_at,
2177
2283
  created_at_epoch: obs.created_at_epoch,
2178
- score: boostedScore,
2284
+ score: Math.min(1, baseScore * knowledgeTypeBoost(obs.type)),
2179
2285
  signals
2180
2286
  };
2181
- });
2182
- items.sort((a, b) => b.score - a.score);
2287
+ };
2288
+ const scoredKnowledge = knowledgeObs.map(scoreObs).sort((a, b) => b.score - a.score);
2289
+ const scoredNormal = normalObs.map(scoreObs).sort((a, b) => b.score - a.score);
2290
+ items = [...scoredKnowledge, ...scoredNormal];
2183
2291
  }
2184
2292
  let tokensUsed = 0;
2293
+ const budgetItems = [];
2185
2294
  for (const item of items) {
2186
- tokensUsed += Math.ceil((item.title.length + item.content.length) / 4);
2187
- if (tokensUsed > tokenBudget) break;
2295
+ const itemTokens = Math.ceil((item.title.length + item.content.length) / 4);
2296
+ if (tokensUsed + itemTokens > tokenBudget) break;
2297
+ tokensUsed += itemTokens;
2298
+ budgetItems.push(item);
2188
2299
  }
2300
+ items = budgetItems;
2189
2301
  return {
2190
2302
  project: this.project,
2191
2303
  items,