kiro-memory 1.8.1 → 2.1.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.
@@ -99,9 +99,25 @@ function consolidateObservations(db, project, options = {}) {
99
99
  ORDER BY cnt DESC
100
100
  `).all(project, minGroupSize);
101
101
  if (groups.length === 0) return { merged: 0, removed: 0 };
102
- let totalMerged = 0;
103
- let totalRemoved = 0;
102
+ if (options.dryRun) {
103
+ let totalMerged = 0;
104
+ let totalRemoved = 0;
105
+ for (const group of groups) {
106
+ const obsIds = group.ids.split(",").map(Number);
107
+ const placeholders = obsIds.map(() => "?").join(",");
108
+ const count = db.query(
109
+ `SELECT COUNT(*) as cnt FROM observations WHERE id IN (${placeholders})`
110
+ ).get(...obsIds)?.cnt || 0;
111
+ if (count >= minGroupSize) {
112
+ totalMerged += 1;
113
+ totalRemoved += count - 1;
114
+ }
115
+ }
116
+ return { merged: totalMerged, removed: totalRemoved };
117
+ }
104
118
  const runConsolidation = db.transaction(() => {
119
+ let merged = 0;
120
+ let removed = 0;
105
121
  for (const group of groups) {
106
122
  const obsIds = group.ids.split(",").map(Number);
107
123
  const placeholders = obsIds.map(() => "?").join(",");
@@ -109,11 +125,6 @@ function consolidateObservations(db, project, options = {}) {
109
125
  `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
110
126
  ).all(...obsIds);
111
127
  if (observations.length < minGroupSize) continue;
112
- if (options.dryRun) {
113
- totalMerged += 1;
114
- totalRemoved += observations.length - 1;
115
- continue;
116
- }
117
128
  const keeper = observations[0];
118
129
  const others = observations.slice(1);
119
130
  const uniqueTexts = /* @__PURE__ */ new Set();
@@ -126,18 +137,18 @@ function consolidateObservations(db, project, options = {}) {
126
137
  const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
127
138
  db.run(
128
139
  "UPDATE observations SET text = ?, title = ? WHERE id = ?",
129
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
140
+ [consolidatedText, `[consolidated x${observations.length}] ${keeper.title}`, keeper.id]
130
141
  );
131
142
  const removeIds = others.map((o) => o.id);
132
143
  const removePlaceholders = removeIds.map(() => "?").join(",");
133
144
  db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
134
145
  db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
135
- totalMerged += 1;
136
- totalRemoved += removeIds.length;
146
+ merged += 1;
147
+ removed += removeIds.length;
137
148
  }
149
+ return { merged, removed };
138
150
  });
139
- runConsolidation();
140
- return { merged: totalMerged, removed: totalRemoved };
151
+ return runConsolidation();
141
152
  }
142
153
  var init_Observations = __esm({
143
154
  "src/services/sqlite/Observations.ts"() {
@@ -164,7 +175,7 @@ function escapeLikePattern3(input) {
164
175
  }
165
176
  function sanitizeFTS5Query(query) {
166
177
  const trimmed = query.length > 1e4 ? query.substring(0, 1e4) : query;
167
- const terms = trimmed.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
178
+ const terms = trimmed.replace(/[""\u0022]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
168
179
  return terms.join(" ");
169
180
  }
170
181
  function searchObservationsFTS(db, query, filters = {}) {
@@ -329,26 +340,38 @@ function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
329
340
  return [...before, ...self, ...after];
330
341
  }
331
342
  function getProjectStats(db, project) {
332
- const obsStmt = db.query("SELECT COUNT(*) as count FROM observations WHERE project = ?");
333
- const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
334
- const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
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;
343
+ const sql = `
344
+ WITH
345
+ obs_stats AS (
346
+ SELECT
347
+ COUNT(*) as count,
348
+ COALESCE(SUM(discovery_tokens), 0) as discovery_tokens,
349
+ COALESCE(SUM(
350
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
351
+ ), 0) as read_tokens
352
+ FROM observations WHERE project = ?
353
+ ),
354
+ sum_count AS (SELECT COUNT(*) as count FROM summaries WHERE project = ?),
355
+ ses_count AS (SELECT COUNT(*) as count FROM sessions WHERE project = ?),
356
+ prm_count AS (SELECT COUNT(*) as count FROM prompts WHERE project = ?)
357
+ SELECT
358
+ obs_stats.count as observations,
359
+ obs_stats.discovery_tokens,
360
+ obs_stats.read_tokens,
361
+ sum_count.count as summaries,
362
+ ses_count.count as sessions,
363
+ prm_count.count as prompts
364
+ FROM obs_stats, sum_count, ses_count, prm_count
365
+ `;
366
+ const row = db.query(sql).get(project, project, project, project);
367
+ const discoveryTokens = row?.discovery_tokens || 0;
368
+ const readTokens = row?.read_tokens || 0;
346
369
  const savings = Math.max(0, discoveryTokens - readTokens);
347
370
  return {
348
- observations: obsStmt.get(project)?.count || 0,
349
- summaries: sumStmt.get(project)?.count || 0,
350
- sessions: sesStmt.get(project)?.count || 0,
351
- prompts: prmStmt.get(project)?.count || 0,
371
+ observations: row?.observations || 0,
372
+ summaries: row?.summaries || 0,
373
+ sessions: row?.sessions || 0,
374
+ prompts: row?.prompts || 0,
352
375
  tokenEconomics: { discoveryTokens, readTokens, savings }
353
376
  };
354
377
  }
@@ -493,7 +516,7 @@ async function readStdin() {
493
516
  }
494
517
  resolve(JSON.parse(data));
495
518
  } catch (err) {
496
- reject(new Error(`Errore parsing stdin JSON: ${err}`));
519
+ reject(new Error(`Error parsing stdin JSON: ${err}`));
497
520
  }
498
521
  });
499
522
  process.stdin.on("error", (err) => {
@@ -551,11 +574,11 @@ async function runHook(name, handler) {
551
574
  }
552
575
  debugLog(name, "stdin", input);
553
576
  await handler(input);
554
- debugLog(name, "completato", { success: true });
577
+ debugLog(name, "completed", { success: true });
555
578
  process.exit(0);
556
579
  } catch (error) {
557
- debugLog(name, "errore", { error: String(error) });
558
- process.stderr.write(`[kiro-memory:${name}] Errore: ${error}
580
+ debugLog(name, "error", { error: String(error) });
581
+ process.stderr.write(`[kiro-memory:${name}] Error: ${error}
559
582
  `);
560
583
  process.exit(0);
561
584
  }
@@ -565,14 +588,15 @@ async function runHook(name, handler) {
565
588
  import BetterSqlite3 from "better-sqlite3";
566
589
  var Database = class {
567
590
  _db;
591
+ _stmtCache = /* @__PURE__ */ new Map();
568
592
  constructor(path, options) {
569
593
  this._db = new BetterSqlite3(path, {
570
- // better-sqlite3 crea il file di default (non serve 'create')
594
+ // better-sqlite3 creates the file by default ('create' not needed)
571
595
  readonly: options?.readwrite === false ? true : false
572
596
  });
573
597
  }
574
598
  /**
575
- * Esegui una query SQL senza risultati
599
+ * Execute a SQL query without results
576
600
  */
577
601
  run(sql, params) {
578
602
  const stmt = this._db.prepare(sql);
@@ -580,51 +604,53 @@ var Database = class {
580
604
  return result;
581
605
  }
582
606
  /**
583
- * Prepara una query con interfaccia compatibile bun:sqlite
607
+ * Prepare a query with bun:sqlite-compatible interface.
608
+ * Returns a cached prepared statement for repeated queries.
584
609
  */
585
610
  query(sql) {
586
- return new BunQueryCompat(this._db, sql);
611
+ let cached = this._stmtCache.get(sql);
612
+ if (!cached) {
613
+ cached = new BunQueryCompat(this._db, sql);
614
+ this._stmtCache.set(sql, cached);
615
+ }
616
+ return cached;
587
617
  }
588
618
  /**
589
- * Crea una transazione
619
+ * Create a transaction
590
620
  */
591
621
  transaction(fn) {
592
622
  return this._db.transaction(fn);
593
623
  }
594
624
  /**
595
- * Chiudi la connessione
625
+ * Close the connection
596
626
  */
597
627
  close() {
628
+ this._stmtCache.clear();
598
629
  this._db.close();
599
630
  }
600
631
  };
601
632
  var BunQueryCompat = class {
602
- _db;
603
- _sql;
633
+ _stmt;
604
634
  constructor(db, sql) {
605
- this._db = db;
606
- this._sql = sql;
635
+ this._stmt = db.prepare(sql);
607
636
  }
608
637
  /**
609
- * Restituisce tutte le righe
638
+ * Returns all rows
610
639
  */
611
640
  all(...params) {
612
- const stmt = this._db.prepare(this._sql);
613
- return params.length > 0 ? stmt.all(...params) : stmt.all();
641
+ return params.length > 0 ? this._stmt.all(...params) : this._stmt.all();
614
642
  }
615
643
  /**
616
- * Restituisce la prima riga o null
644
+ * Returns the first row or null
617
645
  */
618
646
  get(...params) {
619
- const stmt = this._db.prepare(this._sql);
620
- return params.length > 0 ? stmt.get(...params) : stmt.get();
647
+ return params.length > 0 ? this._stmt.get(...params) : this._stmt.get();
621
648
  }
622
649
  /**
623
- * Esegui senza risultati
650
+ * Execute without results
624
651
  */
625
652
  run(...params) {
626
- const stmt = this._db.prepare(this._sql);
627
- return params.length > 0 ? stmt.run(...params) : stmt.run();
653
+ return params.length > 0 ? this._stmt.run(...params) : this._stmt.run();
628
654
  }
629
655
  };
630
656
 
@@ -886,40 +912,62 @@ function ensureDir(dirPath) {
886
912
  var SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024;
887
913
  var SQLITE_CACHE_SIZE_PAGES = 1e4;
888
914
  var KiroMemoryDatabase = class {
889
- db;
915
+ _db;
890
916
  /**
891
- * @param dbPath - Percorso al file SQLite (default: DB_PATH)
892
- * @param skipMigrations - Se true, salta il migration runner (per hook ad alta frequenza)
917
+ * Readonly accessor for the underlying Database instance.
918
+ * Prefer using query() and run() proxy methods directly.
919
+ */
920
+ get db() {
921
+ return this._db;
922
+ }
923
+ /**
924
+ * @param dbPath - Path to the SQLite file (default: DB_PATH)
925
+ * @param skipMigrations - If true, skip the migration runner (for high-frequency hooks)
893
926
  */
894
927
  constructor(dbPath = DB_PATH, skipMigrations = false) {
895
928
  if (dbPath !== ":memory:") {
896
929
  ensureDir(DATA_DIR2);
897
930
  }
898
- this.db = new Database(dbPath, { create: true, readwrite: true });
899
- this.db.run("PRAGMA journal_mode = WAL");
900
- this.db.run("PRAGMA synchronous = NORMAL");
901
- this.db.run("PRAGMA foreign_keys = ON");
902
- this.db.run("PRAGMA temp_store = memory");
903
- this.db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
904
- this.db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
931
+ this._db = new Database(dbPath, { create: true, readwrite: true });
932
+ this._db.run("PRAGMA journal_mode = WAL");
933
+ this._db.run("PRAGMA busy_timeout = 5000");
934
+ this._db.run("PRAGMA synchronous = NORMAL");
935
+ this._db.run("PRAGMA foreign_keys = ON");
936
+ this._db.run("PRAGMA temp_store = memory");
937
+ this._db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
938
+ this._db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
905
939
  if (!skipMigrations) {
906
- const migrationRunner = new MigrationRunner(this.db);
940
+ const migrationRunner = new MigrationRunner(this._db);
907
941
  migrationRunner.runAllMigrations();
908
942
  }
909
943
  }
910
944
  /**
911
- * Esegue una funzione all'interno di una transazione atomica.
912
- * Se fn() lancia un errore, la transazione viene annullata automaticamente.
945
+ * Prepare a query (delegates to underlying Database).
946
+ * Proxy method to avoid ctx.db.db.query() double access.
947
+ */
948
+ query(sql) {
949
+ return this._db.query(sql);
950
+ }
951
+ /**
952
+ * Execute a SQL statement without results (delegates to underlying Database).
953
+ * Proxy method to avoid ctx.db.db.run() double access.
954
+ */
955
+ run(sql, params) {
956
+ return this._db.run(sql, params);
957
+ }
958
+ /**
959
+ * Executes a function within an atomic transaction.
960
+ * If fn() throws an error, the transaction is automatically rolled back.
913
961
  */
914
962
  withTransaction(fn) {
915
- const transaction = this.db.transaction(fn);
916
- return transaction(this.db);
963
+ const transaction = this._db.transaction(fn);
964
+ return transaction(this._db);
917
965
  }
918
966
  /**
919
967
  * Close the database connection
920
968
  */
921
969
  close() {
922
- this.db.close();
970
+ this._db.close();
923
971
  }
924
972
  };
925
973
  var MigrationRunner = class {
@@ -1414,8 +1462,8 @@ var EmbeddingService = class {
1414
1462
  initialized = false;
1415
1463
  initializing = null;
1416
1464
  /**
1417
- * Inizializza il servizio di embedding.
1418
- * Tenta fastembed, poi @huggingface/transformers, poi fallback a null.
1465
+ * Initialize the embedding service.
1466
+ * Tries fastembed, then @huggingface/transformers, then fallback to null.
1419
1467
  */
1420
1468
  async initialize() {
1421
1469
  if (this.initialized) return this.provider !== null;
@@ -1436,11 +1484,11 @@ var EmbeddingService = class {
1436
1484
  });
1437
1485
  this.provider = "fastembed";
1438
1486
  this.initialized = true;
1439
- logger.info("EMBEDDING", "Inizializzato con fastembed (BGE-small-en-v1.5)");
1487
+ logger.info("EMBEDDING", "Initialized with fastembed (BGE-small-en-v1.5)");
1440
1488
  return true;
1441
1489
  }
1442
1490
  } catch (error) {
1443
- logger.debug("EMBEDDING", `fastembed non disponibile: ${error}`);
1491
+ logger.debug("EMBEDDING", `fastembed not available: ${error}`);
1444
1492
  }
1445
1493
  try {
1446
1494
  const transformers = await import("@huggingface/transformers");
@@ -1451,20 +1499,20 @@ var EmbeddingService = class {
1451
1499
  });
1452
1500
  this.provider = "transformers";
1453
1501
  this.initialized = true;
1454
- logger.info("EMBEDDING", "Inizializzato con @huggingface/transformers (all-MiniLM-L6-v2)");
1502
+ logger.info("EMBEDDING", "Initialized with @huggingface/transformers (all-MiniLM-L6-v2)");
1455
1503
  return true;
1456
1504
  }
1457
1505
  } catch (error) {
1458
- logger.debug("EMBEDDING", `@huggingface/transformers non disponibile: ${error}`);
1506
+ logger.debug("EMBEDDING", `@huggingface/transformers not available: ${error}`);
1459
1507
  }
1460
1508
  this.provider = null;
1461
1509
  this.initialized = true;
1462
- logger.warn("EMBEDDING", "Nessun provider embedding disponibile, ricerca semantica disabilitata");
1510
+ logger.warn("EMBEDDING", "No embedding provider available, semantic search disabled");
1463
1511
  return false;
1464
1512
  }
1465
1513
  /**
1466
- * Genera embedding per un singolo testo.
1467
- * Ritorna Float32Array con 384 dimensioni, o null se non disponibile.
1514
+ * Generate embedding for a single text.
1515
+ * Returns Float32Array with 384 dimensions, or null if not available.
1468
1516
  */
1469
1517
  async embed(text) {
1470
1518
  if (!this.initialized) await this.initialize();
@@ -1477,46 +1525,111 @@ var EmbeddingService = class {
1477
1525
  return await this._embedTransformers(truncated);
1478
1526
  }
1479
1527
  } catch (error) {
1480
- logger.error("EMBEDDING", `Errore generazione embedding: ${error}`);
1528
+ logger.error("EMBEDDING", `Error generating embedding: ${error}`);
1481
1529
  }
1482
1530
  return null;
1483
1531
  }
1484
1532
  /**
1485
- * Genera embeddings in batch.
1533
+ * Generate embeddings in batch.
1534
+ * Uses native batch support when available (fastembed, transformers),
1535
+ * falls back to serial processing on batch failure.
1486
1536
  */
1487
1537
  async embedBatch(texts) {
1488
1538
  if (!this.initialized) await this.initialize();
1489
1539
  if (!this.provider || !this.model) return texts.map(() => null);
1490
- const results = [];
1491
- for (const text of texts) {
1492
- try {
1493
- const embedding = await this.embed(text);
1494
- results.push(embedding);
1495
- } catch {
1496
- results.push(null);
1540
+ if (texts.length === 0) return [];
1541
+ const truncated = texts.map((t) => t.substring(0, 2e3));
1542
+ try {
1543
+ if (this.provider === "fastembed") {
1544
+ return await this._embedBatchFastembed(truncated);
1545
+ } else if (this.provider === "transformers") {
1546
+ return await this._embedBatchTransformers(truncated);
1497
1547
  }
1548
+ } catch (error) {
1549
+ logger.warn("EMBEDDING", `Batch embedding failed, falling back to serial: ${error}`);
1498
1550
  }
1499
- return results;
1551
+ return this._embedBatchSerial(truncated);
1500
1552
  }
1501
1553
  /**
1502
- * Verifica se il servizio è disponibile.
1554
+ * Check if the service is available.
1503
1555
  */
1504
1556
  isAvailable() {
1505
1557
  return this.initialized && this.provider !== null;
1506
1558
  }
1507
1559
  /**
1508
- * Nome del provider attivo.
1560
+ * Name of the active provider.
1509
1561
  */
1510
1562
  getProvider() {
1511
1563
  return this.provider;
1512
1564
  }
1513
1565
  /**
1514
- * Dimensioni del vettore embedding.
1566
+ * Embedding vector dimensions.
1515
1567
  */
1516
1568
  getDimensions() {
1517
1569
  return 384;
1518
1570
  }
1519
- // --- Provider specifici ---
1571
+ // --- Batch implementations ---
1572
+ /**
1573
+ * Native batch embedding with fastembed.
1574
+ * FlagEmbedding.embed() accepts string[] and returns an async iterable of batches.
1575
+ */
1576
+ async _embedBatchFastembed(texts) {
1577
+ const results = [];
1578
+ const embeddings = this.model.embed(texts, texts.length);
1579
+ for await (const batch of embeddings) {
1580
+ if (batch) {
1581
+ for (const vec of batch) {
1582
+ results.push(vec instanceof Float32Array ? vec : new Float32Array(vec));
1583
+ }
1584
+ }
1585
+ }
1586
+ while (results.length < texts.length) {
1587
+ results.push(null);
1588
+ }
1589
+ return results;
1590
+ }
1591
+ /**
1592
+ * Batch embedding with @huggingface/transformers pipeline.
1593
+ * The pipeline accepts string[] and returns a Tensor with shape [N, dims].
1594
+ */
1595
+ async _embedBatchTransformers(texts) {
1596
+ const output = await this.model(texts, {
1597
+ pooling: "mean",
1598
+ normalize: true
1599
+ });
1600
+ if (!output?.data) {
1601
+ return texts.map(() => null);
1602
+ }
1603
+ const dims = this.getDimensions();
1604
+ const data = output.data instanceof Float32Array ? output.data : new Float32Array(output.data);
1605
+ const results = [];
1606
+ for (let i = 0; i < texts.length; i++) {
1607
+ const offset = i * dims;
1608
+ if (offset + dims <= data.length) {
1609
+ results.push(data.slice(offset, offset + dims));
1610
+ } else {
1611
+ results.push(null);
1612
+ }
1613
+ }
1614
+ return results;
1615
+ }
1616
+ /**
1617
+ * Serial fallback: embed texts one at a time.
1618
+ * Used when native batch fails.
1619
+ */
1620
+ async _embedBatchSerial(texts) {
1621
+ const results = [];
1622
+ for (const text of texts) {
1623
+ try {
1624
+ const embedding = await this.embed(text);
1625
+ results.push(embedding);
1626
+ } catch {
1627
+ results.push(null);
1628
+ }
1629
+ }
1630
+ return results;
1631
+ }
1632
+ // --- Single-text provider implementations ---
1520
1633
  async _embedFastembed(text) {
1521
1634
  const embeddings = this.model.embed([text], 1);
1522
1635
  for await (const batch of embeddings) {
@@ -1547,17 +1660,21 @@ function getEmbeddingService() {
1547
1660
  }
1548
1661
 
1549
1662
  // src/services/search/VectorSearch.ts
1663
+ var DEFAULT_MAX_CANDIDATES = 2e3;
1550
1664
  function cosineSimilarity(a, b) {
1551
- if (a.length !== b.length) return 0;
1665
+ const len = a.length;
1666
+ if (len !== b.length) return 0;
1552
1667
  let dotProduct = 0;
1553
1668
  let normA = 0;
1554
1669
  let normB = 0;
1555
- for (let i = 0; i < a.length; i++) {
1556
- dotProduct += a[i] * b[i];
1557
- normA += a[i] * a[i];
1558
- normB += b[i] * b[i];
1559
- }
1560
- const denominator = Math.sqrt(normA) * Math.sqrt(normB);
1670
+ for (let i = 0; i < len; i++) {
1671
+ const ai = a[i];
1672
+ const bi = b[i];
1673
+ dotProduct += ai * bi;
1674
+ normA += ai * ai;
1675
+ normB += bi * bi;
1676
+ }
1677
+ const denominator = Math.sqrt(normA * normB);
1561
1678
  if (denominator === 0) return 0;
1562
1679
  return dotProduct / denominator;
1563
1680
  }
@@ -1570,23 +1687,36 @@ function bufferToFloat32(buf) {
1570
1687
  }
1571
1688
  var VectorSearch = class {
1572
1689
  /**
1573
- * Ricerca semantica: calcola cosine similarity tra query e tutti gli embeddings.
1690
+ * Semantic search with SQL pre-filtering for scalability.
1691
+ *
1692
+ * 2-phase strategy:
1693
+ * 1. SQL pre-filters by project + sorts by recency (loads max N candidates)
1694
+ * 2. JS computes cosine similarity only on filtered candidates
1695
+ *
1696
+ * With 50k observations and maxCandidates=2000, loads only ~4% of data.
1574
1697
  */
1575
1698
  async search(db, queryEmbedding, options = {}) {
1576
1699
  const limit = options.limit || 10;
1577
1700
  const threshold = options.threshold || 0.3;
1701
+ const maxCandidates = options.maxCandidates || DEFAULT_MAX_CANDIDATES;
1578
1702
  try {
1579
- let sql = `
1703
+ const conditions = [];
1704
+ const params = [];
1705
+ if (options.project) {
1706
+ conditions.push("o.project = ?");
1707
+ params.push(options.project);
1708
+ }
1709
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1710
+ const sql = `
1580
1711
  SELECT e.observation_id, e.embedding,
1581
1712
  o.title, o.text, o.type, o.project, o.created_at, o.created_at_epoch
1582
1713
  FROM observation_embeddings e
1583
1714
  JOIN observations o ON o.id = e.observation_id
1715
+ ${whereClause}
1716
+ ORDER BY o.created_at_epoch DESC
1717
+ LIMIT ?
1584
1718
  `;
1585
- const params = [];
1586
- if (options.project) {
1587
- sql += " WHERE o.project = ?";
1588
- params.push(options.project);
1589
- }
1719
+ params.push(maxCandidates);
1590
1720
  const rows = db.query(sql).all(...params);
1591
1721
  const scored = [];
1592
1722
  for (const row of rows) {
@@ -1607,14 +1737,15 @@ var VectorSearch = class {
1607
1737
  }
1608
1738
  }
1609
1739
  scored.sort((a, b) => b.similarity - a.similarity);
1740
+ logger.debug("VECTOR", `Search: ${rows.length} candidates \u2192 ${scored.length} above threshold \u2192 ${Math.min(scored.length, limit)} results`);
1610
1741
  return scored.slice(0, limit);
1611
1742
  } catch (error) {
1612
- logger.error("VECTOR", `Errore ricerca vettoriale: ${error}`);
1743
+ logger.error("VECTOR", `Vector search error: ${error}`);
1613
1744
  return [];
1614
1745
  }
1615
1746
  }
1616
1747
  /**
1617
- * Salva embedding per un'osservazione.
1748
+ * Store embedding for an observation.
1618
1749
  */
1619
1750
  async storeEmbedding(db, observationId, embedding, model) {
1620
1751
  try {
@@ -1630,18 +1761,18 @@ var VectorSearch = class {
1630
1761
  embedding.length,
1631
1762
  (/* @__PURE__ */ new Date()).toISOString()
1632
1763
  );
1633
- logger.debug("VECTOR", `Embedding salvato per osservazione ${observationId}`);
1764
+ logger.debug("VECTOR", `Embedding saved for observation ${observationId}`);
1634
1765
  } catch (error) {
1635
- logger.error("VECTOR", `Errore salvataggio embedding: ${error}`);
1766
+ logger.error("VECTOR", `Error saving embedding: ${error}`);
1636
1767
  }
1637
1768
  }
1638
1769
  /**
1639
- * Genera embeddings per osservazioni che non li hanno ancora.
1770
+ * Generate embeddings for observations that don't have them yet.
1640
1771
  */
1641
1772
  async backfillEmbeddings(db, batchSize = 50) {
1642
1773
  const embeddingService2 = getEmbeddingService();
1643
1774
  if (!await embeddingService2.initialize()) {
1644
- logger.warn("VECTOR", "Embedding service non disponibile, backfill saltato");
1775
+ logger.warn("VECTOR", "Embedding service not available, backfill skipped");
1645
1776
  return 0;
1646
1777
  }
1647
1778
  const rows = db.query(`
@@ -1667,11 +1798,11 @@ var VectorSearch = class {
1667
1798
  count++;
1668
1799
  }
1669
1800
  }
1670
- logger.info("VECTOR", `Backfill completato: ${count}/${rows.length} embeddings generati`);
1801
+ logger.info("VECTOR", `Backfill completed: ${count}/${rows.length} embeddings generated`);
1671
1802
  return count;
1672
1803
  }
1673
1804
  /**
1674
- * Statistiche sugli embeddings.
1805
+ * Embedding statistics.
1675
1806
  */
1676
1807
  getStats(db) {
1677
1808
  try {
@@ -1698,21 +1829,21 @@ function getVectorSearch() {
1698
1829
  var HybridSearch = class {
1699
1830
  embeddingInitialized = false;
1700
1831
  /**
1701
- * Inizializza il servizio di embedding (lazy, non bloccante)
1832
+ * Initialize the embedding service (lazy, non-blocking)
1702
1833
  */
1703
1834
  async initialize() {
1704
1835
  try {
1705
1836
  const embeddingService2 = getEmbeddingService();
1706
1837
  await embeddingService2.initialize();
1707
1838
  this.embeddingInitialized = embeddingService2.isAvailable();
1708
- logger.info("SEARCH", `HybridSearch inizializzato (embedding: ${this.embeddingInitialized ? "attivo" : "disattivato"})`);
1839
+ logger.info("SEARCH", `HybridSearch initialized (embedding: ${this.embeddingInitialized ? "active" : "disabled"})`);
1709
1840
  } catch (error) {
1710
- logger.warn("SEARCH", "Inizializzazione embedding fallita, uso solo FTS5", {}, error);
1841
+ logger.warn("SEARCH", "Embedding initialization failed, using only FTS5", {}, error);
1711
1842
  this.embeddingInitialized = false;
1712
1843
  }
1713
1844
  }
1714
1845
  /**
1715
- * Ricerca ibrida con scoring a 4 segnali
1846
+ * Hybrid search with 4-signal scoring
1716
1847
  */
1717
1848
  async search(db, query, options = {}) {
1718
1849
  const limit = options.limit || 10;
@@ -1728,7 +1859,7 @@ var HybridSearch = class {
1728
1859
  const vectorResults = await vectorSearch2.search(db, queryEmbedding, {
1729
1860
  project: options.project,
1730
1861
  limit: limit * 2,
1731
- // Prendiamo piu risultati per il ranking
1862
+ // Fetch more results for ranking
1732
1863
  threshold: 0.3
1733
1864
  });
1734
1865
  for (const hit of vectorResults) {
@@ -1745,10 +1876,10 @@ var HybridSearch = class {
1745
1876
  source: "vector"
1746
1877
  });
1747
1878
  }
1748
- logger.debug("SEARCH", `Vector search: ${vectorResults.length} risultati`);
1879
+ logger.debug("SEARCH", `Vector search: ${vectorResults.length} results`);
1749
1880
  }
1750
1881
  } catch (error) {
1751
- logger.warn("SEARCH", "Ricerca vettoriale fallita, uso solo keyword", {}, error);
1882
+ logger.warn("SEARCH", "Vector search failed, using only keyword", {}, error);
1752
1883
  }
1753
1884
  }
1754
1885
  try {
@@ -1778,9 +1909,9 @@ var HybridSearch = class {
1778
1909
  });
1779
1910
  }
1780
1911
  }
1781
- logger.debug("SEARCH", `Keyword search: ${keywordResults.length} risultati`);
1912
+ logger.debug("SEARCH", `Keyword search: ${keywordResults.length} results`);
1782
1913
  } catch (error) {
1783
- logger.error("SEARCH", "Ricerca keyword fallita", {}, error);
1914
+ logger.error("SEARCH", "Keyword search failed", {}, error);
1784
1915
  }
1785
1916
  if (rawItems.size === 0) return [];
1786
1917
  const allFTS5Ranks = Array.from(rawItems.values()).filter((item) => item.fts5Rank !== null).map((item) => item.fts5Rank);
@@ -1868,33 +1999,33 @@ var KiroMemorySDK = class {
1868
1999
  };
1869
2000
  }
1870
2001
  /**
1871
- * Valida input per storeObservation
2002
+ * Validate input for storeObservation
1872
2003
  */
1873
2004
  validateObservationInput(data) {
1874
2005
  if (!data.type || typeof data.type !== "string" || data.type.length > 100) {
1875
- throw new Error("type \xE8 obbligatorio (stringa, max 100 caratteri)");
2006
+ throw new Error("type is required (string, max 100 chars)");
1876
2007
  }
1877
2008
  if (!data.title || typeof data.title !== "string" || data.title.length > 500) {
1878
- throw new Error("title \xE8 obbligatorio (stringa, max 500 caratteri)");
2009
+ throw new Error("title is required (string, max 500 chars)");
1879
2010
  }
1880
2011
  if (!data.content || typeof data.content !== "string" || data.content.length > 1e5) {
1881
- throw new Error("content \xE8 obbligatorio (stringa, max 100KB)");
2012
+ throw new Error("content is required (string, max 100KB)");
1882
2013
  }
1883
2014
  }
1884
2015
  /**
1885
- * Valida input per storeSummary
2016
+ * Validate input for storeSummary
1886
2017
  */
1887
2018
  validateSummaryInput(data) {
1888
2019
  const MAX = 5e4;
1889
2020
  for (const [key, val] of Object.entries(data)) {
1890
2021
  if (val !== void 0 && val !== null) {
1891
- if (typeof val !== "string") throw new Error(`${key} deve essere una stringa`);
1892
- if (val.length > MAX) throw new Error(`${key} troppo grande (max 50KB)`);
2022
+ if (typeof val !== "string") throw new Error(`${key} must be a string`);
2023
+ if (val.length > MAX) throw new Error(`${key} too large (max 50KB)`);
1893
2024
  }
1894
2025
  }
1895
2026
  }
1896
2027
  /**
1897
- * Genera e salva embedding per un'osservazione (fire-and-forget, non blocca)
2028
+ * Generate and store embedding for an observation (fire-and-forget, non-blocking)
1898
2029
  */
1899
2030
  async generateEmbeddingAsync(observationId, title, content, concepts) {
1900
2031
  try {
@@ -1914,39 +2045,39 @@ var KiroMemorySDK = class {
1914
2045
  );
1915
2046
  }
1916
2047
  } catch (error) {
1917
- logger.debug("SDK", `Embedding generation fallita per obs ${observationId}: ${error}`);
2048
+ logger.debug("SDK", `Embedding generation failed for obs ${observationId}: ${error}`);
1918
2049
  }
1919
2050
  }
1920
2051
  /**
1921
- * Genera content hash SHA256 per deduplicazione basata su contenuto.
1922
- * Usa (project + type + title + narrative) come tupla di identità semantica.
1923
- * NON include sessionId perché è unico ad ogni invocazione.
2052
+ * Generate SHA256 content hash for content-based deduplication.
2053
+ * Uses (project + type + title + narrative) as semantic identity tuple.
2054
+ * Does NOT include sessionId since it's unique per invocation.
1924
2055
  */
1925
2056
  generateContentHash(type, title, narrative) {
1926
2057
  const payload = `${this.project}|${type}|${title}|${narrative || ""}`;
1927
2058
  return createHash("sha256").update(payload).digest("hex");
1928
2059
  }
1929
2060
  /**
1930
- * Finestre di deduplicazione per tipo (ms).
1931
- * Tipi con molte ripetizioni hanno finestre più ampie.
2061
+ * Deduplication windows per type (ms).
2062
+ * Types with many repetitions have wider windows.
1932
2063
  */
1933
2064
  getDeduplicationWindow(type) {
1934
2065
  switch (type) {
1935
2066
  case "file-read":
1936
2067
  return 6e4;
1937
- // 60s — letture frequenti sugli stessi file
2068
+ // 60s — frequent reads on the same files
1938
2069
  case "file-write":
1939
2070
  return 1e4;
1940
- // 10s — scritture rapide consecutive
2071
+ // 10s — rapid consecutive writes
1941
2072
  case "command":
1942
2073
  return 3e4;
1943
2074
  // 30s — standard
1944
2075
  case "research":
1945
2076
  return 12e4;
1946
- // 120s — web search e fetch ripetuti
2077
+ // 120s — repeated web search and fetch
1947
2078
  case "delegation":
1948
2079
  return 6e4;
1949
- // 60s — delegazioni rapide
2080
+ // 60s — rapid delegations
1950
2081
  default:
1951
2082
  return 3e4;
1952
2083
  }
@@ -1960,7 +2091,7 @@ var KiroMemorySDK = class {
1960
2091
  const contentHash = this.generateContentHash(data.type, data.title, data.narrative);
1961
2092
  const dedupWindow = this.getDeduplicationWindow(data.type);
1962
2093
  if (isDuplicateObservation(this.db.db, contentHash, dedupWindow)) {
1963
- logger.debug("SDK", `Osservazione duplicata scartata (${data.type}, ${dedupWindow}ms): ${data.title}`);
2094
+ logger.debug("SDK", `Duplicate observation discarded (${data.type}, ${dedupWindow}ms): ${data.title}`);
1964
2095
  return -1;
1965
2096
  }
1966
2097
  const filesRead = data.filesRead || (data.type === "file-read" ? data.files : void 0);
@@ -1988,12 +2119,12 @@ var KiroMemorySDK = class {
1988
2119
  return observationId;
1989
2120
  }
1990
2121
  /**
1991
- * Salva conoscenza strutturata (constraint, decision, heuristic, rejected).
1992
- * Usa il campo `type` per il knowledgeType e `facts` per i metadati JSON.
2122
+ * Store structured knowledge (constraint, decision, heuristic, rejected).
2123
+ * Uses the `type` field for knowledgeType and `facts` for JSON metadata.
1993
2124
  */
1994
2125
  async storeKnowledge(data) {
1995
2126
  if (!KNOWLEDGE_TYPES.includes(data.knowledgeType)) {
1996
- throw new Error(`knowledgeType non valido: ${data.knowledgeType}. Valori ammessi: ${KNOWLEDGE_TYPES.join(", ")}`);
2127
+ throw new Error(`Invalid knowledgeType: ${data.knowledgeType}. Allowed values: ${KNOWLEDGE_TYPES.join(", ")}`);
1997
2128
  }
1998
2129
  this.validateObservationInput({ type: data.knowledgeType, title: data.title, content: data.content });
1999
2130
  const metadata = (() => {
@@ -2025,9 +2156,9 @@ var KiroMemorySDK = class {
2025
2156
  }
2026
2157
  })();
2027
2158
  const sessionId = "sdk-" + Date.now();
2028
- const contentHash = this.generateContentHash(data.type, data.title);
2159
+ const contentHash = this.generateContentHash(data.knowledgeType, data.title);
2029
2160
  if (isDuplicateObservation(this.db.db, contentHash)) {
2030
- logger.debug("SDK", `Knowledge duplicata scartata: ${data.title}`);
2161
+ logger.debug("SDK", `Duplicate knowledge discarded: ${data.title}`);
2031
2162
  return -1;
2032
2163
  }
2033
2164
  const discoveryTokens = Math.ceil(data.content.length / 4);
@@ -2044,11 +2175,11 @@ var KiroMemorySDK = class {
2044
2175
  null,
2045
2176
  // narrative
2046
2177
  JSON.stringify(metadata),
2047
- // facts = metadati JSON
2178
+ // facts = JSON metadata
2048
2179
  data.concepts?.join(", ") || null,
2049
2180
  data.files?.join(", ") || null,
2050
2181
  null,
2051
- // filesModified: knowledge non modifica file
2182
+ // filesModified: knowledge doesn't modify files
2052
2183
  0,
2053
2184
  // prompt_number
2054
2185
  contentHash,
@@ -2159,8 +2290,8 @@ var KiroMemorySDK = class {
2159
2290
  return this.project;
2160
2291
  }
2161
2292
  /**
2162
- * Ricerca ibrida: vector search + keyword FTS5
2163
- * Richiede inizializzazione HybridSearch (embedding service)
2293
+ * Hybrid search: vector search + keyword FTS5
2294
+ * Requires HybridSearch initialization (embedding service)
2164
2295
  */
2165
2296
  async hybridSearch(query, options = {}) {
2166
2297
  const hybridSearch2 = getHybridSearch();
@@ -2170,8 +2301,8 @@ var KiroMemorySDK = class {
2170
2301
  });
2171
2302
  }
2172
2303
  /**
2173
- * Ricerca solo semantica (vector search)
2174
- * Ritorna risultati basati su similarità coseno con gli embeddings
2304
+ * Semantic-only search (vector search)
2305
+ * Returns results based on cosine similarity with embeddings
2175
2306
  */
2176
2307
  async semanticSearch(query, options = {}) {
2177
2308
  const embeddingService2 = getEmbeddingService();
@@ -2206,21 +2337,21 @@ var KiroMemorySDK = class {
2206
2337
  }));
2207
2338
  }
2208
2339
  /**
2209
- * Genera embeddings per osservazioni che non li hanno ancora
2340
+ * Generate embeddings for observations that don't have them yet
2210
2341
  */
2211
2342
  async backfillEmbeddings(batchSize = 50) {
2212
2343
  const vectorSearch2 = getVectorSearch();
2213
2344
  return vectorSearch2.backfillEmbeddings(this.db.db, batchSize);
2214
2345
  }
2215
2346
  /**
2216
- * Statistiche sugli embeddings nel database
2347
+ * Embedding statistics in the database
2217
2348
  */
2218
2349
  getEmbeddingStats() {
2219
2350
  const vectorSearch2 = getVectorSearch();
2220
2351
  return vectorSearch2.getStats(this.db.db);
2221
2352
  }
2222
2353
  /**
2223
- * Inizializza il servizio di embedding (lazy, chiamare prima di hybridSearch)
2354
+ * Initialize the embedding service (lazy, call before hybridSearch)
2224
2355
  */
2225
2356
  async initializeEmbeddings() {
2226
2357
  const hybridSearch2 = getHybridSearch();
@@ -2228,10 +2359,10 @@ var KiroMemorySDK = class {
2228
2359
  return getEmbeddingService().isAvailable();
2229
2360
  }
2230
2361
  /**
2231
- * Contesto smart con ranking a 4 segnali e budget token.
2362
+ * Smart context with 4-signal ranking and token budget.
2232
2363
  *
2233
- * Se query presente: usa HybridSearch con SEARCH_WEIGHTS.
2234
- * Se senza query: ranking per recency + project match (CONTEXT_WEIGHTS).
2364
+ * If query present: uses HybridSearch with SEARCH_WEIGHTS.
2365
+ * If no query: ranking by recency + project match (CONTEXT_WEIGHTS).
2235
2366
  */
2236
2367
  async getSmartContext(options = {}) {
2237
2368
  const tokenBudget = options.tokenBudget || parseInt(process.env.KIRO_MEMORY_CONTEXT_TOKENS || "0", 10) || 2e3;
@@ -2305,8 +2436,8 @@ var KiroMemorySDK = class {
2305
2436
  };
2306
2437
  }
2307
2438
  /**
2308
- * Rileva osservazioni stale (file modificati dopo la creazione) e le marca nel DB.
2309
- * Ritorna il numero di osservazioni marcate come stale.
2439
+ * Detect stale observations (files modified after creation) and mark them in DB.
2440
+ * Returns the number of observations marked as stale.
2310
2441
  */
2311
2442
  async detectStaleObservations() {
2312
2443
  const staleObs = getStaleObservations(this.db.db, this.project);
@@ -2317,14 +2448,14 @@ var KiroMemorySDK = class {
2317
2448
  return staleObs.length;
2318
2449
  }
2319
2450
  /**
2320
- * Consolida osservazioni duplicate sullo stesso file e tipo.
2321
- * Raggruppa per (project, type, files_modified), mantiene la piu recente.
2451
+ * Consolidate duplicate observations on the same file and type.
2452
+ * Groups by (project, type, files_modified), keeps the most recent.
2322
2453
  */
2323
2454
  async consolidateObservations(options = {}) {
2324
2455
  return consolidateObservations(this.db.db, this.project, options);
2325
2456
  }
2326
2457
  /**
2327
- * Statistiche decay: totale, stale, mai accedute, accedute di recente.
2458
+ * Decay statistics: total, stale, never accessed, recently accessed.
2328
2459
  */
2329
2460
  async getDecayStats() {
2330
2461
  const total = this.db.db.query(
@@ -2343,8 +2474,8 @@ var KiroMemorySDK = class {
2343
2474
  return { total, stale, neverAccessed, recentlyAccessed };
2344
2475
  }
2345
2476
  /**
2346
- * Crea un checkpoint strutturato per resume sessione.
2347
- * Salva automaticamente un context_snapshot con le ultime 10 osservazioni.
2477
+ * Create a structured checkpoint for session resume.
2478
+ * Automatically saves a context_snapshot with the last 10 observations.
2348
2479
  */
2349
2480
  async createCheckpoint(sessionId, data) {
2350
2481
  const recentObs = getObservationsByProject(this.db.db, this.project, 10);
@@ -2361,21 +2492,21 @@ var KiroMemorySDK = class {
2361
2492
  });
2362
2493
  }
2363
2494
  /**
2364
- * Recupera l'ultimo checkpoint di una sessione specifica.
2495
+ * Retrieve the latest checkpoint of a specific session.
2365
2496
  */
2366
2497
  async getCheckpoint(sessionId) {
2367
2498
  return getLatestCheckpoint(this.db.db, sessionId);
2368
2499
  }
2369
2500
  /**
2370
- * Recupera l'ultimo checkpoint per il progetto corrente.
2371
- * Utile per resume automatico senza specificare session ID.
2501
+ * Retrieve the latest checkpoint for the current project.
2502
+ * Useful for automatic resume without specifying session ID.
2372
2503
  */
2373
2504
  async getLatestProjectCheckpoint() {
2374
2505
  return getLatestCheckpointByProject(this.db.db, this.project);
2375
2506
  }
2376
2507
  /**
2377
- * Genera un report di attività per il progetto corrente.
2378
- * Aggrega osservazioni, sessioni, summaries e file per un periodo temporale.
2508
+ * Generate an activity report for the current project.
2509
+ * Aggregates observations, sessions, summaries and files for a time period.
2379
2510
  */
2380
2511
  async generateReport(options) {
2381
2512
  const now = /* @__PURE__ */ new Date();