engrm 0.4.27 → 0.4.28

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/dist/server.js CHANGED
@@ -14230,6 +14230,17 @@ var MIGRATIONS = [
14230
14230
  ON chat_messages(session_id, transcript_index)
14231
14231
  WHERE transcript_index IS NOT NULL;
14232
14232
  `
14233
+ },
14234
+ {
14235
+ version: 18,
14236
+ description: "Add sqlite-vec semantic search for chat recall",
14237
+ condition: (db) => isVecExtensionLoaded(db),
14238
+ sql: `
14239
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
14240
+ chat_message_id INTEGER PRIMARY KEY,
14241
+ embedding FLOAT[384]
14242
+ );
14243
+ `
14233
14244
  }
14234
14245
  ];
14235
14246
  function isVecExtensionLoaded(db) {
@@ -14303,6 +14314,9 @@ function inferLegacySchemaVersion(db) {
14303
14314
  if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
14304
14315
  version2 = Math.max(version2, 17);
14305
14316
  }
14317
+ if (tableExists(db, "vec_chat_messages")) {
14318
+ version2 = Math.max(version2, 18);
14319
+ }
14306
14320
  return version2;
14307
14321
  }
14308
14322
  function runMigrations(db) {
@@ -14419,6 +14433,20 @@ function ensureChatMessageColumns(db) {
14419
14433
  db.exec("PRAGMA user_version = 17");
14420
14434
  }
14421
14435
  }
14436
+ function ensureChatVectorTable(db) {
14437
+ if (!isVecExtensionLoaded(db))
14438
+ return;
14439
+ db.exec(`
14440
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
14441
+ chat_message_id INTEGER PRIMARY KEY,
14442
+ embedding FLOAT[384]
14443
+ );
14444
+ `);
14445
+ const current = getSchemaVersion(db);
14446
+ if (current < 18) {
14447
+ db.exec("PRAGMA user_version = 18");
14448
+ }
14449
+ }
14422
14450
  function ensureSyncOutboxSupportsChatMessages(db) {
14423
14451
  if (syncOutboxSupportsChatMessages(db)) {
14424
14452
  const current = getSchemaVersion(db);
@@ -14632,6 +14660,7 @@ class MemDatabase {
14632
14660
  ensureObservationTypes(this.db);
14633
14661
  ensureSessionSummaryColumns(this.db);
14634
14662
  ensureChatMessageColumns(this.db);
14663
+ ensureChatVectorTable(this.db);
14635
14664
  ensureSyncOutboxSupportsChatMessages(this.db);
14636
14665
  }
14637
14666
  loadVecExtension() {
@@ -14998,6 +15027,14 @@ class MemDatabase {
14998
15027
  getChatMessageByRemoteSourceId(remoteSourceId) {
14999
15028
  return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
15000
15029
  }
15030
+ getChatMessagesByIds(ids) {
15031
+ if (ids.length === 0)
15032
+ return [];
15033
+ const placeholders = ids.map(() => "?").join(",");
15034
+ const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
15035
+ const order = new Map(ids.map((id, index) => [id, index]));
15036
+ return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
15037
+ }
15001
15038
  getSessionChatMessages(sessionId, limit = 50) {
15002
15039
  return this.db.query(`SELECT * FROM chat_messages
15003
15040
  WHERE session_id = ?
@@ -15074,6 +15111,39 @@ class MemDatabase {
15074
15111
  ORDER BY created_at_epoch DESC, id DESC
15075
15112
  LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
15076
15113
  }
15114
+ vecChatInsert(chatMessageId, embedding) {
15115
+ if (!this.vecAvailable)
15116
+ return;
15117
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
15118
+ }
15119
+ searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
15120
+ if (!this.vecAvailable)
15121
+ return [];
15122
+ const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
15123
+ const visibilityClause = userId ? " AND c.user_id = ?" : "";
15124
+ const transcriptPreference = `
15125
+ AND (
15126
+ c.source_kind = 'transcript'
15127
+ OR NOT EXISTS (
15128
+ SELECT 1 FROM chat_messages t2
15129
+ WHERE t2.session_id = c.session_id
15130
+ AND t2.source_kind = 'transcript'
15131
+ )
15132
+ )`;
15133
+ if (projectId !== null) {
15134
+ return this.db.query(`SELECT v.chat_message_id, v.distance
15135
+ FROM vec_chat_messages v
15136
+ JOIN chat_messages c ON c.id = v.chat_message_id
15137
+ WHERE v.embedding MATCH ?
15138
+ AND k = ?
15139
+ AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
15140
+ }
15141
+ return this.db.query(`SELECT v.chat_message_id, v.distance
15142
+ FROM vec_chat_messages v
15143
+ JOIN chat_messages c ON c.id = v.chat_message_id
15144
+ WHERE v.embedding MATCH ?
15145
+ AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
15146
+ }
15077
15147
  getTranscriptChatMessage(sessionId, transcriptIndex) {
15078
15148
  return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
15079
15149
  }
@@ -15815,6 +15885,9 @@ function composeEmbeddingText(obs) {
15815
15885
 
15816
15886
  `);
15817
15887
  }
15888
+ function composeChatEmbeddingText(text) {
15889
+ return text.replace(/\s+/g, " ").trim().slice(0, 2000);
15890
+ }
15818
15891
  async function getPipeline() {
15819
15892
  if (_pipeline)
15820
15893
  return _pipeline;
@@ -16322,7 +16395,7 @@ function sanitizeFtsQuery(query) {
16322
16395
  }
16323
16396
 
16324
16397
  // src/tools/search-chat.ts
16325
- function searchChat(db, input) {
16398
+ async function searchChat(db, input) {
16326
16399
  const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
16327
16400
  const projectScoped = input.project_scoped !== false;
16328
16401
  let projectId = null;
@@ -16336,15 +16409,39 @@ function searchChat(db, input) {
16336
16409
  projectName = project.name;
16337
16410
  }
16338
16411
  }
16339
- const messages = db.searchChatMessages(input.query, projectId, limit, input.user_id);
16412
+ const lexical = db.searchChatMessages(input.query, projectId, limit * 2, input.user_id);
16413
+ let semantic = [];
16414
+ const queryEmbedding = db.vecAvailable ? await embedText(composeChatEmbeddingText(queryForEmbedding(input.query))) : null;
16415
+ if (queryEmbedding && db.vecAvailable) {
16416
+ semantic = db.searchChatVec(queryEmbedding, projectId, limit * 2, input.user_id);
16417
+ }
16418
+ const messageIds = mergeChatResults(lexical, semantic, limit);
16419
+ const messages = messageIds.length > 0 ? db.getChatMessagesByIds(messageIds) : [];
16340
16420
  return {
16341
16421
  messages,
16342
16422
  project: projectName,
16343
16423
  session_count: countDistinctSessions(messages),
16344
16424
  source_summary: summarizeChatSources(messages),
16345
- transcript_backed: messages.some((message) => message.source_kind === "transcript")
16425
+ transcript_backed: messages.some((message) => message.source_kind === "transcript"),
16426
+ semantic_backed: semantic.length > 0
16346
16427
  };
16347
16428
  }
16429
+ var RRF_K2 = 40;
16430
+ function mergeChatResults(lexical, semantic, limit) {
16431
+ const scores = new Map;
16432
+ for (let rank = 0;rank < lexical.length; rank++) {
16433
+ const message = lexical[rank];
16434
+ scores.set(message.id, (scores.get(message.id) ?? 0) + 1 / (RRF_K2 + rank + 1));
16435
+ }
16436
+ for (let rank = 0;rank < semantic.length; rank++) {
16437
+ const match = semantic[rank];
16438
+ scores.set(match.chat_message_id, (scores.get(match.chat_message_id) ?? 0) + 1 / (RRF_K2 + rank + 1));
16439
+ }
16440
+ return Array.from(scores.entries()).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([id]) => id);
16441
+ }
16442
+ function queryForEmbedding(query) {
16443
+ return query.replace(/\s+/g, " ").trim().slice(0, 400);
16444
+ }
16348
16445
  function summarizeChatSources(messages) {
16349
16446
  return messages.reduce((summary, message) => {
16350
16447
  summary[message.source_kind] += 1;
@@ -16368,13 +16465,13 @@ async function searchRecall(db, input) {
16368
16465
  const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
16369
16466
  const [memory, chat] = await Promise.all([
16370
16467
  searchObservations(db, input),
16371
- Promise.resolve(searchChat(db, {
16468
+ searchChat(db, {
16372
16469
  query,
16373
16470
  limit: limit * 2,
16374
16471
  project_scoped: input.project_scoped,
16375
16472
  cwd: input.cwd,
16376
16473
  user_id: input.user_id
16377
- }))
16474
+ })
16378
16475
  ]);
16379
16476
  const merged = mergeRecallResults(memory.observations, chat.messages, limit);
16380
16477
  return {
@@ -18220,6 +18317,9 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
18220
18317
  if (requestCount > 0 || toolCount > 0) {
18221
18318
  suggested.push("activity_feed");
18222
18319
  }
18320
+ if (requestCount > 0 || recentChatCount > 0 || observationCount > 0) {
18321
+ suggested.push("search_recall");
18322
+ }
18223
18323
  if (observationCount > 0) {
18224
18324
  suggested.push("tool_memory_index", "capture_git_worktree");
18225
18325
  }
@@ -18317,6 +18417,8 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
18317
18417
  suggested.push("recent_sessions");
18318
18418
  if (requestCount > 0 || toolCount > 0)
18319
18419
  suggested.push("activity_feed");
18420
+ if (requestCount > 0 || chatCount > 0 || observationCount > 0)
18421
+ suggested.push("search_recall");
18320
18422
  if (observationCount > 0)
18321
18423
  suggested.push("tool_memory_index", "capture_git_worktree");
18322
18424
  if (sessionCount > 0)
@@ -19192,6 +19294,9 @@ function buildSuggestedTools2(context, transcriptBackedChat) {
19192
19294
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
19193
19295
  tools.push("activity_feed");
19194
19296
  }
19297
+ if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
19298
+ tools.push("search_recall");
19299
+ }
19195
19300
  if (context.observations.length > 0) {
19196
19301
  tools.push("tool_memory_index", "capture_git_worktree");
19197
19302
  }
@@ -21177,7 +21282,7 @@ function readTranscript(sessionId, cwd, transcriptPath) {
21177
21282
  }
21178
21283
  return messages;
21179
21284
  }
21180
- function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
21285
+ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
21181
21286
  const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
21182
21287
  ...message,
21183
21288
  text: message.text.trim()
@@ -21207,6 +21312,12 @@ function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
21207
21312
  transcript_index: transcriptIndex
21208
21313
  });
21209
21314
  db.addToOutbox("chat_message", row.id);
21315
+ if (db.vecAvailable) {
21316
+ const embedding = await embedText(composeChatEmbeddingText(message.text));
21317
+ if (embedding) {
21318
+ db.vecChatInsert(row.id, embedding);
21319
+ }
21320
+ }
21210
21321
  imported++;
21211
21322
  }
21212
21323
  return { imported, total: messages.length };
@@ -21282,7 +21393,7 @@ process.on("SIGTERM", () => {
21282
21393
  });
21283
21394
  var server = new McpServer({
21284
21395
  name: "engrm",
21285
- version: "0.4.27"
21396
+ version: "0.4.28"
21286
21397
  });
21287
21398
  server.tool("save_observation", "Save an observation to memory", {
21288
21399
  type: exports_external.enum([
@@ -22506,7 +22617,7 @@ server.tool("refresh_chat_recall", "Hydrate the separate chat lane from the curr
22506
22617
  content: [{ type: "text", text: "No session available to hydrate chat recall from." }]
22507
22618
  };
22508
22619
  }
22509
- const result = syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
22620
+ const result = await syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
22510
22621
  return {
22511
22622
  content: [
22512
22623
  {
@@ -22622,10 +22733,10 @@ server.tool("search_chat", "Search the separate chat lane without mixing it into
22622
22733
  cwd: exports_external.string().optional(),
22623
22734
  user_id: exports_external.string().optional()
22624
22735
  }, async (params) => {
22625
- const result = searchChat(db, params);
22736
+ const result = await searchChat(db, params);
22626
22737
  const projectLine = result.project ? `Project: ${result.project}
22627
22738
  ` : "";
22628
- const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · hook ${result.source_summary.hook}
22739
+ const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · hook ${result.source_summary.hook}` + `${result.semantic_backed ? " · semantic yes" : ""}
22629
22740
  ` + `${result.transcript_backed ? "" : `Hint: run refresh_chat_recall if this looks under-captured.
22630
22741
  `}`;
22631
22742
  const rows = result.messages.length > 0 ? result.messages.map((msg) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
4
4
  "description": "Shared memory across devices, sessions, and coding agents",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",