engrm 0.4.26 → 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;
@@ -16321,6 +16394,163 @@ function sanitizeFtsQuery(query) {
16321
16394
  return safe;
16322
16395
  }
16323
16396
 
16397
+ // src/tools/search-chat.ts
16398
+ async function searchChat(db, input) {
16399
+ const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
16400
+ const projectScoped = input.project_scoped !== false;
16401
+ let projectId = null;
16402
+ let projectName;
16403
+ if (projectScoped) {
16404
+ const cwd = input.cwd ?? process.cwd();
16405
+ const detected = detectProject(cwd);
16406
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
16407
+ if (project) {
16408
+ projectId = project.id;
16409
+ projectName = project.name;
16410
+ }
16411
+ }
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) : [];
16420
+ return {
16421
+ messages,
16422
+ project: projectName,
16423
+ session_count: countDistinctSessions(messages),
16424
+ source_summary: summarizeChatSources(messages),
16425
+ transcript_backed: messages.some((message) => message.source_kind === "transcript"),
16426
+ semantic_backed: semantic.length > 0
16427
+ };
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
+ }
16445
+ function summarizeChatSources(messages) {
16446
+ return messages.reduce((summary, message) => {
16447
+ summary[message.source_kind] += 1;
16448
+ return summary;
16449
+ }, { transcript: 0, hook: 0 });
16450
+ }
16451
+ function countDistinctSessions(messages) {
16452
+ return new Set(messages.map((message) => message.session_id)).size;
16453
+ }
16454
+
16455
+ // src/tools/search-recall.ts
16456
+ async function searchRecall(db, input) {
16457
+ const query = input.query.trim();
16458
+ if (!query) {
16459
+ return {
16460
+ query,
16461
+ results: [],
16462
+ totals: { memory: 0, chat: 0 }
16463
+ };
16464
+ }
16465
+ const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
16466
+ const [memory, chat] = await Promise.all([
16467
+ searchObservations(db, input),
16468
+ searchChat(db, {
16469
+ query,
16470
+ limit: limit * 2,
16471
+ project_scoped: input.project_scoped,
16472
+ cwd: input.cwd,
16473
+ user_id: input.user_id
16474
+ })
16475
+ ]);
16476
+ const merged = mergeRecallResults(memory.observations, chat.messages, limit);
16477
+ return {
16478
+ query,
16479
+ project: memory.project ?? chat.project,
16480
+ results: merged,
16481
+ totals: {
16482
+ memory: memory.total,
16483
+ chat: chat.messages.length
16484
+ }
16485
+ };
16486
+ }
16487
+ function mergeRecallResults(memory, chat, limit) {
16488
+ const nowEpoch = Math.floor(Date.now() / 1000);
16489
+ const scored = [];
16490
+ for (let index = 0;index < memory.length; index++) {
16491
+ const item = memory[index];
16492
+ const base = 1 / (60 + index + 1);
16493
+ const score = base + Math.max(0, item.rank) * 0.08;
16494
+ scored.push({
16495
+ kind: "memory",
16496
+ rank: score,
16497
+ created_at: item.created_at,
16498
+ created_at_epoch: Math.floor(new Date(item.created_at).getTime() / 1000) || undefined,
16499
+ project_name: item.project_name,
16500
+ observation_id: item.id,
16501
+ id: item.id,
16502
+ session_id: null,
16503
+ type: item.type,
16504
+ title: item.title,
16505
+ detail: firstNonEmpty(item.narrative, parseFactsPreview(item.facts), item.files_modified ? `Files: ${item.files_modified}` : null, item.type) ?? item.type
16506
+ });
16507
+ }
16508
+ for (let index = 0;index < chat.length; index++) {
16509
+ const item = chat[index];
16510
+ const base = 1 / (60 + index + 1);
16511
+ const ageHours = Math.max(0, (nowEpoch - item.created_at_epoch) / 3600);
16512
+ const immediacyBoost = ageHours < 1 ? 1 : 0;
16513
+ const recencyBoost = ageHours < 24 ? 0.12 : ageHours < 72 ? 0.05 : 0.02;
16514
+ const sourceBoost = item.source_kind === "transcript" ? 0.06 : 0.03;
16515
+ scored.push({
16516
+ kind: "chat",
16517
+ rank: base + immediacyBoost + recencyBoost + sourceBoost,
16518
+ created_at_epoch: item.created_at_epoch,
16519
+ session_id: item.session_id,
16520
+ id: item.id,
16521
+ role: item.role,
16522
+ source_kind: item.source_kind,
16523
+ title: `${item.role} [${item.source_kind}]`,
16524
+ detail: item.content.replace(/\s+/g, " ").trim()
16525
+ });
16526
+ }
16527
+ return scored.sort((a, b) => {
16528
+ if (b.rank !== a.rank)
16529
+ return b.rank - a.rank;
16530
+ return (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0);
16531
+ }).slice(0, limit);
16532
+ }
16533
+ function parseFactsPreview(facts) {
16534
+ if (!facts)
16535
+ return null;
16536
+ try {
16537
+ const parsed = JSON.parse(facts);
16538
+ if (!Array.isArray(parsed) || parsed.length === 0)
16539
+ return null;
16540
+ const lines = parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
16541
+ return lines.length > 0 ? lines.slice(0, 2).join(" | ") : null;
16542
+ } catch {
16543
+ return facts;
16544
+ }
16545
+ }
16546
+ function firstNonEmpty(...values) {
16547
+ for (const value of values) {
16548
+ if (value && value.trim().length > 0)
16549
+ return value.trim();
16550
+ }
16551
+ return null;
16552
+ }
16553
+
16324
16554
  // src/tools/get.ts
16325
16555
  function getObservations(db, input) {
16326
16556
  if (input.ids.length === 0) {
@@ -16461,8 +16691,8 @@ function getRecentChat(db, input) {
16461
16691
  const messages2 = db.getSessionChatMessages(input.session_id, limit).slice(-limit).reverse();
16462
16692
  return {
16463
16693
  messages: messages2,
16464
- session_count: countDistinctSessions(messages2),
16465
- source_summary: summarizeChatSources(messages2),
16694
+ session_count: countDistinctSessions2(messages2),
16695
+ source_summary: summarizeChatSources2(messages2),
16466
16696
  transcript_backed: messages2.some((message) => message.source_kind === "transcript")
16467
16697
  };
16468
16698
  }
@@ -16479,40 +16709,6 @@ function getRecentChat(db, input) {
16479
16709
  }
16480
16710
  }
16481
16711
  const messages = db.getRecentChatMessages(projectId, limit, input.user_id);
16482
- return {
16483
- messages,
16484
- project: projectName,
16485
- session_count: countDistinctSessions(messages),
16486
- source_summary: summarizeChatSources(messages),
16487
- transcript_backed: messages.some((message) => message.source_kind === "transcript")
16488
- };
16489
- }
16490
- function summarizeChatSources(messages) {
16491
- return messages.reduce((summary, message) => {
16492
- summary[message.source_kind] += 1;
16493
- return summary;
16494
- }, { transcript: 0, hook: 0 });
16495
- }
16496
- function countDistinctSessions(messages) {
16497
- return new Set(messages.map((message) => message.session_id)).size;
16498
- }
16499
-
16500
- // src/tools/search-chat.ts
16501
- function searchChat(db, input) {
16502
- const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
16503
- const projectScoped = input.project_scoped !== false;
16504
- let projectId = null;
16505
- let projectName;
16506
- if (projectScoped) {
16507
- const cwd = input.cwd ?? process.cwd();
16508
- const detected = detectProject(cwd);
16509
- const project = db.getProjectByCanonicalId(detected.canonical_id);
16510
- if (project) {
16511
- projectId = project.id;
16512
- projectName = project.name;
16513
- }
16514
- }
16515
- const messages = db.searchChatMessages(input.query, projectId, limit, input.user_id);
16516
16712
  return {
16517
16713
  messages,
16518
16714
  project: projectName,
@@ -16552,6 +16748,8 @@ function getSessionStory(db, input) {
16552
16748
  summary,
16553
16749
  prompts,
16554
16750
  chat_messages: chatMessages,
16751
+ chat_source_summary: summarizeChatSources3(chatMessages),
16752
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
16555
16753
  tool_events: toolEvents,
16556
16754
  observations,
16557
16755
  handoffs,
@@ -16649,6 +16847,12 @@ function collectProvenanceSummary(observations) {
16649
16847
  }
16650
16848
  return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
16651
16849
  }
16850
+ function summarizeChatSources3(messages) {
16851
+ return messages.reduce((summary, message) => {
16852
+ summary[message.source_kind] += 1;
16853
+ return summary;
16854
+ }, { transcript: 0, hook: 0 });
16855
+ }
16652
16856
 
16653
16857
  // src/tools/handoffs.ts
16654
16858
  async function createHandoff(db, config2, input) {
@@ -17995,16 +18199,17 @@ function getProjectMemoryIndex(db, input) {
17995
18199
  }).handoffs;
17996
18200
  const rollingHandoffDraftsCount = recentHandoffsCount.filter((handoff) => isDraftHandoff(handoff)).length;
17997
18201
  const savedHandoffsCount = recentHandoffsCount.length - rollingHandoffDraftsCount;
17998
- const recentChatCount = getRecentChat(db, {
18202
+ const recentChat = getRecentChat(db, {
17999
18203
  cwd,
18000
18204
  project_scoped: true,
18001
18205
  user_id: input.user_id,
18002
18206
  limit: 20
18003
- }).messages.length;
18207
+ });
18208
+ const recentChatCount = recentChat.messages.length;
18004
18209
  const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !looksLikeFileOperationTitle3(title)).slice(0, 8);
18005
18210
  const captureSummary = summarizeCaptureState(recentSessions);
18006
18211
  const topTypes = Object.entries(counts).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
18007
- const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length);
18212
+ const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.transcript_backed);
18008
18213
  const estimatedReadTokens = estimateTokens([
18009
18214
  recentOutcomes.join(`
18010
18215
  `),
@@ -18029,6 +18234,9 @@ function getProjectMemoryIndex(db, input) {
18029
18234
  rolling_handoff_drafts_count: rollingHandoffDraftsCount,
18030
18235
  saved_handoffs_count: savedHandoffsCount,
18031
18236
  recent_chat_count: recentChatCount,
18237
+ recent_chat_sessions: recentChat.session_count,
18238
+ chat_source_summary: recentChat.source_summary,
18239
+ chat_coverage_state: recentChat.transcript_backed ? "transcript-backed" : recentChatCount > 0 ? "hook-only" : "none",
18032
18240
  raw_capture_active: recentRequestsCount > 0 || recentToolsCount > 0,
18033
18241
  capture_summary: captureSummary,
18034
18242
  hot_files: hotFiles,
@@ -18101,7 +18309,7 @@ function summarizeCaptureState(sessions) {
18101
18309
  }
18102
18310
  return summary;
18103
18311
  }
18104
- function buildSuggestedTools(sessions, requestCount, toolCount, observationCount) {
18312
+ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, transcriptBackedChat) {
18105
18313
  const suggested = [];
18106
18314
  if (sessions.length > 0) {
18107
18315
  suggested.push("recent_sessions");
@@ -18109,13 +18317,21 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
18109
18317
  if (requestCount > 0 || toolCount > 0) {
18110
18318
  suggested.push("activity_feed");
18111
18319
  }
18320
+ if (requestCount > 0 || recentChatCount > 0 || observationCount > 0) {
18321
+ suggested.push("search_recall");
18322
+ }
18112
18323
  if (observationCount > 0) {
18113
18324
  suggested.push("tool_memory_index", "capture_git_worktree");
18114
18325
  }
18115
18326
  if (sessions.length > 0) {
18116
18327
  suggested.push("create_handoff", "recent_handoffs");
18117
18328
  }
18118
- suggested.push("recent_chat");
18329
+ if (recentChatCount > 0 && !transcriptBackedChat) {
18330
+ suggested.push("refresh_chat_recall");
18331
+ }
18332
+ if (recentChatCount > 0) {
18333
+ suggested.push("recent_chat", "search_chat");
18334
+ }
18119
18335
  return Array.from(new Set(suggested)).slice(0, 4);
18120
18336
  }
18121
18337
 
@@ -18162,12 +18378,12 @@ function getMemoryConsole(db, input) {
18162
18378
  project_scoped: projectScoped,
18163
18379
  user_id: input.user_id,
18164
18380
  limit: 6
18165
- }).messages;
18381
+ });
18166
18382
  const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
18167
18383
  cwd,
18168
18384
  user_id: input.user_id
18169
18385
  }) : null;
18170
- const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
18386
+ const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.messages.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
18171
18387
  return {
18172
18388
  project: project?.name,
18173
18389
  capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
@@ -18179,7 +18395,10 @@ function getMemoryConsole(db, input) {
18179
18395
  recent_handoffs: recentHandoffs,
18180
18396
  rolling_handoff_drafts: rollingHandoffDrafts,
18181
18397
  saved_handoffs: savedHandoffs,
18182
- recent_chat: recentChat,
18398
+ recent_chat: recentChat.messages,
18399
+ recent_chat_sessions: projectIndex?.recent_chat_sessions ?? recentChat.session_count,
18400
+ chat_source_summary: projectIndex?.chat_source_summary ?? recentChat.source_summary,
18401
+ chat_coverage_state: projectIndex?.chat_coverage_state ?? (recentChat.transcript_backed ? "transcript-backed" : recentChat.messages.length > 0 ? "hook-only" : "none"),
18183
18402
  observations,
18184
18403
  capture_summary: projectIndex?.capture_summary,
18185
18404
  recent_outcomes: projectIndex?.recent_outcomes ?? [],
@@ -18189,23 +18408,27 @@ function getMemoryConsole(db, input) {
18189
18408
  assistant_checkpoint_types: projectIndex?.assistant_checkpoint_types ?? [],
18190
18409
  top_types: projectIndex?.top_types ?? [],
18191
18410
  estimated_read_tokens: projectIndex?.estimated_read_tokens,
18192
- suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.length)
18411
+ suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.transcript_backed)
18193
18412
  };
18194
18413
  }
18195
- function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount) {
18414
+ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, transcriptBackedChat) {
18196
18415
  const suggested = [];
18197
18416
  if (sessionCount > 0)
18198
18417
  suggested.push("recent_sessions");
18199
18418
  if (requestCount > 0 || toolCount > 0)
18200
18419
  suggested.push("activity_feed");
18420
+ if (requestCount > 0 || chatCount > 0 || observationCount > 0)
18421
+ suggested.push("search_recall");
18201
18422
  if (observationCount > 0)
18202
18423
  suggested.push("tool_memory_index", "capture_git_worktree");
18203
18424
  if (sessionCount > 0)
18204
18425
  suggested.push("create_handoff", "recent_handoffs");
18205
18426
  if (handoffCount > 0)
18206
18427
  suggested.push("load_handoff");
18428
+ if (chatCount > 0 && !transcriptBackedChat)
18429
+ suggested.push("refresh_chat_recall");
18207
18430
  if (chatCount > 0)
18208
- suggested.push("recent_chat");
18431
+ suggested.push("recent_chat", "search_chat");
18209
18432
  return Array.from(new Set(suggested)).slice(0, 4);
18210
18433
  }
18211
18434
 
@@ -18428,7 +18651,7 @@ function toChatEvent(message) {
18428
18651
  created_at_epoch: message.created_at_epoch,
18429
18652
  session_id: message.session_id,
18430
18653
  id: message.id,
18431
- title: message.role === "user" ? "user" : "assistant",
18654
+ title: `${message.role} [${message.source_kind}]`,
18432
18655
  detail: content.slice(0, 220)
18433
18656
  };
18434
18657
  }
@@ -19007,12 +19230,13 @@ function getSessionContext(db, input) {
19007
19230
  const rollingHandoffDrafts = (context.recentHandoffs ?? []).filter((handoff) => handoff.title.startsWith("Handoff Draft:")).length;
19008
19231
  const savedHandoffs = recentHandoffs - rollingHandoffDrafts;
19009
19232
  const latestHandoffTitle = context.recentHandoffs?.[0]?.title ?? null;
19010
- const recentChatMessages = getRecentChat(db, {
19233
+ const recentChat = getRecentChat(db, {
19011
19234
  cwd,
19012
19235
  project_scoped: true,
19013
19236
  user_id: input.user_id,
19014
19237
  limit: 8
19015
- }).messages.length;
19238
+ });
19239
+ const recentChatMessages = recentChat.messages.length;
19016
19240
  const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
19017
19241
  const hotFiles = buildHotFiles(context);
19018
19242
  const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
@@ -19031,12 +19255,15 @@ function getSessionContext(db, input) {
19031
19255
  saved_handoffs: savedHandoffs,
19032
19256
  latest_handoff_title: latestHandoffTitle,
19033
19257
  recent_chat_messages: recentChatMessages,
19258
+ recent_chat_sessions: recentChat.session_count,
19259
+ chat_source_summary: recentChat.source_summary,
19260
+ chat_coverage_state: recentChat.transcript_backed ? "transcript-backed" : recentChatMessages > 0 ? "hook-only" : "none",
19034
19261
  recent_outcomes: context.recentOutcomes ?? [],
19035
19262
  hot_files: hotFiles,
19036
19263
  capture_state: captureState,
19037
19264
  raw_capture_active: recentRequests > 0 || recentTools > 0,
19038
19265
  estimated_read_tokens: estimateTokens(preview),
19039
- suggested_tools: buildSuggestedTools2(context),
19266
+ suggested_tools: buildSuggestedTools2(context, recentChat.transcript_backed),
19040
19267
  preview
19041
19268
  };
19042
19269
  }
@@ -19059,7 +19286,7 @@ function parseJsonArray3(value) {
19059
19286
  return [];
19060
19287
  }
19061
19288
  }
19062
- function buildSuggestedTools2(context) {
19289
+ function buildSuggestedTools2(context, transcriptBackedChat) {
19063
19290
  const tools = [];
19064
19291
  if ((context.recentSessions?.length ?? 0) > 0) {
19065
19292
  tools.push("recent_sessions");
@@ -19067,6 +19294,9 @@ function buildSuggestedTools2(context) {
19067
19294
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
19068
19295
  tools.push("activity_feed");
19069
19296
  }
19297
+ if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
19298
+ tools.push("search_recall");
19299
+ }
19070
19300
  if (context.observations.length > 0) {
19071
19301
  tools.push("tool_memory_index", "capture_git_worktree");
19072
19302
  }
@@ -19076,7 +19306,12 @@ function buildSuggestedTools2(context) {
19076
19306
  if ((context.recentHandoffs?.length ?? 0) > 0) {
19077
19307
  tools.push("load_handoff");
19078
19308
  }
19079
- tools.push("recent_chat", "search_chat", "refresh_chat_recall");
19309
+ if ((context.recentChatMessages?.length ?? 0) > 0 && !transcriptBackedChat) {
19310
+ tools.push("refresh_chat_recall");
19311
+ }
19312
+ if ((context.recentChatMessages?.length ?? 0) > 0) {
19313
+ tools.push("recent_chat", "search_chat");
19314
+ }
19080
19315
  return Array.from(new Set(tools)).slice(0, 4);
19081
19316
  }
19082
19317
 
@@ -21047,7 +21282,7 @@ function readTranscript(sessionId, cwd, transcriptPath) {
21047
21282
  }
21048
21283
  return messages;
21049
21284
  }
21050
- function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
21285
+ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
21051
21286
  const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
21052
21287
  ...message,
21053
21288
  text: message.text.trim()
@@ -21077,6 +21312,12 @@ function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
21077
21312
  transcript_index: transcriptIndex
21078
21313
  });
21079
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
+ }
21080
21321
  imported++;
21081
21322
  }
21082
21323
  return { imported, total: messages.length };
@@ -21152,7 +21393,7 @@ process.on("SIGTERM", () => {
21152
21393
  });
21153
21394
  var server = new McpServer({
21154
21395
  name: "engrm",
21155
- version: "0.4.26"
21396
+ version: "0.4.28"
21156
21397
  });
21157
21398
  server.tool("save_observation", "Save an observation to memory", {
21158
21399
  type: exports_external.enum([
@@ -21545,6 +21786,58 @@ ${previews.join(`
21545
21786
  ]
21546
21787
  };
21547
21788
  });
21789
+ server.tool("search_recall", "Search live recall across durable memory and chat together. Best for questions like 'what were we just talking about?'", {
21790
+ query: exports_external.string().describe("Recall query"),
21791
+ project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
21792
+ limit: exports_external.number().optional().describe("Max results (default: 10)"),
21793
+ cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
21794
+ user_id: exports_external.string().optional().describe("Optional user override")
21795
+ }, async (params) => {
21796
+ const result = await searchRecall(db, {
21797
+ query: params.query,
21798
+ project_scoped: params.project_scoped,
21799
+ limit: params.limit,
21800
+ cwd: params.cwd,
21801
+ user_id: params.user_id ?? config2.user_id
21802
+ });
21803
+ if (result.results.length === 0) {
21804
+ return {
21805
+ content: [
21806
+ {
21807
+ type: "text",
21808
+ text: result.project ? `No recall found for "${params.query}" in project ${result.project}` : `No recall found for "${params.query}"`
21809
+ }
21810
+ ]
21811
+ };
21812
+ }
21813
+ const projectLine = result.project ? `Project: ${result.project}
21814
+ ` : "";
21815
+ const summaryLine = `Matches: ${result.results.length} · memory ${result.totals.memory} · chat ${result.totals.chat}
21816
+ `;
21817
+ const rows = result.results.map((item) => {
21818
+ const sourceBits = [item.kind];
21819
+ if (item.type)
21820
+ sourceBits.push(item.type);
21821
+ if (item.role)
21822
+ sourceBits.push(item.role);
21823
+ if (item.source_kind)
21824
+ sourceBits.push(item.source_kind);
21825
+ const idBit = item.observation_id ? `#${item.observation_id}` : item.id ? `chat:${item.id}` : "";
21826
+ const title = `${idBit ? `${idBit} ` : ""}${item.title}${item.project_name ? ` (${item.project_name})` : ""}`;
21827
+ return `- [${sourceBits.join(" · ")}] ${title}
21828
+ ${item.detail.slice(0, 220)}`;
21829
+ }).join(`
21830
+ `);
21831
+ return {
21832
+ content: [
21833
+ {
21834
+ type: "text",
21835
+ text: `${projectLine}${summaryLine}Recall search for "${params.query}":
21836
+ ${rows}`
21837
+ }
21838
+ ]
21839
+ };
21840
+ });
21548
21841
  server.tool("get_observations", "Get observations by ID", {
21549
21842
  ids: exports_external.array(exports_external.number()).describe("Observation IDs")
21550
21843
  }, async (params) => {
@@ -21865,6 +22158,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
21865
22158
  {
21866
22159
  type: "text",
21867
22160
  text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
22161
+ ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat.length} messages across ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
21868
22162
  ` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
21869
22163
  ` : ""}` + `Handoffs: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
21870
22164
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
@@ -22071,6 +22365,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
22071
22365
  ` + `Recent handoffs: ${result.recent_handoffs}
22072
22366
  ` + `Handoff split: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
22073
22367
  ` + `Recent chat messages: ${result.recent_chat_messages}
22368
+ ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
22074
22369
  ` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
22075
22370
  ` + `Raw chronology active: ${result.raw_capture_active ? "yes" : "no"}
22076
22371
 
@@ -22150,6 +22445,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
22150
22445
  ` + `Recent handoffs captured: ${result.recent_handoffs_count}
22151
22446
  ` + `Handoff split: ${result.saved_handoffs_count} saved, ${result.rolling_handoff_drafts_count} rolling drafts
22152
22447
  ` + `Recent chat messages captured: ${result.recent_chat_count}
22448
+ ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
22153
22449
 
22154
22450
  ` + `Raw chronology: ${result.raw_capture_active ? "active" : "observations-only so far"}
22155
22451
 
@@ -22321,7 +22617,7 @@ server.tool("refresh_chat_recall", "Hydrate the separate chat lane from the curr
22321
22617
  content: [{ type: "text", text: "No session available to hydrate chat recall from." }]
22322
22618
  };
22323
22619
  }
22324
- const result = syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
22620
+ const result = await syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
22325
22621
  return {
22326
22622
  content: [
22327
22623
  {
@@ -22437,10 +22733,10 @@ server.tool("search_chat", "Search the separate chat lane without mixing it into
22437
22733
  cwd: exports_external.string().optional(),
22438
22734
  user_id: exports_external.string().optional()
22439
22735
  }, async (params) => {
22440
- const result = searchChat(db, params);
22736
+ const result = await searchChat(db, params);
22441
22737
  const projectLine = result.project ? `Project: ${result.project}
22442
22738
  ` : "";
22443
- 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" : ""}
22444
22740
  ` + `${result.transcript_backed ? "" : `Hint: run refresh_chat_recall if this looks under-captured.
22445
22741
  `}`;
22446
22742
  const rows = result.messages.length > 0 ? result.messages.map((msg) => {
@@ -22554,7 +22850,7 @@ server.tool("session_story", "Show the full local memory story for one session",
22554
22850
  `) : "(none)";
22555
22851
  const promptLines = result.prompts.length > 0 ? result.prompts.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
22556
22852
  `) : "- (none)";
22557
- const chatLines = result.chat_messages.length > 0 ? result.chat_messages.slice(-12).map((msg) => `- [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`).join(`
22853
+ const chatLines = result.chat_messages.length > 0 ? result.chat_messages.slice(-12).map((msg) => `- [${msg.role}] [${msg.source_kind}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`).join(`
22558
22854
  `) : "- (none)";
22559
22855
  const toolLines = result.tool_events.length > 0 ? result.tool_events.slice(-15).map((tool) => {
22560
22856
  const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
@@ -22596,6 +22892,8 @@ ${summaryLines}
22596
22892
  ` + `Prompts:
22597
22893
  ${promptLines}
22598
22894
 
22895
+ ` + `Chat recall: ${result.chat_coverage_state} (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
22896
+
22599
22897
  ` + `Chat:
22600
22898
  ${chatLines}
22601
22899