kiro-memory 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,12 +11,16 @@
11
11
  <a href="https://www.npmjs.com/package/kiro-memory"><img src="https://img.shields.io/npm/v/kiro-memory" alt="npm" /></a>
12
12
  <img src="https://img.shields.io/badge/license-AGPL--3.0-blue" alt="License" />
13
13
  <img src="https://img.shields.io/badge/node-%3E%3D18-green" alt="Node" />
14
+ <a href="https://auritidesign.it/docs/kiro-memory/"><img src="https://img.shields.io/badge/docs-auritidesign.it-00b4d8" alt="Docs" /></a>
14
15
  </p>
15
16
 
16
17
  ---
17
18
 
18
19
  Kiro Memory gives your AI coding assistant memory that persists across sessions. It automatically captures what happened -- files changed, tools used, decisions made -- and feeds relevant context back at the start of the next session. No manual bookkeeping. Your agent picks up exactly where it left off.
19
20
 
21
+
22
+ **[Read the full documentation →](https://auritidesign.it/docs/kiro-memory/)**
23
+
20
24
  Works with **Claude Code** (hooks), **Cursor** (rules + MCP), **Windsurf** (rules + MCP), **Cline** (custom instructions + MCP), and any editor that supports the **Model Context Protocol**.
21
25
 
22
26
  ## What Your Agent Sees
@@ -52,10 +56,13 @@ When a new session starts, Kiro Memory automatically injects previous session co
52
56
  - **Session Checkpoint & Resume** -- Checkpoint sessions and resume from where you left off
53
57
  - **Activity Reports** -- Weekly/monthly digests in text, Markdown, or JSON format
54
58
  - **Analytics Dashboard** -- Activity timeline, type distribution, session stats, and file hotspots
55
- - **Session Summaries** -- Structured summaries generated when sessions end
56
- - **Web Dashboard** -- Real-time viewer at `http://localhost:3001` with dark/light theme, search, project filters, and live updates via SSE
57
- - **MCP Server** -- 10 tools exposed via Model Context Protocol
58
- - **Full-Text Search** -- SQLite FTS5 for fast, typo-tolerant search across all stored context
59
+ - **Session Tracking** -- Sessions view with stats (total, active, completed, avg duration) and expandable session details
60
+ - **Session Summaries** -- Structured summaries with investigated/completed/learned/next_steps sections
61
+ - **Web Dashboard** -- Real-time viewer at `http://localhost:3001` with dark/light/system theme, hybrid search, project filters, mobile drawer, and live updates via SSE
62
+ - **MCP Server** -- 11 tools exposed via Model Context Protocol
63
+ - **Full-Text Search** -- SQLite FTS5 with weighted BM25 scoring for relevance-ranked results
64
+ - **Data Export** -- Export observations and summaries in JSON or Markdown format
65
+ - **Retention Policy** -- Automatic cleanup of old data with configurable age and dry-run mode
59
66
  - **TypeScript SDK** -- Programmatic access to the entire memory system
60
67
  - **CLI** -- Query and manage context directly from the terminal
61
68
 
@@ -147,6 +154,7 @@ The MCP server exposes 10 tools that your AI assistant can use directly.
147
154
  | `resume_session` | Get checkpoint data to resume a previous session |
148
155
  | `generate_report` | Generate weekly/monthly activity report in Markdown |
149
156
  | `get_recent_context` | Get recent memory context for session injection |
157
+ | `save_memory` | Save a structured observation from external tools or scripts |
150
158
 
151
159
  ### Storage
152
160
 
@@ -282,9 +290,12 @@ kiro-memory decay --days=30
282
290
  The worker starts automatically when a Kiro session begins (via the `agentSpawn` hook). Once running, open `http://localhost:3001` in your browser to access the web dashboard with:
283
291
 
284
292
  - **Live feed** of observations, summaries, and prompts (via SSE)
285
- - **Project sidebar** with type filters and stats
286
- - **Spotlight search** (Ctrl+K / Cmd+K) with instant results
287
- - **Dark/light theme** toggle
293
+ - **Sessions view** with stats cards and expandable session details
294
+ - **Analytics dashboard** with timeline charts and type distribution
295
+ - **Project sidebar** with type filters, stats, and token economics
296
+ - **Spotlight search** (Ctrl+K / Cmd+K) with hybrid search and source badges
297
+ - **Dark/light/system theme** cycling
298
+ - **Mobile-responsive** sidebar drawer
288
299
 
289
300
  For development, you can also manage the worker manually:
290
301
 
@@ -443,3 +454,11 @@ Contributions are welcome. Please open an issue to discuss proposed changes befo
443
454
  ---
444
455
 
445
456
  Built by [auriti-web-design](https://github.com/auriti-web-design)
457
+
458
+ ---
459
+
460
+ <p align="center">
461
+ <a href="https://buymeacoffee.com/auritidesign">
462
+ <img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me a Coffee" />
463
+ </a>
464
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-memory",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Persistent cross-session memory for AI coding assistants. Works with Claude Code, Cursor, Windsurf, Cline, and any MCP-compatible editor.",
5
5
  "keywords": [
6
6
  "kiro",
@@ -94,7 +94,6 @@
94
94
  "better-sqlite3": "^12.6.2",
95
95
  "chromadb": "^3.2.2",
96
96
  "class-variance-authority": "^0.7.1",
97
- "clsx": "^2.1.1",
98
97
  "cors": "^2.8.5",
99
98
  "dompurify": "^3.3.1",
100
99
  "express": "^4.18.2",
@@ -105,13 +104,12 @@
105
104
  "lucide-react": "^0.574.0",
106
105
  "react": "^18.3.1",
107
106
  "react-dom": "^18.3.1",
108
- "tailwind-merge": "^3.4.1",
109
107
  "yaml": "^2.8.2",
110
108
  "zod": "^3.23.8"
111
109
  },
112
110
  "optionalDependencies": {
113
- "fastembed": "^2.1.0",
114
- "@huggingface/transformers": "^3.8.0"
111
+ "@huggingface/transformers": "^3.8.0",
112
+ "fastembed": "^2.1.0"
115
113
  },
116
114
  "devDependencies": {
117
115
  "@types/better-sqlite3": "^7.6.13",
@@ -249,19 +249,28 @@ __export(Observations_exports, {
249
249
  deleteObservation: () => deleteObservation,
250
250
  getObservationsByProject: () => getObservationsByProject,
251
251
  getObservationsBySession: () => getObservationsBySession,
252
+ isDuplicateObservation: () => isDuplicateObservation,
252
253
  searchObservations: () => searchObservations,
253
254
  updateLastAccessed: () => updateLastAccessed
254
255
  });
255
256
  function escapeLikePattern(input) {
256
257
  return input.replace(/[%_\\]/g, "\\$&");
257
258
  }
258
- function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
259
+ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
260
+ if (!contentHash) return false;
261
+ const threshold = Date.now() - windowMs;
262
+ const result = db.query(
263
+ "SELECT id FROM observations WHERE content_hash = ? AND created_at_epoch > ? LIMIT 1"
264
+ ).get(contentHash, threshold);
265
+ return !!result;
266
+ }
267
+ function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
259
268
  const now = /* @__PURE__ */ new Date();
260
269
  const result = db.run(
261
- `INSERT INTO observations
262
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
263
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
264
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
270
+ `INSERT INTO observations
271
+ (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)
272
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
273
+ [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
265
274
  );
266
275
  return Number(result.lastInsertRowid);
267
276
  }
@@ -317,39 +326,42 @@ function consolidateObservations(db, project, options = {}) {
317
326
  if (groups.length === 0) return { merged: 0, removed: 0 };
318
327
  let totalMerged = 0;
319
328
  let totalRemoved = 0;
320
- for (const group of groups) {
321
- const obsIds = group.ids.split(",").map(Number);
322
- const placeholders = obsIds.map(() => "?").join(",");
323
- const observations = db.query(
324
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
325
- ).all(...obsIds);
326
- if (observations.length < minGroupSize) continue;
327
- if (options.dryRun) {
328
- totalMerged += 1;
329
- totalRemoved += observations.length - 1;
330
- continue;
331
- }
332
- const keeper = observations[0];
333
- const others = observations.slice(1);
334
- const uniqueTexts = /* @__PURE__ */ new Set();
335
- if (keeper.text) uniqueTexts.add(keeper.text);
336
- for (const obs of others) {
337
- if (obs.text && !uniqueTexts.has(obs.text)) {
338
- uniqueTexts.add(obs.text);
329
+ const runConsolidation = db.transaction(() => {
330
+ for (const group of groups) {
331
+ const obsIds = group.ids.split(",").map(Number);
332
+ const placeholders = obsIds.map(() => "?").join(",");
333
+ const observations = db.query(
334
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
335
+ ).all(...obsIds);
336
+ if (observations.length < minGroupSize) continue;
337
+ if (options.dryRun) {
338
+ totalMerged += 1;
339
+ totalRemoved += observations.length - 1;
340
+ continue;
341
+ }
342
+ const keeper = observations[0];
343
+ const others = observations.slice(1);
344
+ const uniqueTexts = /* @__PURE__ */ new Set();
345
+ if (keeper.text) uniqueTexts.add(keeper.text);
346
+ for (const obs of others) {
347
+ if (obs.text && !uniqueTexts.has(obs.text)) {
348
+ uniqueTexts.add(obs.text);
349
+ }
339
350
  }
351
+ const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
352
+ db.run(
353
+ "UPDATE observations SET text = ?, title = ? WHERE id = ?",
354
+ [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
355
+ );
356
+ const removeIds = others.map((o) => o.id);
357
+ const removePlaceholders = removeIds.map(() => "?").join(",");
358
+ db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
359
+ db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
360
+ totalMerged += 1;
361
+ totalRemoved += removeIds.length;
340
362
  }
341
- const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
342
- db.run(
343
- "UPDATE observations SET text = ?, title = ? WHERE id = ?",
344
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
345
- );
346
- const removeIds = others.map((o) => o.id);
347
- const removePlaceholders = removeIds.map(() => "?").join(",");
348
- db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
349
- db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
350
- totalMerged += 1;
351
- totalRemoved += removeIds.length;
352
- }
363
+ });
364
+ runConsolidation();
353
365
  return { merged: totalMerged, removed: totalRemoved };
354
366
  }
355
367
  var init_Observations = __esm({
@@ -407,7 +419,7 @@ function searchObservationsFTS(db, query, filters = {}) {
407
419
  sql += " AND o.created_at_epoch <= ?";
408
420
  params.push(filters.dateEnd);
409
421
  }
410
- sql += " ORDER BY rank LIMIT ?";
422
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
411
423
  params.push(limit);
412
424
  const stmt = db.query(sql);
413
425
  return stmt.all(...params);
@@ -421,7 +433,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
421
433
  const safeQuery = sanitizeFTS5Query(query);
422
434
  if (!safeQuery) return [];
423
435
  let sql = `
424
- SELECT o.*, rank as fts5_rank FROM observations o
436
+ SELECT o.*, bm25(observations_fts, ${BM25_WEIGHTS}) as fts5_rank FROM observations o
425
437
  JOIN observations_fts fts ON o.id = fts.rowid
426
438
  WHERE observations_fts MATCH ?
427
439
  `;
@@ -442,7 +454,7 @@ function searchObservationsFTSWithRank(db, query, filters = {}) {
442
454
  sql += " AND o.created_at_epoch <= ?";
443
455
  params.push(filters.dateEnd);
444
456
  }
445
- sql += " ORDER BY rank LIMIT ?";
457
+ sql += ` ORDER BY bm25(observations_fts, ${BM25_WEIGHTS}) LIMIT ?`;
446
458
  params.push(limit);
447
459
  const stmt = db.query(sql);
448
460
  return stmt.all(...params);
@@ -546,11 +558,23 @@ function getProjectStats(db, project) {
546
558
  const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
547
559
  const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
548
560
  const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
561
+ const discoveryStmt = db.query(
562
+ "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
563
+ );
564
+ const discoveryTokens = discoveryStmt.get(project)?.total || 0;
565
+ const readStmt = db.query(
566
+ `SELECT COALESCE(SUM(
567
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
568
+ ), 0) as total FROM observations WHERE project = ?`
569
+ );
570
+ const readTokens = readStmt.get(project)?.total || 0;
571
+ const savings = Math.max(0, discoveryTokens - readTokens);
549
572
  return {
550
573
  observations: obsStmt.get(project)?.count || 0,
551
574
  summaries: sumStmt.get(project)?.count || 0,
552
575
  sessions: sesStmt.get(project)?.count || 0,
553
- prompts: prmStmt.get(project)?.count || 0
576
+ prompts: prmStmt.get(project)?.count || 0,
577
+ tokenEconomics: { discoveryTokens, readTokens, savings }
554
578
  };
555
579
  }
556
580
  function getStaleObservations(db, project) {
@@ -592,9 +616,11 @@ function markObservationsStale(db, ids, stale) {
592
616
  [stale ? 1 : 0, ...validIds]
593
617
  );
594
618
  }
619
+ var BM25_WEIGHTS;
595
620
  var init_Search = __esm({
596
621
  "src/services/sqlite/Search.ts"() {
597
622
  "use strict";
623
+ BM25_WEIGHTS = "10.0, 1.0, 5.0, 3.0";
598
624
  }
599
625
  });
600
626
 
@@ -1106,6 +1132,29 @@ var MigrationRunner = class {
1106
1132
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
1107
1133
  db.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
1108
1134
  }
1135
+ },
1136
+ {
1137
+ version: 7,
1138
+ up: (db) => {
1139
+ db.run("ALTER TABLE observations ADD COLUMN content_hash TEXT");
1140
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_hash ON observations(content_hash)");
1141
+ }
1142
+ },
1143
+ {
1144
+ version: 8,
1145
+ up: (db) => {
1146
+ db.run("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
1147
+ db.run("ALTER TABLE summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0");
1148
+ }
1149
+ },
1150
+ {
1151
+ version: 9,
1152
+ up: (db) => {
1153
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_epoch ON observations(project, created_at_epoch DESC)");
1154
+ db.run("CREATE INDEX IF NOT EXISTS idx_observations_project_type ON observations(project, type)");
1155
+ db.run("CREATE INDEX IF NOT EXISTS idx_summaries_project_epoch ON summaries(project, created_at_epoch DESC)");
1156
+ db.run("CREATE INDEX IF NOT EXISTS idx_prompts_project_epoch ON prompts(project, created_at_epoch DESC)");
1157
+ }
1109
1158
  }
1110
1159
  ];
1111
1160
  }
@@ -1350,6 +1399,7 @@ init_Search();
1350
1399
 
1351
1400
  // src/sdk/index.ts
1352
1401
  init_Observations();
1402
+ import { createHash } from "crypto";
1353
1403
  init_Search();
1354
1404
 
1355
1405
  // src/services/search/HybridSearch.ts
@@ -1779,31 +1829,71 @@ var KiroMemorySDK = class {
1779
1829
  logger.debug("SDK", `Embedding generation fallita per obs ${observationId}: ${error}`);
1780
1830
  }
1781
1831
  }
1832
+ /**
1833
+ * Genera content hash SHA256 per deduplicazione basata su contenuto.
1834
+ * Usa (project + type + title + narrative) come tupla di identità semantica.
1835
+ * NON include sessionId perché è unico ad ogni invocazione.
1836
+ */
1837
+ generateContentHash(type, title, narrative) {
1838
+ const payload = `${this.project}|${type}|${title}|${narrative || ""}`;
1839
+ return createHash("sha256").update(payload).digest("hex");
1840
+ }
1841
+ /**
1842
+ * Finestre di deduplicazione per tipo (ms).
1843
+ * Tipi con molte ripetizioni hanno finestre più ampie.
1844
+ */
1845
+ getDeduplicationWindow(type) {
1846
+ switch (type) {
1847
+ case "file-read":
1848
+ return 6e4;
1849
+ // 60s — letture frequenti sugli stessi file
1850
+ case "file-write":
1851
+ return 1e4;
1852
+ // 10s — scritture rapide consecutive
1853
+ case "command":
1854
+ return 3e4;
1855
+ // 30s — standard
1856
+ case "research":
1857
+ return 12e4;
1858
+ // 120s — web search e fetch ripetuti
1859
+ case "delegation":
1860
+ return 6e4;
1861
+ // 60s — delegazioni rapide
1862
+ default:
1863
+ return 3e4;
1864
+ }
1865
+ }
1782
1866
  /**
1783
1867
  * Store a new observation
1784
1868
  */
1785
1869
  async storeObservation(data) {
1786
1870
  this.validateObservationInput(data);
1871
+ const sessionId = "sdk-" + Date.now();
1872
+ const contentHash = this.generateContentHash(data.type, data.title, data.narrative);
1873
+ const dedupWindow = this.getDeduplicationWindow(data.type);
1874
+ if (isDuplicateObservation(this.db.db, contentHash, dedupWindow)) {
1875
+ logger.debug("SDK", `Osservazione duplicata scartata (${data.type}, ${dedupWindow}ms): ${data.title}`);
1876
+ return -1;
1877
+ }
1878
+ const filesRead = data.filesRead || (data.type === "file-read" ? data.files : void 0);
1879
+ const filesModified = data.filesModified || (data.type === "file-write" ? data.files : void 0);
1880
+ const discoveryTokens = Math.ceil(data.content.length / 4);
1787
1881
  const observationId = createObservation(
1788
1882
  this.db.db,
1789
- "sdk-" + Date.now(),
1883
+ sessionId,
1790
1884
  this.project,
1791
1885
  data.type,
1792
1886
  data.title,
1793
- null,
1794
- // subtitle
1887
+ data.subtitle || null,
1795
1888
  data.content,
1796
- null,
1797
- // narrative
1798
- null,
1799
- // facts
1889
+ data.narrative || null,
1890
+ data.facts || null,
1800
1891
  data.concepts?.join(", ") || null,
1801
- data.files?.join(", ") || null,
1802
- // files_read
1803
- data.files?.join(", ") || null,
1804
- // files_modified
1805
- 0
1806
- // prompt_number
1892
+ filesRead?.join(", ") || null,
1893
+ filesModified?.join(", ") || null,
1894
+ 0,
1895
+ contentHash,
1896
+ discoveryTokens
1807
1897
  );
1808
1898
  this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1809
1899
  });
@@ -1846,9 +1936,16 @@ var KiroMemorySDK = class {
1846
1936
  };
1847
1937
  }
1848
1938
  })();
1939
+ const sessionId = "sdk-" + Date.now();
1940
+ const contentHash = this.generateContentHash(data.type, data.title);
1941
+ if (isDuplicateObservation(this.db.db, contentHash)) {
1942
+ logger.debug("SDK", `Knowledge duplicata scartata: ${data.title}`);
1943
+ return -1;
1944
+ }
1945
+ const discoveryTokens = Math.ceil(data.content.length / 4);
1849
1946
  const observationId = createObservation(
1850
1947
  this.db.db,
1851
- "sdk-" + Date.now(),
1948
+ sessionId,
1852
1949
  data.project || this.project,
1853
1950
  data.knowledgeType,
1854
1951
  // type = knowledgeType
@@ -1862,9 +1959,12 @@ var KiroMemorySDK = class {
1862
1959
  // facts = metadati JSON
1863
1960
  data.concepts?.join(", ") || null,
1864
1961
  data.files?.join(", ") || null,
1865
- data.files?.join(", ") || null,
1866
- 0
1962
+ null,
1963
+ // filesModified: knowledge non modifica file
1964
+ 0,
1867
1965
  // prompt_number
1966
+ contentHash,
1967
+ discoveryTokens
1868
1968
  );
1869
1969
  this.generateEmbeddingAsync(observationId, data.title, data.content, data.concepts).catch(() => {
1870
1970
  });
@@ -1880,11 +1980,11 @@ var KiroMemorySDK = class {
1880
1980
  "sdk-" + Date.now(),
1881
1981
  this.project,
1882
1982
  data.request || null,
1883
- null,
1983
+ data.investigated || null,
1884
1984
  data.learned || null,
1885
1985
  data.completed || null,
1886
1986
  data.nextSteps || null,
1887
- null
1987
+ data.notes || null
1888
1988
  );
1889
1989
  }
1890
1990
  /**
@@ -2068,7 +2168,14 @@ var KiroMemorySDK = class {
2068
2168
  }));
2069
2169
  } else {
2070
2170
  const observations = getObservationsByProject(this.db.db, this.project, 30);
2071
- items = observations.map((obs) => {
2171
+ const knowledgeTypes = new Set(KNOWLEDGE_TYPES);
2172
+ const knowledgeObs = [];
2173
+ const normalObs = [];
2174
+ for (const obs of observations) {
2175
+ if (knowledgeTypes.has(obs.type)) knowledgeObs.push(obs);
2176
+ else normalObs.push(obs);
2177
+ }
2178
+ const scoreObs = (obs) => {
2072
2179
  const signals = {
2073
2180
  semantic: 0,
2074
2181
  fts5: 0,
@@ -2076,7 +2183,6 @@ var KiroMemorySDK = class {
2076
2183
  projectMatch: projectMatchScore(obs.project, this.project)
2077
2184
  };
2078
2185
  const baseScore = computeCompositeScore(signals, CONTEXT_WEIGHTS);
2079
- const boostedScore = Math.min(1, baseScore * knowledgeTypeBoost(obs.type));
2080
2186
  return {
2081
2187
  id: obs.id,
2082
2188
  title: obs.title,
@@ -2085,17 +2191,23 @@ var KiroMemorySDK = class {
2085
2191
  project: obs.project,
2086
2192
  created_at: obs.created_at,
2087
2193
  created_at_epoch: obs.created_at_epoch,
2088
- score: boostedScore,
2194
+ score: Math.min(1, baseScore * knowledgeTypeBoost(obs.type)),
2089
2195
  signals
2090
2196
  };
2091
- });
2092
- items.sort((a, b) => b.score - a.score);
2197
+ };
2198
+ const scoredKnowledge = knowledgeObs.map(scoreObs).sort((a, b) => b.score - a.score);
2199
+ const scoredNormal = normalObs.map(scoreObs).sort((a, b) => b.score - a.score);
2200
+ items = [...scoredKnowledge, ...scoredNormal];
2093
2201
  }
2094
2202
  let tokensUsed = 0;
2203
+ const budgetItems = [];
2095
2204
  for (const item of items) {
2096
- tokensUsed += Math.ceil((item.title.length + item.content.length) / 4);
2097
- if (tokensUsed > tokenBudget) break;
2205
+ const itemTokens = Math.ceil((item.title.length + item.content.length) / 4);
2206
+ if (tokensUsed + itemTokens > tokenBudget) break;
2207
+ tokensUsed += itemTokens;
2208
+ budgetItems.push(item);
2098
2209
  }
2210
+ items = budgetItems;
2099
2211
  return {
2100
2212
  project: this.project,
2101
2213
  items,