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
  }
@@ -497,7 +520,7 @@ async function readStdin() {
497
520
  }
498
521
  resolve(JSON.parse(data));
499
522
  } catch (err) {
500
- reject(new Error(`Errore parsing stdin JSON: ${err}`));
523
+ reject(new Error(`Error parsing stdin JSON: ${err}`));
501
524
  }
502
525
  });
503
526
  process.stdin.on("error", (err) => {
@@ -523,17 +546,17 @@ function formatSmartContext(data) {
523
546
  const budget = data.tokenBudget || parseInt(process.env.KIRO_MEMORY_CONTEXT_TOKENS || "0", 10) || 2e3;
524
547
  let output = "";
525
548
  let tokensUsed = 0;
526
- const header = "# Kiro Memory: Contesto Sessioni Precedenti\n\n";
549
+ const header = "# Kiro Memory: Previous Sessions Context\n\n";
527
550
  tokensUsed += estimateTokens(header);
528
551
  output += header;
529
552
  if (data.summaries && data.summaries.length > 0) {
530
- let sumSection = "## Sessioni Precedenti\n\n";
553
+ let sumSection = "## Previous Sessions\n\n";
531
554
  for (const sum of data.summaries.slice(0, 3)) {
532
- if (sum.learned) sumSection += `- **Appreso**: ${sum.learned}
555
+ if (sum.learned) sumSection += `- **Learned**: ${sum.learned}
533
556
  `;
534
- if (sum.completed) sumSection += `- **Completato**: ${sum.completed}
557
+ if (sum.completed) sumSection += `- **Completed**: ${sum.completed}
535
558
  `;
536
- if (sum.next_steps) sumSection += `- **Prossimi passi**: ${sum.next_steps}
559
+ if (sum.next_steps) sumSection += `- **Next steps**: ${sum.next_steps}
537
560
  `;
538
561
  sumSection += "\n";
539
562
  }
@@ -541,7 +564,7 @@ function formatSmartContext(data) {
541
564
  output += sumSection;
542
565
  }
543
566
  if (data.items && data.items.length > 0) {
544
- let obsSection = "## Osservazioni Rilevanti\n\n";
567
+ let obsSection = "## Relevant Observations\n\n";
545
568
  tokensUsed += estimateTokens(obsSection);
546
569
  const sorted = [...data.items].sort((a, b) => b.score - a.score);
547
570
  for (const item of sorted) {
@@ -560,7 +583,7 @@ function formatSmartContext(data) {
560
583
  output += obsSection;
561
584
  }
562
585
  const footer = `
563
- > Progetto: ${data.project} | Items: ${data.items?.length || 0} | Token usati: ~${tokensUsed}/${budget}
586
+ > Project: ${data.project} | Items: ${data.items?.length || 0} | Tokens used: ~${tokensUsed}/${budget}
564
587
  `;
565
588
  output += footer;
566
589
  return output;
@@ -576,11 +599,11 @@ async function runHook(name, handler) {
576
599
  }
577
600
  debugLog(name, "stdin", input);
578
601
  await handler(input);
579
- debugLog(name, "completato", { success: true });
602
+ debugLog(name, "completed", { success: true });
580
603
  process.exit(0);
581
604
  } catch (error) {
582
- debugLog(name, "errore", { error: String(error) });
583
- process.stderr.write(`[kiro-memory:${name}] Errore: ${error}
605
+ debugLog(name, "error", { error: String(error) });
606
+ process.stderr.write(`[kiro-memory:${name}] Error: ${error}
584
607
  `);
585
608
  process.exit(0);
586
609
  }
@@ -590,14 +613,15 @@ async function runHook(name, handler) {
590
613
  import BetterSqlite3 from "better-sqlite3";
591
614
  var Database = class {
592
615
  _db;
616
+ _stmtCache = /* @__PURE__ */ new Map();
593
617
  constructor(path, options) {
594
618
  this._db = new BetterSqlite3(path, {
595
- // better-sqlite3 crea il file di default (non serve 'create')
619
+ // better-sqlite3 creates the file by default ('create' not needed)
596
620
  readonly: options?.readwrite === false ? true : false
597
621
  });
598
622
  }
599
623
  /**
600
- * Esegui una query SQL senza risultati
624
+ * Execute a SQL query without results
601
625
  */
602
626
  run(sql, params) {
603
627
  const stmt = this._db.prepare(sql);
@@ -605,51 +629,53 @@ var Database = class {
605
629
  return result;
606
630
  }
607
631
  /**
608
- * Prepara una query con interfaccia compatibile bun:sqlite
632
+ * Prepare a query with bun:sqlite-compatible interface.
633
+ * Returns a cached prepared statement for repeated queries.
609
634
  */
610
635
  query(sql) {
611
- return new BunQueryCompat(this._db, sql);
636
+ let cached = this._stmtCache.get(sql);
637
+ if (!cached) {
638
+ cached = new BunQueryCompat(this._db, sql);
639
+ this._stmtCache.set(sql, cached);
640
+ }
641
+ return cached;
612
642
  }
613
643
  /**
614
- * Crea una transazione
644
+ * Create a transaction
615
645
  */
616
646
  transaction(fn) {
617
647
  return this._db.transaction(fn);
618
648
  }
619
649
  /**
620
- * Chiudi la connessione
650
+ * Close the connection
621
651
  */
622
652
  close() {
653
+ this._stmtCache.clear();
623
654
  this._db.close();
624
655
  }
625
656
  };
626
657
  var BunQueryCompat = class {
627
- _db;
628
- _sql;
658
+ _stmt;
629
659
  constructor(db, sql) {
630
- this._db = db;
631
- this._sql = sql;
660
+ this._stmt = db.prepare(sql);
632
661
  }
633
662
  /**
634
- * Restituisce tutte le righe
663
+ * Returns all rows
635
664
  */
636
665
  all(...params) {
637
- const stmt = this._db.prepare(this._sql);
638
- return params.length > 0 ? stmt.all(...params) : stmt.all();
666
+ return params.length > 0 ? this._stmt.all(...params) : this._stmt.all();
639
667
  }
640
668
  /**
641
- * Restituisce la prima riga o null
669
+ * Returns the first row or null
642
670
  */
643
671
  get(...params) {
644
- const stmt = this._db.prepare(this._sql);
645
- return params.length > 0 ? stmt.get(...params) : stmt.get();
672
+ return params.length > 0 ? this._stmt.get(...params) : this._stmt.get();
646
673
  }
647
674
  /**
648
- * Esegui senza risultati
675
+ * Execute without results
649
676
  */
650
677
  run(...params) {
651
- const stmt = this._db.prepare(this._sql);
652
- return params.length > 0 ? stmt.run(...params) : stmt.run();
678
+ return params.length > 0 ? this._stmt.run(...params) : this._stmt.run();
653
679
  }
654
680
  };
655
681
 
@@ -911,40 +937,62 @@ function ensureDir(dirPath) {
911
937
  var SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024;
912
938
  var SQLITE_CACHE_SIZE_PAGES = 1e4;
913
939
  var KiroMemoryDatabase = class {
914
- db;
940
+ _db;
915
941
  /**
916
- * @param dbPath - Percorso al file SQLite (default: DB_PATH)
917
- * @param skipMigrations - Se true, salta il migration runner (per hook ad alta frequenza)
942
+ * Readonly accessor for the underlying Database instance.
943
+ * Prefer using query() and run() proxy methods directly.
944
+ */
945
+ get db() {
946
+ return this._db;
947
+ }
948
+ /**
949
+ * @param dbPath - Path to the SQLite file (default: DB_PATH)
950
+ * @param skipMigrations - If true, skip the migration runner (for high-frequency hooks)
918
951
  */
919
952
  constructor(dbPath = DB_PATH, skipMigrations = false) {
920
953
  if (dbPath !== ":memory:") {
921
954
  ensureDir(DATA_DIR2);
922
955
  }
923
- this.db = new Database(dbPath, { create: true, readwrite: true });
924
- this.db.run("PRAGMA journal_mode = WAL");
925
- this.db.run("PRAGMA synchronous = NORMAL");
926
- this.db.run("PRAGMA foreign_keys = ON");
927
- this.db.run("PRAGMA temp_store = memory");
928
- this.db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
929
- this.db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
956
+ this._db = new Database(dbPath, { create: true, readwrite: true });
957
+ this._db.run("PRAGMA journal_mode = WAL");
958
+ this._db.run("PRAGMA busy_timeout = 5000");
959
+ this._db.run("PRAGMA synchronous = NORMAL");
960
+ this._db.run("PRAGMA foreign_keys = ON");
961
+ this._db.run("PRAGMA temp_store = memory");
962
+ this._db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
963
+ this._db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
930
964
  if (!skipMigrations) {
931
- const migrationRunner = new MigrationRunner(this.db);
965
+ const migrationRunner = new MigrationRunner(this._db);
932
966
  migrationRunner.runAllMigrations();
933
967
  }
934
968
  }
935
969
  /**
936
- * Esegue una funzione all'interno di una transazione atomica.
937
- * Se fn() lancia un errore, la transazione viene annullata automaticamente.
970
+ * Prepare a query (delegates to underlying Database).
971
+ * Proxy method to avoid ctx.db.db.query() double access.
972
+ */
973
+ query(sql) {
974
+ return this._db.query(sql);
975
+ }
976
+ /**
977
+ * Execute a SQL statement without results (delegates to underlying Database).
978
+ * Proxy method to avoid ctx.db.db.run() double access.
979
+ */
980
+ run(sql, params) {
981
+ return this._db.run(sql, params);
982
+ }
983
+ /**
984
+ * Executes a function within an atomic transaction.
985
+ * If fn() throws an error, the transaction is automatically rolled back.
938
986
  */
939
987
  withTransaction(fn) {
940
- const transaction = this.db.transaction(fn);
941
- return transaction(this.db);
988
+ const transaction = this._db.transaction(fn);
989
+ return transaction(this._db);
942
990
  }
943
991
  /**
944
992
  * Close the database connection
945
993
  */
946
994
  close() {
947
- this.db.close();
995
+ this._db.close();
948
996
  }
949
997
  };
950
998
  var MigrationRunner = class {
@@ -1439,8 +1487,8 @@ var EmbeddingService = class {
1439
1487
  initialized = false;
1440
1488
  initializing = null;
1441
1489
  /**
1442
- * Inizializza il servizio di embedding.
1443
- * Tenta fastembed, poi @huggingface/transformers, poi fallback a null.
1490
+ * Initialize the embedding service.
1491
+ * Tries fastembed, then @huggingface/transformers, then fallback to null.
1444
1492
  */
1445
1493
  async initialize() {
1446
1494
  if (this.initialized) return this.provider !== null;
@@ -1461,11 +1509,11 @@ var EmbeddingService = class {
1461
1509
  });
1462
1510
  this.provider = "fastembed";
1463
1511
  this.initialized = true;
1464
- logger.info("EMBEDDING", "Inizializzato con fastembed (BGE-small-en-v1.5)");
1512
+ logger.info("EMBEDDING", "Initialized with fastembed (BGE-small-en-v1.5)");
1465
1513
  return true;
1466
1514
  }
1467
1515
  } catch (error) {
1468
- logger.debug("EMBEDDING", `fastembed non disponibile: ${error}`);
1516
+ logger.debug("EMBEDDING", `fastembed not available: ${error}`);
1469
1517
  }
1470
1518
  try {
1471
1519
  const transformers = await import("@huggingface/transformers");
@@ -1476,20 +1524,20 @@ var EmbeddingService = class {
1476
1524
  });
1477
1525
  this.provider = "transformers";
1478
1526
  this.initialized = true;
1479
- logger.info("EMBEDDING", "Inizializzato con @huggingface/transformers (all-MiniLM-L6-v2)");
1527
+ logger.info("EMBEDDING", "Initialized with @huggingface/transformers (all-MiniLM-L6-v2)");
1480
1528
  return true;
1481
1529
  }
1482
1530
  } catch (error) {
1483
- logger.debug("EMBEDDING", `@huggingface/transformers non disponibile: ${error}`);
1531
+ logger.debug("EMBEDDING", `@huggingface/transformers not available: ${error}`);
1484
1532
  }
1485
1533
  this.provider = null;
1486
1534
  this.initialized = true;
1487
- logger.warn("EMBEDDING", "Nessun provider embedding disponibile, ricerca semantica disabilitata");
1535
+ logger.warn("EMBEDDING", "No embedding provider available, semantic search disabled");
1488
1536
  return false;
1489
1537
  }
1490
1538
  /**
1491
- * Genera embedding per un singolo testo.
1492
- * Ritorna Float32Array con 384 dimensioni, o null se non disponibile.
1539
+ * Generate embedding for a single text.
1540
+ * Returns Float32Array with 384 dimensions, or null if not available.
1493
1541
  */
1494
1542
  async embed(text) {
1495
1543
  if (!this.initialized) await this.initialize();
@@ -1502,46 +1550,111 @@ var EmbeddingService = class {
1502
1550
  return await this._embedTransformers(truncated);
1503
1551
  }
1504
1552
  } catch (error) {
1505
- logger.error("EMBEDDING", `Errore generazione embedding: ${error}`);
1553
+ logger.error("EMBEDDING", `Error generating embedding: ${error}`);
1506
1554
  }
1507
1555
  return null;
1508
1556
  }
1509
1557
  /**
1510
- * Genera embeddings in batch.
1558
+ * Generate embeddings in batch.
1559
+ * Uses native batch support when available (fastembed, transformers),
1560
+ * falls back to serial processing on batch failure.
1511
1561
  */
1512
1562
  async embedBatch(texts) {
1513
1563
  if (!this.initialized) await this.initialize();
1514
1564
  if (!this.provider || !this.model) return texts.map(() => null);
1515
- const results = [];
1516
- for (const text of texts) {
1517
- try {
1518
- const embedding = await this.embed(text);
1519
- results.push(embedding);
1520
- } catch {
1521
- results.push(null);
1565
+ if (texts.length === 0) return [];
1566
+ const truncated = texts.map((t) => t.substring(0, 2e3));
1567
+ try {
1568
+ if (this.provider === "fastembed") {
1569
+ return await this._embedBatchFastembed(truncated);
1570
+ } else if (this.provider === "transformers") {
1571
+ return await this._embedBatchTransformers(truncated);
1522
1572
  }
1573
+ } catch (error) {
1574
+ logger.warn("EMBEDDING", `Batch embedding failed, falling back to serial: ${error}`);
1523
1575
  }
1524
- return results;
1576
+ return this._embedBatchSerial(truncated);
1525
1577
  }
1526
1578
  /**
1527
- * Verifica se il servizio è disponibile.
1579
+ * Check if the service is available.
1528
1580
  */
1529
1581
  isAvailable() {
1530
1582
  return this.initialized && this.provider !== null;
1531
1583
  }
1532
1584
  /**
1533
- * Nome del provider attivo.
1585
+ * Name of the active provider.
1534
1586
  */
1535
1587
  getProvider() {
1536
1588
  return this.provider;
1537
1589
  }
1538
1590
  /**
1539
- * Dimensioni del vettore embedding.
1591
+ * Embedding vector dimensions.
1540
1592
  */
1541
1593
  getDimensions() {
1542
1594
  return 384;
1543
1595
  }
1544
- // --- Provider specifici ---
1596
+ // --- Batch implementations ---
1597
+ /**
1598
+ * Native batch embedding with fastembed.
1599
+ * FlagEmbedding.embed() accepts string[] and returns an async iterable of batches.
1600
+ */
1601
+ async _embedBatchFastembed(texts) {
1602
+ const results = [];
1603
+ const embeddings = this.model.embed(texts, texts.length);
1604
+ for await (const batch of embeddings) {
1605
+ if (batch) {
1606
+ for (const vec of batch) {
1607
+ results.push(vec instanceof Float32Array ? vec : new Float32Array(vec));
1608
+ }
1609
+ }
1610
+ }
1611
+ while (results.length < texts.length) {
1612
+ results.push(null);
1613
+ }
1614
+ return results;
1615
+ }
1616
+ /**
1617
+ * Batch embedding with @huggingface/transformers pipeline.
1618
+ * The pipeline accepts string[] and returns a Tensor with shape [N, dims].
1619
+ */
1620
+ async _embedBatchTransformers(texts) {
1621
+ const output = await this.model(texts, {
1622
+ pooling: "mean",
1623
+ normalize: true
1624
+ });
1625
+ if (!output?.data) {
1626
+ return texts.map(() => null);
1627
+ }
1628
+ const dims = this.getDimensions();
1629
+ const data = output.data instanceof Float32Array ? output.data : new Float32Array(output.data);
1630
+ const results = [];
1631
+ for (let i = 0; i < texts.length; i++) {
1632
+ const offset = i * dims;
1633
+ if (offset + dims <= data.length) {
1634
+ results.push(data.slice(offset, offset + dims));
1635
+ } else {
1636
+ results.push(null);
1637
+ }
1638
+ }
1639
+ return results;
1640
+ }
1641
+ /**
1642
+ * Serial fallback: embed texts one at a time.
1643
+ * Used when native batch fails.
1644
+ */
1645
+ async _embedBatchSerial(texts) {
1646
+ const results = [];
1647
+ for (const text of texts) {
1648
+ try {
1649
+ const embedding = await this.embed(text);
1650
+ results.push(embedding);
1651
+ } catch {
1652
+ results.push(null);
1653
+ }
1654
+ }
1655
+ return results;
1656
+ }
1657
+ // --- Single-text provider implementations ---
1545
1658
  async _embedFastembed(text) {
1546
1659
  const embeddings = this.model.embed([text], 1);
1547
1660
  for await (const batch of embeddings) {
@@ -1572,17 +1685,21 @@ function getEmbeddingService() {
1572
1685
  }
1573
1686
 
1574
1687
  // src/services/search/VectorSearch.ts
1688
+ var DEFAULT_MAX_CANDIDATES = 2e3;
1575
1689
  function cosineSimilarity(a, b) {
1576
- if (a.length !== b.length) return 0;
1690
+ const len = a.length;
1691
+ if (len !== b.length) return 0;
1577
1692
  let dotProduct = 0;
1578
1693
  let normA = 0;
1579
1694
  let normB = 0;
1580
- for (let i = 0; i < a.length; i++) {
1581
- dotProduct += a[i] * b[i];
1582
- normA += a[i] * a[i];
1583
- normB += b[i] * b[i];
1584
- }
1585
- const denominator = Math.sqrt(normA) * Math.sqrt(normB);
1695
+ for (let i = 0; i < len; i++) {
1696
+ const ai = a[i];
1697
+ const bi = b[i];
1698
+ dotProduct += ai * bi;
1699
+ normA += ai * ai;
1700
+ normB += bi * bi;
1701
+ }
1702
+ const denominator = Math.sqrt(normA * normB);
1586
1703
  if (denominator === 0) return 0;
1587
1704
  return dotProduct / denominator;
1588
1705
  }
@@ -1595,23 +1712,36 @@ function bufferToFloat32(buf) {
1595
1712
  }
1596
1713
  var VectorSearch = class {
1597
1714
  /**
1598
- * Ricerca semantica: calcola cosine similarity tra query e tutti gli embeddings.
1715
+ * Semantic search with SQL pre-filtering for scalability.
1716
+ *
1717
+ * 2-phase strategy:
1718
+ * 1. SQL pre-filters by project + sorts by recency (loads max N candidates)
1719
+ * 2. JS computes cosine similarity only on filtered candidates
1720
+ *
1721
+ * With 50k observations and maxCandidates=2000, loads only ~4% of data.
1599
1722
  */
1600
1723
  async search(db, queryEmbedding, options = {}) {
1601
1724
  const limit = options.limit || 10;
1602
1725
  const threshold = options.threshold || 0.3;
1726
+ const maxCandidates = options.maxCandidates || DEFAULT_MAX_CANDIDATES;
1603
1727
  try {
1604
- let sql = `
1728
+ const conditions = [];
1729
+ const params = [];
1730
+ if (options.project) {
1731
+ conditions.push("o.project = ?");
1732
+ params.push(options.project);
1733
+ }
1734
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1735
+ const sql = `
1605
1736
  SELECT e.observation_id, e.embedding,
1606
1737
  o.title, o.text, o.type, o.project, o.created_at, o.created_at_epoch
1607
1738
  FROM observation_embeddings e
1608
1739
  JOIN observations o ON o.id = e.observation_id
1740
+ ${whereClause}
1741
+ ORDER BY o.created_at_epoch DESC
1742
+ LIMIT ?
1609
1743
  `;
1610
- const params = [];
1611
- if (options.project) {
1612
- sql += " WHERE o.project = ?";
1613
- params.push(options.project);
1614
- }
1744
+ params.push(maxCandidates);
1615
1745
  const rows = db.query(sql).all(...params);
1616
1746
  const scored = [];
1617
1747
  for (const row of rows) {
@@ -1632,14 +1762,15 @@ var VectorSearch = class {
1632
1762
  }
1633
1763
  }
1634
1764
  scored.sort((a, b) => b.similarity - a.similarity);
1765
+ logger.debug("VECTOR", `Search: ${rows.length} candidates \u2192 ${scored.length} above threshold \u2192 ${Math.min(scored.length, limit)} results`);
1635
1766
  return scored.slice(0, limit);
1636
1767
  } catch (error) {
1637
- logger.error("VECTOR", `Errore ricerca vettoriale: ${error}`);
1768
+ logger.error("VECTOR", `Vector search error: ${error}`);
1638
1769
  return [];
1639
1770
  }
1640
1771
  }
1641
1772
  /**
1642
- * Salva embedding per un'osservazione.
1773
+ * Store embedding for an observation.
1643
1774
  */
1644
1775
  async storeEmbedding(db, observationId, embedding, model) {
1645
1776
  try {
@@ -1655,18 +1786,18 @@ var VectorSearch = class {
1655
1786
  embedding.length,
1656
1787
  (/* @__PURE__ */ new Date()).toISOString()
1657
1788
  );
1658
- logger.debug("VECTOR", `Embedding salvato per osservazione ${observationId}`);
1789
+ logger.debug("VECTOR", `Embedding saved for observation ${observationId}`);
1659
1790
  } catch (error) {
1660
- logger.error("VECTOR", `Errore salvataggio embedding: ${error}`);
1791
+ logger.error("VECTOR", `Error saving embedding: ${error}`);
1661
1792
  }
1662
1793
  }
1663
1794
  /**
1664
- * Genera embeddings per osservazioni che non li hanno ancora.
1795
+ * Generate embeddings for observations that don't have them yet.
1665
1796
  */
1666
1797
  async backfillEmbeddings(db, batchSize = 50) {
1667
1798
  const embeddingService2 = getEmbeddingService();
1668
1799
  if (!await embeddingService2.initialize()) {
1669
- logger.warn("VECTOR", "Embedding service non disponibile, backfill saltato");
1800
+ logger.warn("VECTOR", "Embedding service not available, backfill skipped");
1670
1801
  return 0;
1671
1802
  }
1672
1803
  const rows = db.query(`
@@ -1692,11 +1823,11 @@ var VectorSearch = class {
1692
1823
  count++;
1693
1824
  }
1694
1825
  }
1695
- logger.info("VECTOR", `Backfill completato: ${count}/${rows.length} embeddings generati`);
1826
+ logger.info("VECTOR", `Backfill completed: ${count}/${rows.length} embeddings generated`);
1696
1827
  return count;
1697
1828
  }
1698
1829
  /**
1699
- * Statistiche sugli embeddings.
1830
+ * Embedding statistics.
1700
1831
  */
1701
1832
  getStats(db) {
1702
1833
  try {
@@ -1723,21 +1854,21 @@ function getVectorSearch() {
1723
1854
  var HybridSearch = class {
1724
1855
  embeddingInitialized = false;
1725
1856
  /**
1726
- * Inizializza il servizio di embedding (lazy, non bloccante)
1857
+ * Initialize the embedding service (lazy, non-blocking)
1727
1858
  */
1728
1859
  async initialize() {
1729
1860
  try {
1730
1861
  const embeddingService2 = getEmbeddingService();
1731
1862
  await embeddingService2.initialize();
1732
1863
  this.embeddingInitialized = embeddingService2.isAvailable();
1733
- logger.info("SEARCH", `HybridSearch inizializzato (embedding: ${this.embeddingInitialized ? "attivo" : "disattivato"})`);
1864
+ logger.info("SEARCH", `HybridSearch initialized (embedding: ${this.embeddingInitialized ? "active" : "disabled"})`);
1734
1865
  } catch (error) {
1735
- logger.warn("SEARCH", "Inizializzazione embedding fallita, uso solo FTS5", {}, error);
1866
+ logger.warn("SEARCH", "Embedding initialization failed, using only FTS5", {}, error);
1736
1867
  this.embeddingInitialized = false;
1737
1868
  }
1738
1869
  }
1739
1870
  /**
1740
- * Ricerca ibrida con scoring a 4 segnali
1871
+ * Hybrid search with 4-signal scoring
1741
1872
  */
1742
1873
  async search(db, query, options = {}) {
1743
1874
  const limit = options.limit || 10;
@@ -1753,7 +1884,7 @@ var HybridSearch = class {
1753
1884
  const vectorResults = await vectorSearch2.search(db, queryEmbedding, {
1754
1885
  project: options.project,
1755
1886
  limit: limit * 2,
1756
- // Prendiamo piu risultati per il ranking
1887
+ // Fetch more results for ranking
1757
1888
  threshold: 0.3
1758
1889
  });
1759
1890
  for (const hit of vectorResults) {
@@ -1770,10 +1901,10 @@ var HybridSearch = class {
1770
1901
  source: "vector"
1771
1902
  });
1772
1903
  }
1773
- logger.debug("SEARCH", `Vector search: ${vectorResults.length} risultati`);
1904
+ logger.debug("SEARCH", `Vector search: ${vectorResults.length} results`);
1774
1905
  }
1775
1906
  } catch (error) {
1776
- logger.warn("SEARCH", "Ricerca vettoriale fallita, uso solo keyword", {}, error);
1907
+ logger.warn("SEARCH", "Vector search failed, using only keyword", {}, error);
1777
1908
  }
1778
1909
  }
1779
1910
  try {
@@ -1803,9 +1934,9 @@ var HybridSearch = class {
1803
1934
  });
1804
1935
  }
1805
1936
  }
1806
- logger.debug("SEARCH", `Keyword search: ${keywordResults.length} risultati`);
1937
+ logger.debug("SEARCH", `Keyword search: ${keywordResults.length} results`);
1807
1938
  } catch (error) {
1808
- logger.error("SEARCH", "Ricerca keyword fallita", {}, error);
1939
+ logger.error("SEARCH", "Keyword search failed", {}, error);
1809
1940
  }
1810
1941
  if (rawItems.size === 0) return [];
1811
1942
  const allFTS5Ranks = Array.from(rawItems.values()).filter((item) => item.fts5Rank !== null).map((item) => item.fts5Rank);
@@ -1893,33 +2024,33 @@ var KiroMemorySDK = class {
1893
2024
  };
1894
2025
  }
1895
2026
  /**
1896
- * Valida input per storeObservation
2027
+ * Validate input for storeObservation
1897
2028
  */
1898
2029
  validateObservationInput(data) {
1899
2030
  if (!data.type || typeof data.type !== "string" || data.type.length > 100) {
1900
- throw new Error("type \xE8 obbligatorio (stringa, max 100 caratteri)");
2031
+ throw new Error("type is required (string, max 100 chars)");
1901
2032
  }
1902
2033
  if (!data.title || typeof data.title !== "string" || data.title.length > 500) {
1903
- throw new Error("title \xE8 obbligatorio (stringa, max 500 caratteri)");
2034
+ throw new Error("title is required (string, max 500 chars)");
1904
2035
  }
1905
2036
  if (!data.content || typeof data.content !== "string" || data.content.length > 1e5) {
1906
- throw new Error("content \xE8 obbligatorio (stringa, max 100KB)");
2037
+ throw new Error("content is required (string, max 100KB)");
1907
2038
  }
1908
2039
  }
1909
2040
  /**
1910
- * Valida input per storeSummary
2041
+ * Validate input for storeSummary
1911
2042
  */
1912
2043
  validateSummaryInput(data) {
1913
2044
  const MAX = 5e4;
1914
2045
  for (const [key, val] of Object.entries(data)) {
1915
2046
  if (val !== void 0 && val !== null) {
1916
- if (typeof val !== "string") throw new Error(`${key} deve essere una stringa`);
1917
- if (val.length > MAX) throw new Error(`${key} troppo grande (max 50KB)`);
2047
+ if (typeof val !== "string") throw new Error(`${key} must be a string`);
2048
+ if (val.length > MAX) throw new Error(`${key} too large (max 50KB)`);
1918
2049
  }
1919
2050
  }
1920
2051
  }
1921
2052
  /**
1922
- * Genera e salva embedding per un'osservazione (fire-and-forget, non blocca)
2053
+ * Generate and store embedding for an observation (fire-and-forget, non-blocking)
1923
2054
  */
1924
2055
  async generateEmbeddingAsync(observationId, title, content, concepts) {
1925
2056
  try {
@@ -1939,39 +2070,39 @@ var KiroMemorySDK = class {
1939
2070
  );
1940
2071
  }
1941
2072
  } catch (error) {
1942
- logger.debug("SDK", `Embedding generation fallita per obs ${observationId}: ${error}`);
2073
+ logger.debug("SDK", `Embedding generation failed for obs ${observationId}: ${error}`);
1943
2074
  }
1944
2075
  }
1945
2076
  /**
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.
2077
+ * Generate SHA256 content hash for content-based deduplication.
2078
+ * Uses (project + type + title + narrative) as semantic identity tuple.
2079
+ * Does NOT include sessionId since it's unique per invocation.
1949
2080
  */
1950
2081
  generateContentHash(type, title, narrative) {
1951
2082
  const payload = `${this.project}|${type}|${title}|${narrative || ""}`;
1952
2083
  return createHash("sha256").update(payload).digest("hex");
1953
2084
  }
1954
2085
  /**
1955
- * Finestre di deduplicazione per tipo (ms).
1956
- * Tipi con molte ripetizioni hanno finestre più ampie.
2086
+ * Deduplication windows per type (ms).
2087
+ * Types with many repetitions have wider windows.
1957
2088
  */
1958
2089
  getDeduplicationWindow(type) {
1959
2090
  switch (type) {
1960
2091
  case "file-read":
1961
2092
  return 6e4;
1962
- // 60s — letture frequenti sugli stessi file
2093
+ // 60s — frequent reads on the same files
1963
2094
  case "file-write":
1964
2095
  return 1e4;
1965
- // 10s — scritture rapide consecutive
2096
+ // 10s — rapid consecutive writes
1966
2097
  case "command":
1967
2098
  return 3e4;
1968
2099
  // 30s — standard
1969
2100
  case "research":
1970
2101
  return 12e4;
1971
- // 120s — web search e fetch ripetuti
2102
+ // 120s — repeated web search and fetch
1972
2103
  case "delegation":
1973
2104
  return 6e4;
1974
- // 60s — delegazioni rapide
2105
+ // 60s — rapid delegations
1975
2106
  default:
1976
2107
  return 3e4;
1977
2108
  }
@@ -1985,7 +2116,7 @@ var KiroMemorySDK = class {
1985
2116
  const contentHash = this.generateContentHash(data.type, data.title, data.narrative);
1986
2117
  const dedupWindow = this.getDeduplicationWindow(data.type);
1987
2118
  if (isDuplicateObservation(this.db.db, contentHash, dedupWindow)) {
1988
- logger.debug("SDK", `Osservazione duplicata scartata (${data.type}, ${dedupWindow}ms): ${data.title}`);
2119
+ logger.debug("SDK", `Duplicate observation discarded (${data.type}, ${dedupWindow}ms): ${data.title}`);
1989
2120
  return -1;
1990
2121
  }
1991
2122
  const filesRead = data.filesRead || (data.type === "file-read" ? data.files : void 0);
@@ -2013,12 +2144,12 @@ var KiroMemorySDK = class {
2013
2144
  return observationId;
2014
2145
  }
2015
2146
  /**
2016
- * Salva conoscenza strutturata (constraint, decision, heuristic, rejected).
2017
- * Usa il campo `type` per il knowledgeType e `facts` per i metadati JSON.
2147
+ * Store structured knowledge (constraint, decision, heuristic, rejected).
2148
+ * Uses the `type` field for knowledgeType and `facts` for JSON metadata.
2018
2149
  */
2019
2150
  async storeKnowledge(data) {
2020
2151
  if (!KNOWLEDGE_TYPES.includes(data.knowledgeType)) {
2021
- throw new Error(`knowledgeType non valido: ${data.knowledgeType}. Valori ammessi: ${KNOWLEDGE_TYPES.join(", ")}`);
2152
+ throw new Error(`Invalid knowledgeType: ${data.knowledgeType}. Allowed values: ${KNOWLEDGE_TYPES.join(", ")}`);
2022
2153
  }
2023
2154
  this.validateObservationInput({ type: data.knowledgeType, title: data.title, content: data.content });
2024
2155
  const metadata = (() => {
@@ -2050,9 +2181,9 @@ var KiroMemorySDK = class {
2050
2181
  }
2051
2182
  })();
2052
2183
  const sessionId = "sdk-" + Date.now();
2053
- const contentHash = this.generateContentHash(data.type, data.title);
2184
+ const contentHash = this.generateContentHash(data.knowledgeType, data.title);
2054
2185
  if (isDuplicateObservation(this.db.db, contentHash)) {
2055
- logger.debug("SDK", `Knowledge duplicata scartata: ${data.title}`);
2186
+ logger.debug("SDK", `Duplicate knowledge discarded: ${data.title}`);
2056
2187
  return -1;
2057
2188
  }
2058
2189
  const discoveryTokens = Math.ceil(data.content.length / 4);
@@ -2069,11 +2200,11 @@ var KiroMemorySDK = class {
2069
2200
  null,
2070
2201
  // narrative
2071
2202
  JSON.stringify(metadata),
2072
- // facts = metadati JSON
2203
+ // facts = JSON metadata
2073
2204
  data.concepts?.join(", ") || null,
2074
2205
  data.files?.join(", ") || null,
2075
2206
  null,
2076
- // filesModified: knowledge non modifica file
2207
+ // filesModified: knowledge doesn't modify files
2077
2208
  0,
2078
2209
  // prompt_number
2079
2210
  contentHash,
@@ -2184,8 +2315,8 @@ var KiroMemorySDK = class {
2184
2315
  return this.project;
2185
2316
  }
2186
2317
  /**
2187
- * Ricerca ibrida: vector search + keyword FTS5
2188
- * Richiede inizializzazione HybridSearch (embedding service)
2318
+ * Hybrid search: vector search + keyword FTS5
2319
+ * Requires HybridSearch initialization (embedding service)
2189
2320
  */
2190
2321
  async hybridSearch(query, options = {}) {
2191
2322
  const hybridSearch2 = getHybridSearch();
@@ -2195,8 +2326,8 @@ var KiroMemorySDK = class {
2195
2326
  });
2196
2327
  }
2197
2328
  /**
2198
- * Ricerca solo semantica (vector search)
2199
- * Ritorna risultati basati su similarità coseno con gli embeddings
2329
+ * Semantic-only search (vector search)
2330
+ * Returns results based on cosine similarity with embeddings
2200
2331
  */
2201
2332
  async semanticSearch(query, options = {}) {
2202
2333
  const embeddingService2 = getEmbeddingService();
@@ -2231,21 +2362,21 @@ var KiroMemorySDK = class {
2231
2362
  }));
2232
2363
  }
2233
2364
  /**
2234
- * Genera embeddings per osservazioni che non li hanno ancora
2365
+ * Generate embeddings for observations that don't have them yet
2235
2366
  */
2236
2367
  async backfillEmbeddings(batchSize = 50) {
2237
2368
  const vectorSearch2 = getVectorSearch();
2238
2369
  return vectorSearch2.backfillEmbeddings(this.db.db, batchSize);
2239
2370
  }
2240
2371
  /**
2241
- * Statistiche sugli embeddings nel database
2372
+ * Embedding statistics in the database
2242
2373
  */
2243
2374
  getEmbeddingStats() {
2244
2375
  const vectorSearch2 = getVectorSearch();
2245
2376
  return vectorSearch2.getStats(this.db.db);
2246
2377
  }
2247
2378
  /**
2248
- * Inizializza il servizio di embedding (lazy, chiamare prima di hybridSearch)
2379
+ * Initialize the embedding service (lazy, call before hybridSearch)
2249
2380
  */
2250
2381
  async initializeEmbeddings() {
2251
2382
  const hybridSearch2 = getHybridSearch();
@@ -2253,10 +2384,10 @@ var KiroMemorySDK = class {
2253
2384
  return getEmbeddingService().isAvailable();
2254
2385
  }
2255
2386
  /**
2256
- * Contesto smart con ranking a 4 segnali e budget token.
2387
+ * Smart context with 4-signal ranking and token budget.
2257
2388
  *
2258
- * Se query presente: usa HybridSearch con SEARCH_WEIGHTS.
2259
- * Se senza query: ranking per recency + project match (CONTEXT_WEIGHTS).
2389
+ * If query present: uses HybridSearch with SEARCH_WEIGHTS.
2390
+ * If no query: ranking by recency + project match (CONTEXT_WEIGHTS).
2260
2391
  */
2261
2392
  async getSmartContext(options = {}) {
2262
2393
  const tokenBudget = options.tokenBudget || parseInt(process.env.KIRO_MEMORY_CONTEXT_TOKENS || "0", 10) || 2e3;
@@ -2330,8 +2461,8 @@ var KiroMemorySDK = class {
2330
2461
  };
2331
2462
  }
2332
2463
  /**
2333
- * Rileva osservazioni stale (file modificati dopo la creazione) e le marca nel DB.
2334
- * Ritorna il numero di osservazioni marcate come stale.
2464
+ * Detect stale observations (files modified after creation) and mark them in DB.
2465
+ * Returns the number of observations marked as stale.
2335
2466
  */
2336
2467
  async detectStaleObservations() {
2337
2468
  const staleObs = getStaleObservations(this.db.db, this.project);
@@ -2342,14 +2473,14 @@ var KiroMemorySDK = class {
2342
2473
  return staleObs.length;
2343
2474
  }
2344
2475
  /**
2345
- * Consolida osservazioni duplicate sullo stesso file e tipo.
2346
- * Raggruppa per (project, type, files_modified), mantiene la piu recente.
2476
+ * Consolidate duplicate observations on the same file and type.
2477
+ * Groups by (project, type, files_modified), keeps the most recent.
2347
2478
  */
2348
2479
  async consolidateObservations(options = {}) {
2349
2480
  return consolidateObservations(this.db.db, this.project, options);
2350
2481
  }
2351
2482
  /**
2352
- * Statistiche decay: totale, stale, mai accedute, accedute di recente.
2483
+ * Decay statistics: total, stale, never accessed, recently accessed.
2353
2484
  */
2354
2485
  async getDecayStats() {
2355
2486
  const total = this.db.db.query(
@@ -2368,8 +2499,8 @@ var KiroMemorySDK = class {
2368
2499
  return { total, stale, neverAccessed, recentlyAccessed };
2369
2500
  }
2370
2501
  /**
2371
- * Crea un checkpoint strutturato per resume sessione.
2372
- * Salva automaticamente un context_snapshot con le ultime 10 osservazioni.
2502
+ * Create a structured checkpoint for session resume.
2503
+ * Automatically saves a context_snapshot with the last 10 observations.
2373
2504
  */
2374
2505
  async createCheckpoint(sessionId, data) {
2375
2506
  const recentObs = getObservationsByProject(this.db.db, this.project, 10);
@@ -2386,21 +2517,21 @@ var KiroMemorySDK = class {
2386
2517
  });
2387
2518
  }
2388
2519
  /**
2389
- * Recupera l'ultimo checkpoint di una sessione specifica.
2520
+ * Retrieve the latest checkpoint of a specific session.
2390
2521
  */
2391
2522
  async getCheckpoint(sessionId) {
2392
2523
  return getLatestCheckpoint(this.db.db, sessionId);
2393
2524
  }
2394
2525
  /**
2395
- * Recupera l'ultimo checkpoint per il progetto corrente.
2396
- * Utile per resume automatico senza specificare session ID.
2526
+ * Retrieve the latest checkpoint for the current project.
2527
+ * Useful for automatic resume without specifying session ID.
2397
2528
  */
2398
2529
  async getLatestProjectCheckpoint() {
2399
2530
  return getLatestCheckpointByProject(this.db.db, this.project);
2400
2531
  }
2401
2532
  /**
2402
- * Genera un report di attività per il progetto corrente.
2403
- * Aggrega osservazioni, sessioni, summaries e file per un periodo temporale.
2533
+ * Generate an activity report for the current project.
2534
+ * Aggregates observations, sessions, summaries and files for a time period.
2404
2535
  */
2405
2536
  async generateReport(options) {
2406
2537
  const now = /* @__PURE__ */ new Date();
@@ -2485,7 +2616,7 @@ runHook("agentSpawn", async (input) => {
2485
2616
  summaries: smartCtx.summaries,
2486
2617
  project
2487
2618
  });
2488
- output += `> UI disponibile su http://127.0.0.1:${process.env.KIRO_MEMORY_WORKER_PORT || "3001"}
2619
+ output += `> UI available at http://127.0.0.1:${process.env.KIRO_MEMORY_WORKER_PORT || "3001"}
2489
2620
  `;
2490
2621
  process.stdout.write(output);
2491
2622
  } finally {