kiro-memory 1.9.0 → 3.0.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.
Files changed (34) hide show
  1. package/README.md +5 -1
  2. package/package.json +5 -5
  3. package/plugin/dist/cli/contextkit.js +2611 -345
  4. package/plugin/dist/hooks/agentSpawn.js +853 -223
  5. package/plugin/dist/hooks/kiro-hooks.js +841 -211
  6. package/plugin/dist/hooks/postToolUse.js +853 -222
  7. package/plugin/dist/hooks/stop.js +850 -220
  8. package/plugin/dist/hooks/userPromptSubmit.js +848 -216
  9. package/plugin/dist/index.js +843 -340
  10. package/plugin/dist/plugins/github/github-client.js +152 -0
  11. package/plugin/dist/plugins/github/index.js +412 -0
  12. package/plugin/dist/plugins/github/issue-parser.js +54 -0
  13. package/plugin/dist/plugins/slack/formatter.js +90 -0
  14. package/plugin/dist/plugins/slack/index.js +215 -0
  15. package/plugin/dist/sdk/index.js +841 -215
  16. package/plugin/dist/servers/mcp-server.js +4461 -397
  17. package/plugin/dist/services/search/EmbeddingService.js +146 -37
  18. package/plugin/dist/services/search/HybridSearch.js +564 -116
  19. package/plugin/dist/services/search/VectorSearch.js +187 -60
  20. package/plugin/dist/services/search/index.js +565 -254
  21. package/plugin/dist/services/sqlite/Backup.js +416 -0
  22. package/plugin/dist/services/sqlite/Database.js +126 -153
  23. package/plugin/dist/services/sqlite/ImportExport.js +452 -0
  24. package/plugin/dist/services/sqlite/Observations.js +314 -19
  25. package/plugin/dist/services/sqlite/Prompts.js +1 -1
  26. package/plugin/dist/services/sqlite/Search.js +41 -29
  27. package/plugin/dist/services/sqlite/Summaries.js +4 -4
  28. package/plugin/dist/services/sqlite/index.js +1428 -208
  29. package/plugin/dist/viewer.css +1 -0
  30. package/plugin/dist/viewer.html +2 -179
  31. package/plugin/dist/viewer.js +23 -24942
  32. package/plugin/dist/viewer.js.map +7 -0
  33. package/plugin/dist/worker-service.js +427 -5569
  34. package/plugin/dist/worker-service.js.map +7 -0
@@ -16,6 +16,290 @@ var __export = (target, all) => {
16
16
  __defProp(target, name, { get: all[name], enumerable: true });
17
17
  };
18
18
 
19
+ // src/utils/secrets.ts
20
+ function redactSecrets(text) {
21
+ if (!text) return text;
22
+ let redacted = text;
23
+ for (const { pattern } of SECRET_PATTERNS) {
24
+ pattern.lastIndex = 0;
25
+ redacted = redacted.replace(pattern, (match) => {
26
+ const prefix = match.substring(0, Math.min(4, match.length));
27
+ return `${prefix}***REDACTED***`;
28
+ });
29
+ }
30
+ return redacted;
31
+ }
32
+ var SECRET_PATTERNS;
33
+ var init_secrets = __esm({
34
+ "src/utils/secrets.ts"() {
35
+ "use strict";
36
+ SECRET_PATTERNS = [
37
+ // AWS Access Keys (AKIA, ABIA, ACCA, ASIA prefixes + 16 alphanumeric chars)
38
+ { name: "aws-key", pattern: /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/g },
39
+ // JWT tokens (three base64url segments separated by dots)
40
+ { name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
41
+ // Generic API keys in key=value or key: value assignments
42
+ { name: "api-key", pattern: /(?:api[_-]?key|apikey|api[_-]?secret)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{20,})['"]?/gi },
43
+ // Password/secret/token in variable assignments
44
+ { name: "credential", pattern: /(?:password|passwd|pwd|secret|token|auth[_-]?token|access[_-]?token|bearer)\s*[:=]\s*['"]?([^\s'"]{8,})['"]?/gi },
45
+ // Credentials embedded in URLs (user:pass@host)
46
+ { name: "url-credential", pattern: /(?:https?:\/\/)([^:]+):([^@]+)@/g },
47
+ // PEM-encoded private keys (RSA, EC, DSA, OpenSSH)
48
+ { name: "private-key", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
49
+ // GitHub personal access tokens (ghp_, gho_, ghu_, ghs_, ghr_ prefixes)
50
+ { name: "github-token", pattern: /gh[pousr]_[a-zA-Z0-9]{36,}/g },
51
+ // Slack bot/user/app tokens
52
+ { name: "slack-token", pattern: /xox[bpoas]-[a-zA-Z0-9-]{10,}/g },
53
+ // HTTP Authorization Bearer header values
54
+ { name: "bearer-header", pattern: /\bBearer\s+([a-zA-Z0-9_\-\.]{20,})/g },
55
+ // Generic hex secrets (32+ hex chars after a key/secret/token/password label)
56
+ { name: "hex-secret", pattern: /(?:key|secret|token|password)\s*[:=]\s*['"]?([0-9a-f]{32,})['"]?/gi }
57
+ ];
58
+ }
59
+ });
60
+
61
+ // src/utils/categorizer.ts
62
+ function categorize(input) {
63
+ const scores = /* @__PURE__ */ new Map();
64
+ const searchText = [
65
+ input.title,
66
+ input.text || "",
67
+ input.narrative || "",
68
+ input.concepts || ""
69
+ ].join(" ").toLowerCase();
70
+ const allFiles = [input.filesModified || "", input.filesRead || ""].join(",");
71
+ for (const rule of CATEGORY_RULES) {
72
+ let score = 0;
73
+ for (const kw of rule.keywords) {
74
+ if (searchText.includes(kw.toLowerCase())) {
75
+ score += rule.weight;
76
+ }
77
+ }
78
+ if (rule.types && rule.types.includes(input.type)) {
79
+ score += rule.weight * 2;
80
+ }
81
+ if (rule.filePatterns && allFiles) {
82
+ for (const pattern of rule.filePatterns) {
83
+ if (pattern.test(allFiles)) {
84
+ score += rule.weight;
85
+ }
86
+ }
87
+ }
88
+ if (score > 0) {
89
+ scores.set(rule.category, (scores.get(rule.category) || 0) + score);
90
+ }
91
+ }
92
+ let bestCategory = "general";
93
+ let bestScore = 0;
94
+ for (const [category, score] of scores) {
95
+ if (score > bestScore) {
96
+ bestScore = score;
97
+ bestCategory = category;
98
+ }
99
+ }
100
+ return bestCategory;
101
+ }
102
+ var CATEGORY_RULES;
103
+ var init_categorizer = __esm({
104
+ "src/utils/categorizer.ts"() {
105
+ "use strict";
106
+ CATEGORY_RULES = [
107
+ {
108
+ category: "security",
109
+ keywords: [
110
+ "security",
111
+ "vulnerability",
112
+ "cve",
113
+ "xss",
114
+ "csrf",
115
+ "injection",
116
+ "sanitize",
117
+ "escape",
118
+ "auth",
119
+ "authentication",
120
+ "authorization",
121
+ "permission",
122
+ "helmet",
123
+ "cors",
124
+ "rate-limit",
125
+ "token",
126
+ "encrypt",
127
+ "decrypt",
128
+ "secret",
129
+ "redact",
130
+ "owasp"
131
+ ],
132
+ filePatterns: [/security/i, /auth/i, /secrets?\.ts/i],
133
+ weight: 10
134
+ },
135
+ {
136
+ category: "testing",
137
+ keywords: [
138
+ "test",
139
+ "spec",
140
+ "expect",
141
+ "assert",
142
+ "mock",
143
+ "stub",
144
+ "fixture",
145
+ "coverage",
146
+ "jest",
147
+ "vitest",
148
+ "bun test",
149
+ "unit test",
150
+ "integration test",
151
+ "e2e"
152
+ ],
153
+ types: ["test"],
154
+ filePatterns: [/\.test\./i, /\.spec\./i, /tests?\//i, /__tests__/i],
155
+ weight: 8
156
+ },
157
+ {
158
+ category: "debugging",
159
+ keywords: [
160
+ "debug",
161
+ "fix",
162
+ "bug",
163
+ "error",
164
+ "crash",
165
+ "stacktrace",
166
+ "stack trace",
167
+ "exception",
168
+ "breakpoint",
169
+ "investigate",
170
+ "root cause",
171
+ "troubleshoot",
172
+ "diagnose",
173
+ "bisect",
174
+ "regression"
175
+ ],
176
+ types: ["bugfix"],
177
+ weight: 8
178
+ },
179
+ {
180
+ category: "architecture",
181
+ keywords: [
182
+ "architect",
183
+ "design",
184
+ "pattern",
185
+ "modular",
186
+ "migration",
187
+ "schema",
188
+ "database",
189
+ "api design",
190
+ "abstract",
191
+ "dependency injection",
192
+ "singleton",
193
+ "factory",
194
+ "observer",
195
+ "middleware",
196
+ "pipeline",
197
+ "microservice",
198
+ "monolith"
199
+ ],
200
+ types: ["decision", "constraint"],
201
+ weight: 7
202
+ },
203
+ {
204
+ category: "refactoring",
205
+ keywords: [
206
+ "refactor",
207
+ "rename",
208
+ "extract",
209
+ "inline",
210
+ "move",
211
+ "split",
212
+ "merge",
213
+ "simplify",
214
+ "cleanup",
215
+ "clean up",
216
+ "dead code",
217
+ "consolidate",
218
+ "reorganize",
219
+ "restructure",
220
+ "decouple"
221
+ ],
222
+ weight: 6
223
+ },
224
+ {
225
+ category: "config",
226
+ keywords: [
227
+ "config",
228
+ "configuration",
229
+ "env",
230
+ "environment",
231
+ "dotenv",
232
+ ".env",
233
+ "settings",
234
+ "tsconfig",
235
+ "eslint",
236
+ "prettier",
237
+ "webpack",
238
+ "vite",
239
+ "esbuild",
240
+ "docker",
241
+ "ci/cd",
242
+ "github actions",
243
+ "deploy",
244
+ "build",
245
+ "bundle",
246
+ "package.json"
247
+ ],
248
+ filePatterns: [
249
+ /\.config\./i,
250
+ /\.env/i,
251
+ /tsconfig/i,
252
+ /\.ya?ml/i,
253
+ /Dockerfile/i,
254
+ /docker-compose/i
255
+ ],
256
+ weight: 5
257
+ },
258
+ {
259
+ category: "docs",
260
+ keywords: [
261
+ "document",
262
+ "readme",
263
+ "changelog",
264
+ "jsdoc",
265
+ "comment",
266
+ "explain",
267
+ "guide",
268
+ "tutorial",
269
+ "api doc",
270
+ "openapi",
271
+ "swagger"
272
+ ],
273
+ types: ["docs"],
274
+ filePatterns: [/\.md$/i, /docs?\//i, /readme/i, /changelog/i],
275
+ weight: 5
276
+ },
277
+ {
278
+ category: "feature-dev",
279
+ keywords: [
280
+ "feature",
281
+ "implement",
282
+ "add",
283
+ "create",
284
+ "new",
285
+ "endpoint",
286
+ "component",
287
+ "module",
288
+ "service",
289
+ "handler",
290
+ "route",
291
+ "hook",
292
+ "plugin",
293
+ "integration"
294
+ ],
295
+ types: ["feature", "file-write"],
296
+ weight: 3
297
+ // lowest — generic catch-all for development
298
+ }
299
+ ];
300
+ }
301
+ });
302
+
19
303
  // src/services/sqlite/Observations.ts
20
304
  var Observations_exports = {};
21
305
  __export(Observations_exports, {
@@ -41,11 +325,23 @@ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
41
325
  }
42
326
  function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
43
327
  const now = /* @__PURE__ */ new Date();
328
+ const safeTitle = redactSecrets(title);
329
+ const safeText = text ? redactSecrets(text) : text;
330
+ const safeNarrative = narrative ? redactSecrets(narrative) : narrative;
331
+ const autoCategory = categorize({
332
+ type,
333
+ title: safeTitle,
334
+ text: safeText,
335
+ narrative: safeNarrative,
336
+ concepts,
337
+ filesModified,
338
+ filesRead
339
+ });
44
340
  const result = db.run(
45
341
  `INSERT INTO observations
46
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch, content_hash, discovery_tokens)
47
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
48
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
342
+ (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, auto_category)
343
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
344
+ [memorySessionId, project, type, safeTitle, subtitle, safeText, safeNarrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens, autoCategory]
49
345
  );
50
346
  return Number(result.lastInsertRowid);
51
347
  }
@@ -57,16 +353,16 @@ function getObservationsBySession(db, memorySessionId) {
57
353
  }
58
354
  function getObservationsByProject(db, project, limit = 100) {
59
355
  const query = db.query(
60
- "SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
356
+ "SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?"
61
357
  );
62
358
  return query.all(project, limit);
63
359
  }
64
360
  function searchObservations(db, searchTerm, project) {
65
361
  const sql = project ? `SELECT * FROM observations
66
362
  WHERE project = ? AND (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\')
67
- ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
363
+ ORDER BY created_at_epoch DESC, id DESC` : `SELECT * FROM observations
68
364
  WHERE title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\'
69
- ORDER BY created_at_epoch DESC`;
365
+ ORDER BY created_at_epoch DESC, id DESC`;
70
366
  const pattern = `%${escapeLikePattern(searchTerm)}%`;
71
367
  const query = db.query(sql);
72
368
  if (project) {
@@ -99,21 +395,32 @@ function consolidateObservations(db, project, options = {}) {
99
395
  ORDER BY cnt DESC
100
396
  `).all(project, minGroupSize);
101
397
  if (groups.length === 0) return { merged: 0, removed: 0 };
102
- let totalMerged = 0;
103
- let totalRemoved = 0;
398
+ if (options.dryRun) {
399
+ let totalMerged = 0;
400
+ let totalRemoved = 0;
401
+ for (const group of groups) {
402
+ const obsIds = group.ids.split(",").map(Number);
403
+ const placeholders = obsIds.map(() => "?").join(",");
404
+ const count = db.query(
405
+ `SELECT COUNT(*) as cnt FROM observations WHERE id IN (${placeholders})`
406
+ ).get(...obsIds)?.cnt || 0;
407
+ if (count >= minGroupSize) {
408
+ totalMerged += 1;
409
+ totalRemoved += count - 1;
410
+ }
411
+ }
412
+ return { merged: totalMerged, removed: totalRemoved };
413
+ }
104
414
  const runConsolidation = db.transaction(() => {
415
+ let merged = 0;
416
+ let removed = 0;
105
417
  for (const group of groups) {
106
418
  const obsIds = group.ids.split(",").map(Number);
107
419
  const placeholders = obsIds.map(() => "?").join(",");
108
420
  const observations = db.query(
109
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
421
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC, id DESC`
110
422
  ).all(...obsIds);
111
423
  if (observations.length < minGroupSize) continue;
112
- if (options.dryRun) {
113
- totalMerged += 1;
114
- totalRemoved += observations.length - 1;
115
- continue;
116
- }
117
424
  const keeper = observations[0];
118
425
  const others = observations.slice(1);
119
426
  const uniqueTexts = /* @__PURE__ */ new Set();
@@ -126,22 +433,24 @@ function consolidateObservations(db, project, options = {}) {
126
433
  const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
127
434
  db.run(
128
435
  "UPDATE observations SET text = ?, title = ? WHERE id = ?",
129
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
436
+ [consolidatedText, `[consolidated x${observations.length}] ${keeper.title}`, keeper.id]
130
437
  );
131
438
  const removeIds = others.map((o) => o.id);
132
439
  const removePlaceholders = removeIds.map(() => "?").join(",");
133
440
  db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
134
441
  db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
135
- totalMerged += 1;
136
- totalRemoved += removeIds.length;
442
+ merged += 1;
443
+ removed += removeIds.length;
137
444
  }
445
+ return { merged, removed };
138
446
  });
139
- runConsolidation();
140
- return { merged: totalMerged, removed: totalRemoved };
447
+ return runConsolidation();
141
448
  }
142
449
  var init_Observations = __esm({
143
450
  "src/services/sqlite/Observations.ts"() {
144
451
  "use strict";
452
+ init_secrets();
453
+ init_categorizer();
145
454
  }
146
455
  });
147
456
 
@@ -164,7 +473,7 @@ function escapeLikePattern3(input) {
164
473
  }
165
474
  function sanitizeFTS5Query(query) {
166
475
  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}"`);
476
+ const terms = trimmed.replace(/[""\u0022]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
168
477
  return terms.join(" ");
169
478
  }
170
479
  function searchObservationsFTS(db, query, filters = {}) {
@@ -261,7 +570,7 @@ function searchObservationsLIKE(db, query, filters = {}) {
261
570
  sql += " AND created_at_epoch <= ?";
262
571
  params.push(filters.dateEnd);
263
572
  }
264
- sql += " ORDER BY created_at_epoch DESC LIMIT ?";
573
+ sql += " ORDER BY created_at_epoch DESC, id DESC LIMIT ?";
265
574
  params.push(limit);
266
575
  const stmt = db.query(sql);
267
576
  return stmt.all(...params);
@@ -286,7 +595,7 @@ function searchSummariesFiltered(db, query, filters = {}) {
286
595
  sql += " AND created_at_epoch <= ?";
287
596
  params.push(filters.dateEnd);
288
597
  }
289
- sql += " ORDER BY created_at_epoch DESC LIMIT ?";
598
+ sql += " ORDER BY created_at_epoch DESC, id DESC LIMIT ?";
290
599
  params.push(limit);
291
600
  const stmt = db.query(sql);
292
601
  return stmt.all(...params);
@@ -296,7 +605,7 @@ function getObservationsByIds(db, ids) {
296
605
  const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
297
606
  if (validIds.length === 0) return [];
298
607
  const placeholders = validIds.map(() => "?").join(",");
299
- const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`;
608
+ const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC, id DESC`;
300
609
  const stmt = db.query(sql);
301
610
  return stmt.all(...validIds);
302
611
  }
@@ -308,11 +617,11 @@ function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
308
617
  const beforeStmt = db.query(`
309
618
  SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
310
619
  FROM observations
311
- WHERE created_at_epoch < ?
312
- ORDER BY created_at_epoch DESC
620
+ WHERE (created_at_epoch < ? OR (created_at_epoch = ? AND id < ?))
621
+ ORDER BY created_at_epoch DESC, id DESC
313
622
  LIMIT ?
314
623
  `);
315
- const before = beforeStmt.all(anchorEpoch, depthBefore).reverse();
624
+ const before = beforeStmt.all(anchorEpoch, anchorEpoch, anchorId, depthBefore).reverse();
316
625
  const selfStmt = db.query(`
317
626
  SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
318
627
  FROM observations WHERE id = ?
@@ -321,34 +630,46 @@ function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
321
630
  const afterStmt = db.query(`
322
631
  SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
323
632
  FROM observations
324
- WHERE created_at_epoch > ?
325
- ORDER BY created_at_epoch ASC
633
+ WHERE (created_at_epoch > ? OR (created_at_epoch = ? AND id > ?))
634
+ ORDER BY created_at_epoch ASC, id ASC
326
635
  LIMIT ?
327
636
  `);
328
- const after = afterStmt.all(anchorEpoch, depthAfter);
637
+ const after = afterStmt.all(anchorEpoch, anchorEpoch, anchorId, depthAfter);
329
638
  return [...before, ...self, ...after];
330
639
  }
331
640
  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;
641
+ const sql = `
642
+ WITH
643
+ obs_stats AS (
644
+ SELECT
645
+ COUNT(*) as count,
646
+ COALESCE(SUM(discovery_tokens), 0) as discovery_tokens,
647
+ COALESCE(SUM(
648
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
649
+ ), 0) as read_tokens
650
+ FROM observations WHERE project = ?
651
+ ),
652
+ sum_count AS (SELECT COUNT(*) as count FROM summaries WHERE project = ?),
653
+ ses_count AS (SELECT COUNT(*) as count FROM sessions WHERE project = ?),
654
+ prm_count AS (SELECT COUNT(*) as count FROM prompts WHERE project = ?)
655
+ SELECT
656
+ obs_stats.count as observations,
657
+ obs_stats.discovery_tokens,
658
+ obs_stats.read_tokens,
659
+ sum_count.count as summaries,
660
+ ses_count.count as sessions,
661
+ prm_count.count as prompts
662
+ FROM obs_stats, sum_count, ses_count, prm_count
663
+ `;
664
+ const row = db.query(sql).get(project, project, project, project);
665
+ const discoveryTokens = row?.discovery_tokens || 0;
666
+ const readTokens = row?.read_tokens || 0;
346
667
  const savings = Math.max(0, discoveryTokens - readTokens);
347
668
  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,
669
+ observations: row?.observations || 0,
670
+ summaries: row?.summaries || 0,
671
+ sessions: row?.sessions || 0,
672
+ prompts: row?.prompts || 0,
352
673
  tokenEconomics: { discoveryTokens, readTokens, savings }
353
674
  };
354
675
  }
@@ -356,7 +677,7 @@ function getStaleObservations(db, project) {
356
677
  const rows = db.query(`
357
678
  SELECT * FROM observations
358
679
  WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
359
- ORDER BY created_at_epoch DESC
680
+ ORDER BY created_at_epoch DESC, id DESC
360
681
  LIMIT 500
361
682
  `).all(project);
362
683
  const staleObs = [];
@@ -497,7 +818,7 @@ async function readStdin() {
497
818
  }
498
819
  resolve(JSON.parse(data));
499
820
  } catch (err) {
500
- reject(new Error(`Errore parsing stdin JSON: ${err}`));
821
+ reject(new Error(`Error parsing stdin JSON: ${err}`));
501
822
  }
502
823
  });
503
824
  process.stdin.on("error", (err) => {
@@ -523,17 +844,17 @@ function formatSmartContext(data) {
523
844
  const budget = data.tokenBudget || parseInt(process.env.KIRO_MEMORY_CONTEXT_TOKENS || "0", 10) || 2e3;
524
845
  let output = "";
525
846
  let tokensUsed = 0;
526
- const header = "# Kiro Memory: Contesto Sessioni Precedenti\n\n";
847
+ const header = "# Kiro Memory: Previous Sessions Context\n\n";
527
848
  tokensUsed += estimateTokens(header);
528
849
  output += header;
529
850
  if (data.summaries && data.summaries.length > 0) {
530
- let sumSection = "## Sessioni Precedenti\n\n";
851
+ let sumSection = "## Previous Sessions\n\n";
531
852
  for (const sum of data.summaries.slice(0, 3)) {
532
- if (sum.learned) sumSection += `- **Appreso**: ${sum.learned}
853
+ if (sum.learned) sumSection += `- **Learned**: ${sum.learned}
533
854
  `;
534
- if (sum.completed) sumSection += `- **Completato**: ${sum.completed}
855
+ if (sum.completed) sumSection += `- **Completed**: ${sum.completed}
535
856
  `;
536
- if (sum.next_steps) sumSection += `- **Prossimi passi**: ${sum.next_steps}
857
+ if (sum.next_steps) sumSection += `- **Next steps**: ${sum.next_steps}
537
858
  `;
538
859
  sumSection += "\n";
539
860
  }
@@ -541,7 +862,7 @@ function formatSmartContext(data) {
541
862
  output += sumSection;
542
863
  }
543
864
  if (data.items && data.items.length > 0) {
544
- let obsSection = "## Osservazioni Rilevanti\n\n";
865
+ let obsSection = "## Relevant Observations\n\n";
545
866
  tokensUsed += estimateTokens(obsSection);
546
867
  const sorted = [...data.items].sort((a, b) => b.score - a.score);
547
868
  for (const item of sorted) {
@@ -560,7 +881,7 @@ function formatSmartContext(data) {
560
881
  output += obsSection;
561
882
  }
562
883
  const footer = `
563
- > Progetto: ${data.project} | Items: ${data.items?.length || 0} | Token usati: ~${tokensUsed}/${budget}
884
+ > Project: ${data.project} | Items: ${data.items?.length || 0} | Tokens used: ~${tokensUsed}/${budget}
564
885
  `;
565
886
  output += footer;
566
887
  return output;
@@ -576,11 +897,11 @@ async function runHook(name, handler) {
576
897
  }
577
898
  debugLog(name, "stdin", input);
578
899
  await handler(input);
579
- debugLog(name, "completato", { success: true });
900
+ debugLog(name, "completed", { success: true });
580
901
  process.exit(0);
581
902
  } catch (error) {
582
- debugLog(name, "errore", { error: String(error) });
583
- process.stderr.write(`[kiro-memory:${name}] Errore: ${error}
903
+ debugLog(name, "error", { error: String(error) });
904
+ process.stderr.write(`[kiro-memory:${name}] Error: ${error}
584
905
  `);
585
906
  process.exit(0);
586
907
  }
@@ -590,14 +911,15 @@ async function runHook(name, handler) {
590
911
  import BetterSqlite3 from "better-sqlite3";
591
912
  var Database = class {
592
913
  _db;
914
+ _stmtCache = /* @__PURE__ */ new Map();
593
915
  constructor(path, options) {
594
916
  this._db = new BetterSqlite3(path, {
595
- // better-sqlite3 crea il file di default (non serve 'create')
917
+ // better-sqlite3 creates the file by default ('create' not needed)
596
918
  readonly: options?.readwrite === false ? true : false
597
919
  });
598
920
  }
599
921
  /**
600
- * Esegui una query SQL senza risultati
922
+ * Execute a SQL query without results
601
923
  */
602
924
  run(sql, params) {
603
925
  const stmt = this._db.prepare(sql);
@@ -605,51 +927,53 @@ var Database = class {
605
927
  return result;
606
928
  }
607
929
  /**
608
- * Prepara una query con interfaccia compatibile bun:sqlite
930
+ * Prepare a query with bun:sqlite-compatible interface.
931
+ * Returns a cached prepared statement for repeated queries.
609
932
  */
610
933
  query(sql) {
611
- return new BunQueryCompat(this._db, sql);
934
+ let cached = this._stmtCache.get(sql);
935
+ if (!cached) {
936
+ cached = new BunQueryCompat(this._db, sql);
937
+ this._stmtCache.set(sql, cached);
938
+ }
939
+ return cached;
612
940
  }
613
941
  /**
614
- * Crea una transazione
942
+ * Create a transaction
615
943
  */
616
944
  transaction(fn) {
617
945
  return this._db.transaction(fn);
618
946
  }
619
947
  /**
620
- * Chiudi la connessione
948
+ * Close the connection
621
949
  */
622
950
  close() {
951
+ this._stmtCache.clear();
623
952
  this._db.close();
624
953
  }
625
954
  };
626
955
  var BunQueryCompat = class {
627
- _db;
628
- _sql;
956
+ _stmt;
629
957
  constructor(db, sql) {
630
- this._db = db;
631
- this._sql = sql;
958
+ this._stmt = db.prepare(sql);
632
959
  }
633
960
  /**
634
- * Restituisce tutte le righe
961
+ * Returns all rows
635
962
  */
636
963
  all(...params) {
637
- const stmt = this._db.prepare(this._sql);
638
- return params.length > 0 ? stmt.all(...params) : stmt.all();
964
+ return params.length > 0 ? this._stmt.all(...params) : this._stmt.all();
639
965
  }
640
966
  /**
641
- * Restituisce la prima riga o null
967
+ * Returns the first row or null
642
968
  */
643
969
  get(...params) {
644
- const stmt = this._db.prepare(this._sql);
645
- return params.length > 0 ? stmt.get(...params) : stmt.get();
970
+ return params.length > 0 ? this._stmt.get(...params) : this._stmt.get();
646
971
  }
647
972
  /**
648
- * Esegui senza risultati
973
+ * Execute without results
649
974
  */
650
975
  run(...params) {
651
- const stmt = this._db.prepare(this._sql);
652
- return params.length > 0 ? stmt.run(...params) : stmt.run();
976
+ return params.length > 0 ? this._stmt.run(...params) : this._stmt.run();
653
977
  }
654
978
  };
655
979
 
@@ -911,40 +1235,62 @@ function ensureDir(dirPath) {
911
1235
  var SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024;
912
1236
  var SQLITE_CACHE_SIZE_PAGES = 1e4;
913
1237
  var KiroMemoryDatabase = class {
914
- db;
1238
+ _db;
915
1239
  /**
916
- * @param dbPath - Percorso al file SQLite (default: DB_PATH)
917
- * @param skipMigrations - Se true, salta il migration runner (per hook ad alta frequenza)
1240
+ * Readonly accessor for the underlying Database instance.
1241
+ * Prefer using query() and run() proxy methods directly.
1242
+ */
1243
+ get db() {
1244
+ return this._db;
1245
+ }
1246
+ /**
1247
+ * @param dbPath - Path to the SQLite file (default: DB_PATH)
1248
+ * @param skipMigrations - If true, skip the migration runner (for high-frequency hooks)
918
1249
  */
919
1250
  constructor(dbPath = DB_PATH, skipMigrations = false) {
920
1251
  if (dbPath !== ":memory:") {
921
1252
  ensureDir(DATA_DIR2);
922
1253
  }
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}`);
1254
+ this._db = new Database(dbPath, { create: true, readwrite: true });
1255
+ this._db.run("PRAGMA journal_mode = WAL");
1256
+ this._db.run("PRAGMA busy_timeout = 5000");
1257
+ this._db.run("PRAGMA synchronous = NORMAL");
1258
+ this._db.run("PRAGMA foreign_keys = ON");
1259
+ this._db.run("PRAGMA temp_store = memory");
1260
+ this._db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
1261
+ this._db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
930
1262
  if (!skipMigrations) {
931
- const migrationRunner = new MigrationRunner(this.db);
1263
+ const migrationRunner = new MigrationRunner(this._db);
932
1264
  migrationRunner.runAllMigrations();
933
1265
  }
934
1266
  }
935
1267
  /**
936
- * Esegue una funzione all'interno di una transazione atomica.
937
- * Se fn() lancia un errore, la transazione viene annullata automaticamente.
1268
+ * Prepare a query (delegates to underlying Database).
1269
+ * Proxy method to avoid ctx.db.db.query() double access.
1270
+ */
1271
+ query(sql) {
1272
+ return this._db.query(sql);
1273
+ }
1274
+ /**
1275
+ * Execute a SQL statement without results (delegates to underlying Database).
1276
+ * Proxy method to avoid ctx.db.db.run() double access.
1277
+ */
1278
+ run(sql, params) {
1279
+ return this._db.run(sql, params);
1280
+ }
1281
+ /**
1282
+ * Executes a function within an atomic transaction.
1283
+ * If fn() throws an error, the transaction is automatically rolled back.
938
1284
  */
939
1285
  withTransaction(fn) {
940
- const transaction = this.db.transaction(fn);
941
- return transaction(this.db);
1286
+ const transaction = this._db.transaction(fn);
1287
+ return transaction(this._db);
942
1288
  }
943
1289
  /**
944
1290
  * Close the database connection
945
1291
  */
946
1292
  close() {
947
- this.db.close();
1293
+ this._db.close();
948
1294
  }
949
1295
  };
950
1296
  var MigrationRunner = class {
@@ -1185,11 +1531,104 @@ var MigrationRunner = class {
1185
1531
  db.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_epoch ON summaries(project, created_at_epoch DESC)");
1186
1532
  db.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_epoch ON prompts(project, created_at_epoch DESC)");
1187
1533
  }
1534
+ },
1535
+ {
1536
+ version: 10,
1537
+ up: (db) => {
1538
+ db.run(`
1539
+ CREATE TABLE IF NOT EXISTS job_queue (
1540
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1541
+ type TEXT NOT NULL,
1542
+ status TEXT NOT NULL DEFAULT 'pending',
1543
+ payload TEXT,
1544
+ result TEXT,
1545
+ error TEXT,
1546
+ retry_count INTEGER DEFAULT 0,
1547
+ max_retries INTEGER DEFAULT 3,
1548
+ priority INTEGER DEFAULT 0,
1549
+ created_at TEXT NOT NULL,
1550
+ created_at_epoch INTEGER NOT NULL,
1551
+ started_at_epoch INTEGER,
1552
+ completed_at_epoch INTEGER
1553
+ )
1554
+ `);
1555
+ db.run("CREATE INDEX IF NOT EXISTS idx_jobs_status ON job_queue(status)");
1556
+ db.run("CREATE INDEX IF NOT EXISTS idx_jobs_type ON job_queue(type)");
1557
+ db.run("CREATE INDEX IF NOT EXISTS idx_jobs_priority ON job_queue(status, priority DESC, created_at_epoch ASC)");
1558
+ }
1559
+ },
1560
+ {
1561
+ version: 11,
1562
+ up: (db) => {
1563
+ db.run("ALTER TABLE observations ADD COLUMN auto_category TEXT");
1564
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_category ON observations(auto_category)");
1565
+ }
1566
+ },
1567
+ {
1568
+ version: 12,
1569
+ up: (db) => {
1570
+ db.run(`
1571
+ CREATE TABLE IF NOT EXISTS github_links (
1572
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1573
+ observation_id INTEGER,
1574
+ session_id TEXT,
1575
+ repo TEXT NOT NULL,
1576
+ issue_number INTEGER,
1577
+ pr_number INTEGER,
1578
+ event_type TEXT NOT NULL,
1579
+ action TEXT,
1580
+ title TEXT,
1581
+ url TEXT,
1582
+ author TEXT,
1583
+ created_at TEXT NOT NULL,
1584
+ created_at_epoch INTEGER NOT NULL,
1585
+ FOREIGN KEY (observation_id) REFERENCES observations(id)
1586
+ )
1587
+ `);
1588
+ db.run("CREATE INDEX IF NOT EXISTS idx_github_links_repo ON github_links(repo)");
1589
+ db.run("CREATE INDEX IF NOT EXISTS idx_github_links_obs ON github_links(observation_id)");
1590
+ db.run("CREATE INDEX IF NOT EXISTS idx_github_links_event ON github_links(event_type)");
1591
+ db.run("CREATE INDEX IF NOT EXISTS idx_github_links_repo_issue ON github_links(repo, issue_number)");
1592
+ db.run("CREATE INDEX IF NOT EXISTS idx_github_links_repo_pr ON github_links(repo, pr_number)");
1593
+ }
1594
+ },
1595
+ {
1596
+ version: 13,
1597
+ up: (db) => {
1598
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_keyset ON observations(created_at_epoch DESC, id DESC)");
1599
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_keyset ON observations(project, created_at_epoch DESC, id DESC)");
1600
+ db.run("CREATE INDEX IF NOT EXISTS idx_summaries_keyset ON summaries(created_at_epoch DESC, id DESC)");
1601
+ db.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_keyset ON summaries(project, created_at_epoch DESC, id DESC)");
1602
+ db.run("CREATE INDEX IF NOT EXISTS idx_prompts_keyset ON prompts(created_at_epoch DESC, id DESC)");
1603
+ db.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_keyset ON prompts(project, created_at_epoch DESC, id DESC)");
1604
+ }
1188
1605
  }
1189
1606
  ];
1190
1607
  }
1191
1608
  };
1192
1609
 
1610
+ // src/services/sqlite/cursor.ts
1611
+ function encodeCursor(id, epoch) {
1612
+ const raw = `${epoch}:${id}`;
1613
+ return Buffer.from(raw, "utf8").toString("base64url");
1614
+ }
1615
+ function decodeCursor(cursor) {
1616
+ try {
1617
+ const raw = Buffer.from(cursor, "base64url").toString("utf8");
1618
+ const colonIdx = raw.indexOf(":");
1619
+ if (colonIdx === -1) return null;
1620
+ const epochStr = raw.substring(0, colonIdx);
1621
+ const idStr = raw.substring(colonIdx + 1);
1622
+ const epoch = parseInt(epochStr, 10);
1623
+ const id = parseInt(idStr, 10);
1624
+ if (!Number.isInteger(epoch) || epoch <= 0) return null;
1625
+ if (!Number.isInteger(id) || id <= 0) return null;
1626
+ return { epoch, id };
1627
+ } catch {
1628
+ return null;
1629
+ }
1630
+ }
1631
+
1193
1632
  // src/services/sqlite/Sessions.ts
1194
1633
  function createSession(db, contentSessionId, project, userPrompt) {
1195
1634
  const now = /* @__PURE__ */ new Date();
@@ -1233,16 +1672,16 @@ function createSummary(db, sessionId, project, request, investigated, learned, c
1233
1672
  }
1234
1673
  function getSummariesByProject(db, project, limit = 50) {
1235
1674
  const query = db.query(
1236
- "SELECT * FROM summaries WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
1675
+ "SELECT * FROM summaries WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?"
1237
1676
  );
1238
1677
  return query.all(project, limit);
1239
1678
  }
1240
1679
  function searchSummaries(db, searchTerm, project) {
1241
1680
  const sql = project ? `SELECT * FROM summaries
1242
1681
  WHERE project = ? AND (request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\')
1243
- ORDER BY created_at_epoch DESC` : `SELECT * FROM summaries
1682
+ ORDER BY created_at_epoch DESC, id DESC` : `SELECT * FROM summaries
1244
1683
  WHERE request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\'
1245
- ORDER BY created_at_epoch DESC`;
1684
+ ORDER BY created_at_epoch DESC, id DESC`;
1246
1685
  const pattern = `%${escapeLikePattern2(searchTerm)}%`;
1247
1686
  const query = db.query(sql);
1248
1687
  if (project) {
@@ -1264,7 +1703,7 @@ function createPrompt(db, contentSessionId, project, promptNumber, promptText) {
1264
1703
  }
1265
1704
  function getPromptsByProject(db, project, limit = 100) {
1266
1705
  const query = db.query(
1267
- "SELECT * FROM prompts WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
1706
+ "SELECT * FROM prompts WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?"
1268
1707
  );
1269
1708
  return query.all(project, limit);
1270
1709
  }
@@ -1292,13 +1731,13 @@ function createCheckpoint(db, sessionId, project, data) {
1292
1731
  }
1293
1732
  function getLatestCheckpoint(db, sessionId) {
1294
1733
  const query = db.query(
1295
- "SELECT * FROM checkpoints WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1"
1734
+ "SELECT * FROM checkpoints WHERE session_id = ? ORDER BY created_at_epoch DESC, id DESC LIMIT 1"
1296
1735
  );
1297
1736
  return query.get(sessionId);
1298
1737
  }
1299
1738
  function getLatestCheckpointByProject(db, project) {
1300
1739
  const query = db.query(
1301
- "SELECT * FROM checkpoints WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1"
1740
+ "SELECT * FROM checkpoints WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT 1"
1302
1741
  );
1303
1742
  return query.get(project);
1304
1743
  }
@@ -1360,9 +1799,9 @@ function getReportData(db, project, startEpoch, endEpoch) {
1360
1799
  const staleCount = (project ? db.query(staleSql).get(project, startEpoch, endEpoch)?.count : db.query(staleSql).get(startEpoch, endEpoch)?.count) || 0;
1361
1800
  const summarySql = project ? `SELECT learned, completed, next_steps FROM summaries
1362
1801
  WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
1363
- ORDER BY created_at_epoch DESC` : `SELECT learned, completed, next_steps FROM summaries
1802
+ ORDER BY created_at_epoch DESC, id DESC` : `SELECT learned, completed, next_steps FROM summaries
1364
1803
  WHERE created_at_epoch >= ? AND created_at_epoch <= ?
1365
- ORDER BY created_at_epoch DESC`;
1804
+ ORDER BY created_at_epoch DESC, id DESC`;
1366
1805
  const summaryRows = project ? db.query(summarySql).all(project, startEpoch, endEpoch) : db.query(summarySql).all(startEpoch, endEpoch);
1367
1806
  const topLearnings = [];
1368
1807
  const completedTasks = [];
@@ -1427,20 +1866,61 @@ function getReportData(db, project, startEpoch, endEpoch) {
1427
1866
  // src/services/sqlite/index.ts
1428
1867
  init_Search();
1429
1868
 
1869
+ // src/types/worker-types.ts
1870
+ var KNOWLEDGE_TYPES = ["constraint", "decision", "heuristic", "rejected"];
1871
+
1872
+ // src/services/sqlite/Retention.ts
1873
+ var KNOWLEDGE_TYPE_LIST = KNOWLEDGE_TYPES;
1874
+ var KNOWLEDGE_PLACEHOLDERS = KNOWLEDGE_TYPE_LIST.map(() => "?").join(", ");
1875
+
1430
1876
  // src/sdk/index.ts
1431
1877
  init_Observations();
1432
1878
  import { createHash } from "crypto";
1433
1879
  init_Search();
1434
1880
 
1435
1881
  // src/services/search/EmbeddingService.ts
1882
+ var MODEL_CONFIGS = {
1883
+ "all-MiniLM-L6-v2": {
1884
+ modelId: "Xenova/all-MiniLM-L6-v2",
1885
+ dimensions: 384
1886
+ },
1887
+ "jina-code-v2": {
1888
+ modelId: "jinaai/jina-embeddings-v2-base-code",
1889
+ dimensions: 768
1890
+ },
1891
+ "bge-small-en": {
1892
+ modelId: "BAAI/bge-small-en-v1.5",
1893
+ dimensions: 384
1894
+ }
1895
+ };
1896
+ var FASTEMBED_COMPATIBLE_MODELS = /* @__PURE__ */ new Set(["all-MiniLM-L6-v2", "bge-small-en"]);
1436
1897
  var EmbeddingService = class {
1437
1898
  provider = null;
1438
1899
  model = null;
1439
1900
  initialized = false;
1440
1901
  initializing = null;
1902
+ config;
1903
+ configName;
1904
+ constructor() {
1905
+ const envModel = process.env.KIRO_MEMORY_EMBEDDING_MODEL || "all-MiniLM-L6-v2";
1906
+ this.configName = envModel;
1907
+ if (MODEL_CONFIGS[envModel]) {
1908
+ this.config = MODEL_CONFIGS[envModel];
1909
+ } else if (envModel.includes("/")) {
1910
+ const dimensions = parseInt(process.env.KIRO_MEMORY_EMBEDDING_DIMENSIONS || "384", 10);
1911
+ this.config = {
1912
+ modelId: envModel,
1913
+ dimensions: isNaN(dimensions) ? 384 : dimensions
1914
+ };
1915
+ } else {
1916
+ logger.warn("EMBEDDING", `Unknown model name '${envModel}', falling back to 'all-MiniLM-L6-v2'`);
1917
+ this.configName = "all-MiniLM-L6-v2";
1918
+ this.config = MODEL_CONFIGS["all-MiniLM-L6-v2"];
1919
+ }
1920
+ }
1441
1921
  /**
1442
- * Inizializza il servizio di embedding.
1443
- * Tenta fastembed, poi @huggingface/transformers, poi fallback a null.
1922
+ * Initialize the embedding service.
1923
+ * Tries fastembed (when compatible), then @huggingface/transformers, then falls back to null.
1444
1924
  */
1445
1925
  async initialize() {
1446
1926
  if (this.initialized) return this.provider !== null;
@@ -1451,45 +1931,48 @@ var EmbeddingService = class {
1451
1931
  return result;
1452
1932
  }
1453
1933
  async _doInitialize() {
1454
- try {
1455
- const fastembed = await import("fastembed");
1456
- const EmbeddingModel = fastembed.EmbeddingModel || fastembed.default?.EmbeddingModel;
1457
- const FlagEmbedding = fastembed.FlagEmbedding || fastembed.default?.FlagEmbedding;
1458
- if (FlagEmbedding && EmbeddingModel) {
1459
- this.model = await FlagEmbedding.init({
1460
- model: EmbeddingModel.BGESmallENV15
1461
- });
1462
- this.provider = "fastembed";
1463
- this.initialized = true;
1464
- logger.info("EMBEDDING", "Inizializzato con fastembed (BGE-small-en-v1.5)");
1465
- return true;
1934
+ const fastembedCompatible = FASTEMBED_COMPATIBLE_MODELS.has(this.configName);
1935
+ if (fastembedCompatible) {
1936
+ try {
1937
+ const fastembed = await import("fastembed");
1938
+ const EmbeddingModel = fastembed.EmbeddingModel || fastembed.default?.EmbeddingModel;
1939
+ const FlagEmbedding = fastembed.FlagEmbedding || fastembed.default?.FlagEmbedding;
1940
+ if (FlagEmbedding && EmbeddingModel) {
1941
+ this.model = await FlagEmbedding.init({
1942
+ model: EmbeddingModel.BGESmallENV15
1943
+ });
1944
+ this.provider = "fastembed";
1945
+ this.initialized = true;
1946
+ logger.info("EMBEDDING", `Initialized with fastembed (BGE-small-en-v1.5) for model '${this.configName}'`);
1947
+ return true;
1948
+ }
1949
+ } catch (error) {
1950
+ logger.debug("EMBEDDING", `fastembed not available: ${error}`);
1466
1951
  }
1467
- } catch (error) {
1468
- logger.debug("EMBEDDING", `fastembed non disponibile: ${error}`);
1469
1952
  }
1470
1953
  try {
1471
1954
  const transformers = await import("@huggingface/transformers");
1472
1955
  const pipeline = transformers.pipeline || transformers.default?.pipeline;
1473
1956
  if (pipeline) {
1474
- this.model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
1957
+ this.model = await pipeline("feature-extraction", this.config.modelId, {
1475
1958
  quantized: true
1476
1959
  });
1477
1960
  this.provider = "transformers";
1478
1961
  this.initialized = true;
1479
- logger.info("EMBEDDING", "Inizializzato con @huggingface/transformers (all-MiniLM-L6-v2)");
1962
+ logger.info("EMBEDDING", `Initialized with @huggingface/transformers (${this.config.modelId})`);
1480
1963
  return true;
1481
1964
  }
1482
1965
  } catch (error) {
1483
- logger.debug("EMBEDDING", `@huggingface/transformers non disponibile: ${error}`);
1966
+ logger.debug("EMBEDDING", `@huggingface/transformers not available: ${error}`);
1484
1967
  }
1485
1968
  this.provider = null;
1486
1969
  this.initialized = true;
1487
- logger.warn("EMBEDDING", "Nessun provider embedding disponibile, ricerca semantica disabilitata");
1970
+ logger.warn("EMBEDDING", "No embedding provider available, semantic search disabled");
1488
1971
  return false;
1489
1972
  }
1490
1973
  /**
1491
- * Genera embedding per un singolo testo.
1492
- * Ritorna Float32Array con 384 dimensioni, o null se non disponibile.
1974
+ * Generate embedding for a single text.
1975
+ * Returns Float32Array with configured dimensions, or null if not available.
1493
1976
  */
1494
1977
  async embed(text) {
1495
1978
  if (!this.initialized) await this.initialize();
@@ -1502,46 +1985,118 @@ var EmbeddingService = class {
1502
1985
  return await this._embedTransformers(truncated);
1503
1986
  }
1504
1987
  } catch (error) {
1505
- logger.error("EMBEDDING", `Errore generazione embedding: ${error}`);
1988
+ logger.error("EMBEDDING", `Error generating embedding: ${error}`);
1506
1989
  }
1507
1990
  return null;
1508
1991
  }
1509
1992
  /**
1510
- * Genera embeddings in batch.
1993
+ * Generate embeddings in batch.
1994
+ * Uses native batch support when available (fastembed, transformers),
1995
+ * falls back to serial processing on batch failure.
1511
1996
  */
1512
1997
  async embedBatch(texts) {
1513
1998
  if (!this.initialized) await this.initialize();
1514
1999
  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);
2000
+ if (texts.length === 0) return [];
2001
+ const truncated = texts.map((t) => t.substring(0, 2e3));
2002
+ try {
2003
+ if (this.provider === "fastembed") {
2004
+ return await this._embedBatchFastembed(truncated);
2005
+ } else if (this.provider === "transformers") {
2006
+ return await this._embedBatchTransformers(truncated);
1522
2007
  }
2008
+ } catch (error) {
2009
+ logger.warn("EMBEDDING", `Batch embedding failed, falling back to serial: ${error}`);
1523
2010
  }
1524
- return results;
2011
+ return this._embedBatchSerial(truncated);
1525
2012
  }
1526
2013
  /**
1527
- * Verifica se il servizio è disponibile.
2014
+ * Check if the service is available.
1528
2015
  */
1529
2016
  isAvailable() {
1530
2017
  return this.initialized && this.provider !== null;
1531
2018
  }
1532
2019
  /**
1533
- * Nome del provider attivo.
2020
+ * Name of the active provider.
1534
2021
  */
1535
2022
  getProvider() {
1536
2023
  return this.provider;
1537
2024
  }
1538
2025
  /**
1539
- * Dimensioni del vettore embedding.
2026
+ * Embedding vector dimensions for the active model configuration.
1540
2027
  */
1541
2028
  getDimensions() {
1542
- return 384;
2029
+ return this.config.dimensions;
2030
+ }
2031
+ /**
2032
+ * Human-readable model name used as identifier in the observation_embeddings table.
2033
+ * Returns the short name (e.g., 'all-MiniLM-L6-v2') or the full HF model ID for custom models.
2034
+ */
2035
+ getModelName() {
2036
+ return this.configName;
2037
+ }
2038
+ // --- Batch implementations ---
2039
+ /**
2040
+ * Native batch embedding with fastembed.
2041
+ * FlagEmbedding.embed() accepts string[] and returns an async iterable of batches.
2042
+ */
2043
+ async _embedBatchFastembed(texts) {
2044
+ const results = [];
2045
+ const embeddings = this.model.embed(texts, texts.length);
2046
+ for await (const batch of embeddings) {
2047
+ if (batch) {
2048
+ for (const vec of batch) {
2049
+ results.push(vec instanceof Float32Array ? vec : new Float32Array(vec));
2050
+ }
2051
+ }
2052
+ }
2053
+ while (results.length < texts.length) {
2054
+ results.push(null);
2055
+ }
2056
+ return results;
1543
2057
  }
1544
- // --- Provider specifici ---
2058
+ /**
2059
+ * Batch embedding with @huggingface/transformers pipeline.
2060
+ * The pipeline accepts string[] and returns a Tensor with shape [N, dims].
2061
+ */
2062
+ async _embedBatchTransformers(texts) {
2063
+ const output = await this.model(texts, {
2064
+ pooling: "mean",
2065
+ normalize: true
2066
+ });
2067
+ if (!output?.data) {
2068
+ return texts.map(() => null);
2069
+ }
2070
+ const dims = this.getDimensions();
2071
+ const data = output.data instanceof Float32Array ? output.data : new Float32Array(output.data);
2072
+ const results = [];
2073
+ for (let i = 0; i < texts.length; i++) {
2074
+ const offset = i * dims;
2075
+ if (offset + dims <= data.length) {
2076
+ results.push(data.slice(offset, offset + dims));
2077
+ } else {
2078
+ results.push(null);
2079
+ }
2080
+ }
2081
+ return results;
2082
+ }
2083
+ /**
2084
+ * Serial fallback: embed texts one at a time.
2085
+ * Used when native batch fails.
2086
+ */
2087
+ async _embedBatchSerial(texts) {
2088
+ const results = [];
2089
+ for (const text of texts) {
2090
+ try {
2091
+ const embedding = await this.embed(text);
2092
+ results.push(embedding);
2093
+ } catch {
2094
+ results.push(null);
2095
+ }
2096
+ }
2097
+ return results;
2098
+ }
2099
+ // --- Single-text provider implementations ---
1545
2100
  async _embedFastembed(text) {
1546
2101
  const embeddings = this.model.embed([text], 1);
1547
2102
  for await (const batch of embeddings) {
@@ -1572,17 +2127,21 @@ function getEmbeddingService() {
1572
2127
  }
1573
2128
 
1574
2129
  // src/services/search/VectorSearch.ts
2130
+ var DEFAULT_MAX_CANDIDATES = 2e3;
1575
2131
  function cosineSimilarity(a, b) {
1576
- if (a.length !== b.length) return 0;
2132
+ const len = a.length;
2133
+ if (len !== b.length) return 0;
1577
2134
  let dotProduct = 0;
1578
2135
  let normA = 0;
1579
2136
  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);
2137
+ for (let i = 0; i < len; i++) {
2138
+ const ai = a[i];
2139
+ const bi = b[i];
2140
+ dotProduct += ai * bi;
2141
+ normA += ai * ai;
2142
+ normB += bi * bi;
2143
+ }
2144
+ const denominator = Math.sqrt(normA * normB);
1586
2145
  if (denominator === 0) return 0;
1587
2146
  return dotProduct / denominator;
1588
2147
  }
@@ -1595,23 +2154,36 @@ function bufferToFloat32(buf) {
1595
2154
  }
1596
2155
  var VectorSearch = class {
1597
2156
  /**
1598
- * Ricerca semantica: calcola cosine similarity tra query e tutti gli embeddings.
2157
+ * Semantic search with SQL pre-filtering for scalability.
2158
+ *
2159
+ * 2-phase strategy:
2160
+ * 1. SQL pre-filters by project + sorts by recency (loads max N candidates)
2161
+ * 2. JS computes cosine similarity only on filtered candidates
2162
+ *
2163
+ * With 50k observations and maxCandidates=2000, loads only ~4% of data.
1599
2164
  */
1600
2165
  async search(db, queryEmbedding, options = {}) {
1601
2166
  const limit = options.limit || 10;
1602
2167
  const threshold = options.threshold || 0.3;
2168
+ const maxCandidates = options.maxCandidates || DEFAULT_MAX_CANDIDATES;
1603
2169
  try {
1604
- let sql = `
2170
+ const conditions = [];
2171
+ const params = [];
2172
+ if (options.project) {
2173
+ conditions.push("o.project = ?");
2174
+ params.push(options.project);
2175
+ }
2176
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2177
+ const sql = `
1605
2178
  SELECT e.observation_id, e.embedding,
1606
2179
  o.title, o.text, o.type, o.project, o.created_at, o.created_at_epoch
1607
2180
  FROM observation_embeddings e
1608
2181
  JOIN observations o ON o.id = e.observation_id
2182
+ ${whereClause}
2183
+ ORDER BY o.created_at_epoch DESC
2184
+ LIMIT ?
1609
2185
  `;
1610
- const params = [];
1611
- if (options.project) {
1612
- sql += " WHERE o.project = ?";
1613
- params.push(options.project);
1614
- }
2186
+ params.push(maxCandidates);
1615
2187
  const rows = db.query(sql).all(...params);
1616
2188
  const scored = [];
1617
2189
  for (const row of rows) {
@@ -1632,14 +2204,15 @@ var VectorSearch = class {
1632
2204
  }
1633
2205
  }
1634
2206
  scored.sort((a, b) => b.similarity - a.similarity);
2207
+ logger.debug("VECTOR", `Search: ${rows.length} candidates \u2192 ${scored.length} above threshold \u2192 ${Math.min(scored.length, limit)} results`);
1635
2208
  return scored.slice(0, limit);
1636
2209
  } catch (error) {
1637
- logger.error("VECTOR", `Errore ricerca vettoriale: ${error}`);
2210
+ logger.error("VECTOR", `Vector search error: ${error}`);
1638
2211
  return [];
1639
2212
  }
1640
2213
  }
1641
2214
  /**
1642
- * Salva embedding per un'osservazione.
2215
+ * Store embedding for an observation.
1643
2216
  */
1644
2217
  async storeEmbedding(db, observationId, embedding, model) {
1645
2218
  try {
@@ -1655,18 +2228,18 @@ var VectorSearch = class {
1655
2228
  embedding.length,
1656
2229
  (/* @__PURE__ */ new Date()).toISOString()
1657
2230
  );
1658
- logger.debug("VECTOR", `Embedding salvato per osservazione ${observationId}`);
2231
+ logger.debug("VECTOR", `Embedding saved for observation ${observationId}`);
1659
2232
  } catch (error) {
1660
- logger.error("VECTOR", `Errore salvataggio embedding: ${error}`);
2233
+ logger.error("VECTOR", `Error saving embedding: ${error}`);
1661
2234
  }
1662
2235
  }
1663
2236
  /**
1664
- * Genera embeddings per osservazioni che non li hanno ancora.
2237
+ * Generate embeddings for observations that don't have them yet.
1665
2238
  */
1666
2239
  async backfillEmbeddings(db, batchSize = 50) {
1667
2240
  const embeddingService2 = getEmbeddingService();
1668
2241
  if (!await embeddingService2.initialize()) {
1669
- logger.warn("VECTOR", "Embedding service non disponibile, backfill saltato");
2242
+ logger.warn("VECTOR", "Embedding service not available, backfill skipped");
1670
2243
  return 0;
1671
2244
  }
1672
2245
  const rows = db.query(`
@@ -1679,7 +2252,7 @@ var VectorSearch = class {
1679
2252
  `).all(batchSize);
1680
2253
  if (rows.length === 0) return 0;
1681
2254
  let count = 0;
1682
- const model = embeddingService2.getProvider() || "unknown";
2255
+ const model = embeddingService2.getModelName();
1683
2256
  for (const row of rows) {
1684
2257
  const parts = [row.title];
1685
2258
  if (row.text) parts.push(row.text);
@@ -1692,11 +2265,11 @@ var VectorSearch = class {
1692
2265
  count++;
1693
2266
  }
1694
2267
  }
1695
- logger.info("VECTOR", `Backfill completato: ${count}/${rows.length} embeddings generati`);
2268
+ logger.info("VECTOR", `Backfill completed: ${count}/${rows.length} embeddings generated`);
1696
2269
  return count;
1697
2270
  }
1698
2271
  /**
1699
- * Statistiche sugli embeddings.
2272
+ * Embedding statistics.
1700
2273
  */
1701
2274
  getStats(db) {
1702
2275
  try {
@@ -1723,21 +2296,21 @@ function getVectorSearch() {
1723
2296
  var HybridSearch = class {
1724
2297
  embeddingInitialized = false;
1725
2298
  /**
1726
- * Inizializza il servizio di embedding (lazy, non bloccante)
2299
+ * Initialize the embedding service (lazy, non-blocking)
1727
2300
  */
1728
2301
  async initialize() {
1729
2302
  try {
1730
2303
  const embeddingService2 = getEmbeddingService();
1731
2304
  await embeddingService2.initialize();
1732
2305
  this.embeddingInitialized = embeddingService2.isAvailable();
1733
- logger.info("SEARCH", `HybridSearch inizializzato (embedding: ${this.embeddingInitialized ? "attivo" : "disattivato"})`);
2306
+ logger.info("SEARCH", `HybridSearch initialized (embedding: ${this.embeddingInitialized ? "active" : "disabled"})`);
1734
2307
  } catch (error) {
1735
- logger.warn("SEARCH", "Inizializzazione embedding fallita, uso solo FTS5", {}, error);
2308
+ logger.warn("SEARCH", "Embedding initialization failed, using only FTS5", {}, error);
1736
2309
  this.embeddingInitialized = false;
1737
2310
  }
1738
2311
  }
1739
2312
  /**
1740
- * Ricerca ibrida con scoring a 4 segnali
2313
+ * Hybrid search with 4-signal scoring
1741
2314
  */
1742
2315
  async search(db, query, options = {}) {
1743
2316
  const limit = options.limit || 10;
@@ -1753,7 +2326,7 @@ var HybridSearch = class {
1753
2326
  const vectorResults = await vectorSearch2.search(db, queryEmbedding, {
1754
2327
  project: options.project,
1755
2328
  limit: limit * 2,
1756
- // Prendiamo piu risultati per il ranking
2329
+ // Fetch more results for ranking
1757
2330
  threshold: 0.3
1758
2331
  });
1759
2332
  for (const hit of vectorResults) {
@@ -1770,10 +2343,10 @@ var HybridSearch = class {
1770
2343
  source: "vector"
1771
2344
  });
1772
2345
  }
1773
- logger.debug("SEARCH", `Vector search: ${vectorResults.length} risultati`);
2346
+ logger.debug("SEARCH", `Vector search: ${vectorResults.length} results`);
1774
2347
  }
1775
2348
  } catch (error) {
1776
- logger.warn("SEARCH", "Ricerca vettoriale fallita, uso solo keyword", {}, error);
2349
+ logger.warn("SEARCH", "Vector search failed, using only keyword", {}, error);
1777
2350
  }
1778
2351
  }
1779
2352
  try {
@@ -1803,9 +2376,9 @@ var HybridSearch = class {
1803
2376
  });
1804
2377
  }
1805
2378
  }
1806
- logger.debug("SEARCH", `Keyword search: ${keywordResults.length} risultati`);
2379
+ logger.debug("SEARCH", `Keyword search: ${keywordResults.length} results`);
1807
2380
  } catch (error) {
1808
- logger.error("SEARCH", "Ricerca keyword fallita", {}, error);
2381
+ logger.error("SEARCH", "Keyword search failed", {}, error);
1809
2382
  }
1810
2383
  if (rawItems.size === 0) return [];
1811
2384
  const allFTS5Ranks = Array.from(rawItems.values()).filter((item) => item.fts5Rank !== null).map((item) => item.fts5Rank);
@@ -1857,9 +2430,6 @@ function getHybridSearch() {
1857
2430
  return hybridSearch;
1858
2431
  }
1859
2432
 
1860
- // src/types/worker-types.ts
1861
- var KNOWLEDGE_TYPES = ["constraint", "decision", "heuristic", "rejected"];
1862
-
1863
2433
  // src/sdk/index.ts
1864
2434
  var KiroMemorySDK = class {
1865
2435
  db;
@@ -1893,33 +2463,33 @@ var KiroMemorySDK = class {
1893
2463
  };
1894
2464
  }
1895
2465
  /**
1896
- * Valida input per storeObservation
2466
+ * Validate input for storeObservation
1897
2467
  */
1898
2468
  validateObservationInput(data) {
1899
2469
  if (!data.type || typeof data.type !== "string" || data.type.length > 100) {
1900
- throw new Error("type \xE8 obbligatorio (stringa, max 100 caratteri)");
2470
+ throw new Error("type is required (string, max 100 chars)");
1901
2471
  }
1902
2472
  if (!data.title || typeof data.title !== "string" || data.title.length > 500) {
1903
- throw new Error("title \xE8 obbligatorio (stringa, max 500 caratteri)");
2473
+ throw new Error("title is required (string, max 500 chars)");
1904
2474
  }
1905
2475
  if (!data.content || typeof data.content !== "string" || data.content.length > 1e5) {
1906
- throw new Error("content \xE8 obbligatorio (stringa, max 100KB)");
2476
+ throw new Error("content is required (string, max 100KB)");
1907
2477
  }
1908
2478
  }
1909
2479
  /**
1910
- * Valida input per storeSummary
2480
+ * Validate input for storeSummary
1911
2481
  */
1912
2482
  validateSummaryInput(data) {
1913
2483
  const MAX = 5e4;
1914
2484
  for (const [key, val] of Object.entries(data)) {
1915
2485
  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)`);
2486
+ if (typeof val !== "string") throw new Error(`${key} must be a string`);
2487
+ if (val.length > MAX) throw new Error(`${key} too large (max 50KB)`);
1918
2488
  }
1919
2489
  }
1920
2490
  }
1921
2491
  /**
1922
- * Genera e salva embedding per un'osservazione (fire-and-forget, non blocca)
2492
+ * Generate and store embedding for an observation (fire-and-forget, non-blocking)
1923
2493
  */
1924
2494
  async generateEmbeddingAsync(observationId, title, content, concepts) {
1925
2495
  try {
@@ -1939,39 +2509,39 @@ var KiroMemorySDK = class {
1939
2509
  );
1940
2510
  }
1941
2511
  } catch (error) {
1942
- logger.debug("SDK", `Embedding generation fallita per obs ${observationId}: ${error}`);
2512
+ logger.debug("SDK", `Embedding generation failed for obs ${observationId}: ${error}`);
1943
2513
  }
1944
2514
  }
1945
2515
  /**
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.
2516
+ * Generate SHA256 content hash for content-based deduplication.
2517
+ * Uses (project + type + title + narrative) as semantic identity tuple.
2518
+ * Does NOT include sessionId since it's unique per invocation.
1949
2519
  */
1950
2520
  generateContentHash(type, title, narrative) {
1951
2521
  const payload = `${this.project}|${type}|${title}|${narrative || ""}`;
1952
2522
  return createHash("sha256").update(payload).digest("hex");
1953
2523
  }
1954
2524
  /**
1955
- * Finestre di deduplicazione per tipo (ms).
1956
- * Tipi con molte ripetizioni hanno finestre più ampie.
2525
+ * Deduplication windows per type (ms).
2526
+ * Types with many repetitions have wider windows.
1957
2527
  */
1958
2528
  getDeduplicationWindow(type) {
1959
2529
  switch (type) {
1960
2530
  case "file-read":
1961
2531
  return 6e4;
1962
- // 60s — letture frequenti sugli stessi file
2532
+ // 60s — frequent reads on the same files
1963
2533
  case "file-write":
1964
2534
  return 1e4;
1965
- // 10s — scritture rapide consecutive
2535
+ // 10s — rapid consecutive writes
1966
2536
  case "command":
1967
2537
  return 3e4;
1968
2538
  // 30s — standard
1969
2539
  case "research":
1970
2540
  return 12e4;
1971
- // 120s — web search e fetch ripetuti
2541
+ // 120s — repeated web search and fetch
1972
2542
  case "delegation":
1973
2543
  return 6e4;
1974
- // 60s — delegazioni rapide
2544
+ // 60s — rapid delegations
1975
2545
  default:
1976
2546
  return 3e4;
1977
2547
  }
@@ -1985,7 +2555,7 @@ var KiroMemorySDK = class {
1985
2555
  const contentHash = this.generateContentHash(data.type, data.title, data.narrative);
1986
2556
  const dedupWindow = this.getDeduplicationWindow(data.type);
1987
2557
  if (isDuplicateObservation(this.db.db, contentHash, dedupWindow)) {
1988
- logger.debug("SDK", `Osservazione duplicata scartata (${data.type}, ${dedupWindow}ms): ${data.title}`);
2558
+ logger.debug("SDK", `Duplicate observation discarded (${data.type}, ${dedupWindow}ms): ${data.title}`);
1989
2559
  return -1;
1990
2560
  }
1991
2561
  const filesRead = data.filesRead || (data.type === "file-read" ? data.files : void 0);
@@ -2013,12 +2583,12 @@ var KiroMemorySDK = class {
2013
2583
  return observationId;
2014
2584
  }
2015
2585
  /**
2016
- * Salva conoscenza strutturata (constraint, decision, heuristic, rejected).
2017
- * Usa il campo `type` per il knowledgeType e `facts` per i metadati JSON.
2586
+ * Store structured knowledge (constraint, decision, heuristic, rejected).
2587
+ * Uses the `type` field for knowledgeType and `facts` for JSON metadata.
2018
2588
  */
2019
2589
  async storeKnowledge(data) {
2020
2590
  if (!KNOWLEDGE_TYPES.includes(data.knowledgeType)) {
2021
- throw new Error(`knowledgeType non valido: ${data.knowledgeType}. Valori ammessi: ${KNOWLEDGE_TYPES.join(", ")}`);
2591
+ throw new Error(`Invalid knowledgeType: ${data.knowledgeType}. Allowed values: ${KNOWLEDGE_TYPES.join(", ")}`);
2022
2592
  }
2023
2593
  this.validateObservationInput({ type: data.knowledgeType, title: data.title, content: data.content });
2024
2594
  const metadata = (() => {
@@ -2050,9 +2620,9 @@ var KiroMemorySDK = class {
2050
2620
  }
2051
2621
  })();
2052
2622
  const sessionId = "sdk-" + Date.now();
2053
- const contentHash = this.generateContentHash(data.type, data.title);
2623
+ const contentHash = this.generateContentHash(data.knowledgeType, data.title);
2054
2624
  if (isDuplicateObservation(this.db.db, contentHash)) {
2055
- logger.debug("SDK", `Knowledge duplicata scartata: ${data.title}`);
2625
+ logger.debug("SDK", `Duplicate knowledge discarded: ${data.title}`);
2056
2626
  return -1;
2057
2627
  }
2058
2628
  const discoveryTokens = Math.ceil(data.content.length / 4);
@@ -2069,11 +2639,11 @@ var KiroMemorySDK = class {
2069
2639
  null,
2070
2640
  // narrative
2071
2641
  JSON.stringify(metadata),
2072
- // facts = metadati JSON
2642
+ // facts = JSON metadata
2073
2643
  data.concepts?.join(", ") || null,
2074
2644
  data.files?.join(", ") || null,
2075
2645
  null,
2076
- // filesModified: knowledge non modifica file
2646
+ // filesModified: knowledge doesn't modify files
2077
2647
  0,
2078
2648
  // prompt_number
2079
2649
  contentHash,
@@ -2184,8 +2754,8 @@ var KiroMemorySDK = class {
2184
2754
  return this.project;
2185
2755
  }
2186
2756
  /**
2187
- * Ricerca ibrida: vector search + keyword FTS5
2188
- * Richiede inizializzazione HybridSearch (embedding service)
2757
+ * Hybrid search: vector search + keyword FTS5
2758
+ * Requires HybridSearch initialization (embedding service)
2189
2759
  */
2190
2760
  async hybridSearch(query, options = {}) {
2191
2761
  const hybridSearch2 = getHybridSearch();
@@ -2195,8 +2765,8 @@ var KiroMemorySDK = class {
2195
2765
  });
2196
2766
  }
2197
2767
  /**
2198
- * Ricerca solo semantica (vector search)
2199
- * Ritorna risultati basati su similarità coseno con gli embeddings
2768
+ * Semantic-only search (vector search)
2769
+ * Returns results based on cosine similarity with embeddings
2200
2770
  */
2201
2771
  async semanticSearch(query, options = {}) {
2202
2772
  const embeddingService2 = getEmbeddingService();
@@ -2231,21 +2801,21 @@ var KiroMemorySDK = class {
2231
2801
  }));
2232
2802
  }
2233
2803
  /**
2234
- * Genera embeddings per osservazioni che non li hanno ancora
2804
+ * Generate embeddings for observations that don't have them yet
2235
2805
  */
2236
2806
  async backfillEmbeddings(batchSize = 50) {
2237
2807
  const vectorSearch2 = getVectorSearch();
2238
2808
  return vectorSearch2.backfillEmbeddings(this.db.db, batchSize);
2239
2809
  }
2240
2810
  /**
2241
- * Statistiche sugli embeddings nel database
2811
+ * Embedding statistics in the database
2242
2812
  */
2243
2813
  getEmbeddingStats() {
2244
2814
  const vectorSearch2 = getVectorSearch();
2245
2815
  return vectorSearch2.getStats(this.db.db);
2246
2816
  }
2247
2817
  /**
2248
- * Inizializza il servizio di embedding (lazy, chiamare prima di hybridSearch)
2818
+ * Initialize the embedding service (lazy, call before hybridSearch)
2249
2819
  */
2250
2820
  async initializeEmbeddings() {
2251
2821
  const hybridSearch2 = getHybridSearch();
@@ -2253,10 +2823,10 @@ var KiroMemorySDK = class {
2253
2823
  return getEmbeddingService().isAvailable();
2254
2824
  }
2255
2825
  /**
2256
- * Contesto smart con ranking a 4 segnali e budget token.
2826
+ * Smart context with 4-signal ranking and token budget.
2257
2827
  *
2258
- * Se query presente: usa HybridSearch con SEARCH_WEIGHTS.
2259
- * Se senza query: ranking per recency + project match (CONTEXT_WEIGHTS).
2828
+ * If query present: uses HybridSearch with SEARCH_WEIGHTS.
2829
+ * If no query: ranking by recency + project match (CONTEXT_WEIGHTS).
2260
2830
  */
2261
2831
  async getSmartContext(options = {}) {
2262
2832
  const tokenBudget = options.tokenBudget || parseInt(process.env.KIRO_MEMORY_CONTEXT_TOKENS || "0", 10) || 2e3;
@@ -2330,8 +2900,8 @@ var KiroMemorySDK = class {
2330
2900
  };
2331
2901
  }
2332
2902
  /**
2333
- * Rileva osservazioni stale (file modificati dopo la creazione) e le marca nel DB.
2334
- * Ritorna il numero di osservazioni marcate come stale.
2903
+ * Detect stale observations (files modified after creation) and mark them in DB.
2904
+ * Returns the number of observations marked as stale.
2335
2905
  */
2336
2906
  async detectStaleObservations() {
2337
2907
  const staleObs = getStaleObservations(this.db.db, this.project);
@@ -2342,14 +2912,14 @@ var KiroMemorySDK = class {
2342
2912
  return staleObs.length;
2343
2913
  }
2344
2914
  /**
2345
- * Consolida osservazioni duplicate sullo stesso file e tipo.
2346
- * Raggruppa per (project, type, files_modified), mantiene la piu recente.
2915
+ * Consolidate duplicate observations on the same file and type.
2916
+ * Groups by (project, type, files_modified), keeps the most recent.
2347
2917
  */
2348
2918
  async consolidateObservations(options = {}) {
2349
2919
  return consolidateObservations(this.db.db, this.project, options);
2350
2920
  }
2351
2921
  /**
2352
- * Statistiche decay: totale, stale, mai accedute, accedute di recente.
2922
+ * Decay statistics: total, stale, never accessed, recently accessed.
2353
2923
  */
2354
2924
  async getDecayStats() {
2355
2925
  const total = this.db.db.query(
@@ -2368,8 +2938,8 @@ var KiroMemorySDK = class {
2368
2938
  return { total, stale, neverAccessed, recentlyAccessed };
2369
2939
  }
2370
2940
  /**
2371
- * Crea un checkpoint strutturato per resume sessione.
2372
- * Salva automaticamente un context_snapshot con le ultime 10 osservazioni.
2941
+ * Create a structured checkpoint for session resume.
2942
+ * Automatically saves a context_snapshot with the last 10 observations.
2373
2943
  */
2374
2944
  async createCheckpoint(sessionId, data) {
2375
2945
  const recentObs = getObservationsByProject(this.db.db, this.project, 10);
@@ -2386,21 +2956,21 @@ var KiroMemorySDK = class {
2386
2956
  });
2387
2957
  }
2388
2958
  /**
2389
- * Recupera l'ultimo checkpoint di una sessione specifica.
2959
+ * Retrieve the latest checkpoint of a specific session.
2390
2960
  */
2391
2961
  async getCheckpoint(sessionId) {
2392
2962
  return getLatestCheckpoint(this.db.db, sessionId);
2393
2963
  }
2394
2964
  /**
2395
- * Recupera l'ultimo checkpoint per il progetto corrente.
2396
- * Utile per resume automatico senza specificare session ID.
2965
+ * Retrieve the latest checkpoint for the current project.
2966
+ * Useful for automatic resume without specifying session ID.
2397
2967
  */
2398
2968
  async getLatestProjectCheckpoint() {
2399
2969
  return getLatestCheckpointByProject(this.db.db, this.project);
2400
2970
  }
2401
2971
  /**
2402
- * Genera un report di attività per il progetto corrente.
2403
- * Aggrega osservazioni, sessioni, summaries e file per un periodo temporale.
2972
+ * Generate an activity report for the current project.
2973
+ * Aggregates observations, sessions, summaries and files for a time period.
2404
2974
  */
2405
2975
  async generateReport(options) {
2406
2976
  const now = /* @__PURE__ */ new Date();
@@ -2416,6 +2986,66 @@ var KiroMemorySDK = class {
2416
2986
  }
2417
2987
  return getReportData(this.db.db, this.project, startEpoch, endEpoch);
2418
2988
  }
2989
+ /**
2990
+ * Lista osservazioni con keyset pagination.
2991
+ * Restituisce un oggetto { data, next_cursor, has_more }.
2992
+ *
2993
+ * Esempio:
2994
+ * const page1 = await sdk.listObservations({ limit: 50 });
2995
+ * const page2 = await sdk.listObservations({ cursor: page1.next_cursor });
2996
+ */
2997
+ async listObservations(options = {}) {
2998
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);
2999
+ const project = options.project ?? this.project;
3000
+ let rows;
3001
+ if (options.cursor) {
3002
+ const decoded = decodeCursor(options.cursor);
3003
+ if (!decoded) throw new Error("Cursor non valido");
3004
+ const sql = project ? `SELECT * FROM observations
3005
+ WHERE project = ? AND (created_at_epoch < ? OR (created_at_epoch = ? AND id < ?))
3006
+ ORDER BY created_at_epoch DESC, id DESC
3007
+ LIMIT ?` : `SELECT * FROM observations
3008
+ WHERE (created_at_epoch < ? OR (created_at_epoch = ? AND id < ?))
3009
+ ORDER BY created_at_epoch DESC, id DESC
3010
+ LIMIT ?`;
3011
+ rows = project ? this.db.db.query(sql).all(project, decoded.epoch, decoded.epoch, decoded.id, limit) : this.db.db.query(sql).all(decoded.epoch, decoded.epoch, decoded.id, limit);
3012
+ } else {
3013
+ const sql = project ? "SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?" : "SELECT * FROM observations ORDER BY created_at_epoch DESC, id DESC LIMIT ?";
3014
+ rows = project ? this.db.db.query(sql).all(project, limit) : this.db.db.query(sql).all(limit);
3015
+ }
3016
+ const next_cursor = rows.length >= limit ? encodeCursor(rows[rows.length - 1].id, rows[rows.length - 1].created_at_epoch) : null;
3017
+ return { data: rows, next_cursor, has_more: next_cursor !== null };
3018
+ }
3019
+ /**
3020
+ * Lista sommari con keyset pagination.
3021
+ * Restituisce un oggetto { data, next_cursor, has_more }.
3022
+ *
3023
+ * Esempio:
3024
+ * const page1 = await sdk.listSummaries({ limit: 20 });
3025
+ * const page2 = await sdk.listSummaries({ cursor: page1.next_cursor });
3026
+ */
3027
+ async listSummaries(options = {}) {
3028
+ const limit = Math.min(Math.max(options.limit ?? 20, 1), 200);
3029
+ const project = options.project ?? this.project;
3030
+ let rows;
3031
+ if (options.cursor) {
3032
+ const decoded = decodeCursor(options.cursor);
3033
+ if (!decoded) throw new Error("Cursor non valido");
3034
+ const sql = project ? `SELECT * FROM summaries
3035
+ WHERE project = ? AND (created_at_epoch < ? OR (created_at_epoch = ? AND id < ?))
3036
+ ORDER BY created_at_epoch DESC, id DESC
3037
+ LIMIT ?` : `SELECT * FROM summaries
3038
+ WHERE (created_at_epoch < ? OR (created_at_epoch = ? AND id < ?))
3039
+ ORDER BY created_at_epoch DESC, id DESC
3040
+ LIMIT ?`;
3041
+ rows = project ? this.db.db.query(sql).all(project, decoded.epoch, decoded.epoch, decoded.id, limit) : this.db.db.query(sql).all(decoded.epoch, decoded.epoch, decoded.id, limit);
3042
+ } else {
3043
+ const sql = project ? "SELECT * FROM summaries WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?" : "SELECT * FROM summaries ORDER BY created_at_epoch DESC, id DESC LIMIT ?";
3044
+ rows = project ? this.db.db.query(sql).all(project, limit) : this.db.db.query(sql).all(limit);
3045
+ }
3046
+ const next_cursor = rows.length >= limit ? encodeCursor(rows[rows.length - 1].id, rows[rows.length - 1].created_at_epoch) : null;
3047
+ return { data: rows, next_cursor, has_more: next_cursor !== null };
3048
+ }
2419
3049
  /**
2420
3050
  * Getter for direct database access (for API routes)
2421
3051
  */
@@ -2485,7 +3115,7 @@ runHook("agentSpawn", async (input) => {
2485
3115
  summaries: smartCtx.summaries,
2486
3116
  project
2487
3117
  });
2488
- output += `> UI disponibile su http://127.0.0.1:${process.env.KIRO_MEMORY_WORKER_PORT || "3001"}
3118
+ output += `> UI available at http://127.0.0.1:${process.env.KIRO_MEMORY_WORKER_PORT || "3001"}
2489
3119
  `;
2490
3120
  process.stdout.write(output);
2491
3121
  } finally {