engrm 0.4.26 → 0.4.27

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
@@ -228,6 +228,7 @@ The MCP server exposes tools that supported agents can call directly:
228
228
  | `refresh_chat_recall` | Rehydrate the separate chat lane from a Claude transcript when a long session feels under-captured |
229
229
  | `recent_chat` | Inspect the separate synced chat lane without mixing it into durable memory |
230
230
  | `search_chat` | Search recent chat recall separately from reusable memory observations |
231
+ | `search_recall` | Search durable memory and chat recall together when you do not want to guess the right lane |
231
232
  | `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
232
233
  | `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
233
234
  | `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
@@ -359,6 +360,8 @@ What each tool is good for:
359
360
  - `capture_status` tells you whether prompt/tool hooks are live on this machine
360
361
  - `capture_quality` shows whether chat recall is transcript-backed or still hook-only across the workspace
361
362
  - `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
363
+ - `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed or only hook-captured
364
+ - when chat continuity is only hook-captured, the workbench and startup hints now prefer `refresh_chat_recall`
362
365
  - `activity_feed` shows the merged chronology across prompts, tools, chat, handoffs, observations, and summaries
363
366
  - `recent_sessions` helps you pick a session worth opening
364
367
  - `session_story` reconstructs one session in detail, including handoffs and chat recall
@@ -3831,6 +3831,8 @@ function getSessionStory(db, input) {
3831
3831
  summary,
3832
3832
  prompts,
3833
3833
  chat_messages: chatMessages,
3834
+ chat_source_summary: summarizeChatSources(chatMessages),
3835
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
3834
3836
  tool_events: toolEvents,
3835
3837
  observations,
3836
3838
  handoffs,
@@ -3928,6 +3930,12 @@ function collectProvenanceSummary(observations) {
3928
3930
  }
3929
3931
  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);
3930
3932
  }
3933
+ function summarizeChatSources(messages) {
3934
+ return messages.reduce((summary, message) => {
3935
+ summary[message.source_kind] += 1;
3936
+ return summary;
3937
+ }, { transcript: 0, hook: 0 });
3938
+ }
3931
3939
 
3932
3940
  // src/tools/handoffs.ts
3933
3941
  async function upsertRollingHandoff(db, config, input) {
@@ -2154,6 +2154,8 @@ function getSessionStory(db, input) {
2154
2154
  summary,
2155
2155
  prompts,
2156
2156
  chat_messages: chatMessages,
2157
+ chat_source_summary: summarizeChatSources(chatMessages),
2158
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
2157
2159
  tool_events: toolEvents,
2158
2160
  observations,
2159
2161
  handoffs,
@@ -2251,6 +2253,12 @@ function collectProvenanceSummary(observations) {
2251
2253
  }
2252
2254
  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);
2253
2255
  }
2256
+ function summarizeChatSources(messages) {
2257
+ return messages.reduce((summary, message) => {
2258
+ summary[message.source_kind] += 1;
2259
+ return summary;
2260
+ }, { transcript: 0, hook: 0 });
2261
+ }
2254
2262
 
2255
2263
  // src/tools/save.ts
2256
2264
  import { relative, isAbsolute } from "node:path";
@@ -494,6 +494,8 @@ function getSessionStory(db, input) {
494
494
  summary,
495
495
  prompts,
496
496
  chat_messages: chatMessages,
497
+ chat_source_summary: summarizeChatSources(chatMessages),
498
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
497
499
  tool_events: toolEvents,
498
500
  observations,
499
501
  handoffs,
@@ -591,6 +593,12 @@ function collectProvenanceSummary(observations) {
591
593
  }
592
594
  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);
593
595
  }
596
+ function summarizeChatSources(messages) {
597
+ return messages.reduce((summary, message) => {
598
+ summary[message.source_kind] += 1;
599
+ return summary;
600
+ }, { transcript: 0, hook: 0 });
601
+ }
594
602
 
595
603
  // src/tools/save.ts
596
604
  import { relative, isAbsolute } from "node:path";
@@ -3049,7 +3057,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3049
3057
  import { join as join3 } from "node:path";
3050
3058
  import { homedir } from "node:os";
3051
3059
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3052
- var CLIENT_VERSION = "0.4.26";
3060
+ var CLIENT_VERSION = "0.4.27";
3053
3061
  function hashFile(filePath) {
3054
3062
  try {
3055
3063
  if (!existsSync3(filePath))
@@ -5671,10 +5679,12 @@ function formatInspectHints(context, visibleObservationIds = []) {
5671
5679
  if ((context.recentChatMessages?.length ?? 0) > 0) {
5672
5680
  hints.push("recent_chat");
5673
5681
  }
5682
+ if (hasHookOnlyRecentChat(context)) {
5683
+ hints.push("refresh_chat_recall");
5684
+ }
5674
5685
  if (continuityState !== "fresh") {
5675
5686
  hints.push("recent_chat");
5676
5687
  hints.push("recent_handoffs");
5677
- hints.push("refresh_chat_recall");
5678
5688
  }
5679
5689
  const unique = Array.from(new Set(hints)).slice(0, 4);
5680
5690
  if (unique.length === 0)
@@ -6144,6 +6154,10 @@ function hasFreshContinuitySignal(context) {
6144
6154
  function getStartupContinuityState(context) {
6145
6155
  return classifyContinuityState(context.recentPrompts?.length ?? 0, context.recentToolEvents?.length ?? 0, context.recentHandoffs?.length ?? 0, context.recentChatMessages?.length ?? 0, context.recentSessions ?? [], context.recentOutcomes?.length ?? 0);
6146
6156
  }
6157
+ function hasHookOnlyRecentChat(context) {
6158
+ const recentChat = context.recentChatMessages ?? [];
6159
+ return recentChat.length > 0 && !recentChat.some((message) => message.source_kind === "transcript");
6160
+ }
6147
6161
  function observationAgeDays3(obs) {
6148
6162
  const createdAt = new Date(obs.created_at).getTime();
6149
6163
  if (!Number.isFinite(createdAt))
@@ -3009,7 +3009,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3009
3009
  sentinel_used: valueSignals.security_findings_count > 0,
3010
3010
  risk_score: riskScore,
3011
3011
  stacks_detected: stacks,
3012
- client_version: "0.4.26",
3012
+ client_version: "0.4.27",
3013
3013
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3014
3014
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3015
3015
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -4040,6 +4040,8 @@ function getSessionStory(db, input) {
4040
4040
  summary,
4041
4041
  prompts,
4042
4042
  chat_messages: chatMessages,
4043
+ chat_source_summary: summarizeChatSources(chatMessages),
4044
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
4043
4045
  tool_events: toolEvents,
4044
4046
  observations,
4045
4047
  handoffs,
@@ -4137,6 +4139,12 @@ function collectProvenanceSummary(observations) {
4137
4139
  }
4138
4140
  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);
4139
4141
  }
4142
+ function summarizeChatSources(messages) {
4143
+ return messages.reduce((summary, message) => {
4144
+ summary[message.source_kind] += 1;
4145
+ return summary;
4146
+ }, { transcript: 0, hook: 0 });
4147
+ }
4140
4148
 
4141
4149
  // src/tools/handoffs.ts
4142
4150
  async function upsertRollingHandoff(db, config, input) {
@@ -2924,6 +2924,8 @@ function getSessionStory(db, input) {
2924
2924
  summary,
2925
2925
  prompts,
2926
2926
  chat_messages: chatMessages,
2927
+ chat_source_summary: summarizeChatSources(chatMessages),
2928
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
2927
2929
  tool_events: toolEvents,
2928
2930
  observations,
2929
2931
  handoffs,
@@ -3021,6 +3023,12 @@ function collectProvenanceSummary(observations) {
3021
3023
  }
3022
3024
  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);
3023
3025
  }
3026
+ function summarizeChatSources(messages) {
3027
+ return messages.reduce((summary, message) => {
3028
+ summary[message.source_kind] += 1;
3029
+ return summary;
3030
+ }, { transcript: 0, hook: 0 });
3031
+ }
3024
3032
 
3025
3033
  // src/tools/handoffs.ts
3026
3034
  async function upsertRollingHandoff(db, config, input) {
package/dist/server.js CHANGED
@@ -16321,6 +16321,139 @@ function sanitizeFtsQuery(query) {
16321
16321
  return safe;
16322
16322
  }
16323
16323
 
16324
+ // src/tools/search-chat.ts
16325
+ function searchChat(db, input) {
16326
+ const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
16327
+ const projectScoped = input.project_scoped !== false;
16328
+ let projectId = null;
16329
+ let projectName;
16330
+ if (projectScoped) {
16331
+ const cwd = input.cwd ?? process.cwd();
16332
+ const detected = detectProject(cwd);
16333
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
16334
+ if (project) {
16335
+ projectId = project.id;
16336
+ projectName = project.name;
16337
+ }
16338
+ }
16339
+ const messages = db.searchChatMessages(input.query, projectId, limit, input.user_id);
16340
+ return {
16341
+ messages,
16342
+ project: projectName,
16343
+ session_count: countDistinctSessions(messages),
16344
+ source_summary: summarizeChatSources(messages),
16345
+ transcript_backed: messages.some((message) => message.source_kind === "transcript")
16346
+ };
16347
+ }
16348
+ function summarizeChatSources(messages) {
16349
+ return messages.reduce((summary, message) => {
16350
+ summary[message.source_kind] += 1;
16351
+ return summary;
16352
+ }, { transcript: 0, hook: 0 });
16353
+ }
16354
+ function countDistinctSessions(messages) {
16355
+ return new Set(messages.map((message) => message.session_id)).size;
16356
+ }
16357
+
16358
+ // src/tools/search-recall.ts
16359
+ async function searchRecall(db, input) {
16360
+ const query = input.query.trim();
16361
+ if (!query) {
16362
+ return {
16363
+ query,
16364
+ results: [],
16365
+ totals: { memory: 0, chat: 0 }
16366
+ };
16367
+ }
16368
+ const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
16369
+ const [memory, chat] = await Promise.all([
16370
+ searchObservations(db, input),
16371
+ Promise.resolve(searchChat(db, {
16372
+ query,
16373
+ limit: limit * 2,
16374
+ project_scoped: input.project_scoped,
16375
+ cwd: input.cwd,
16376
+ user_id: input.user_id
16377
+ }))
16378
+ ]);
16379
+ const merged = mergeRecallResults(memory.observations, chat.messages, limit);
16380
+ return {
16381
+ query,
16382
+ project: memory.project ?? chat.project,
16383
+ results: merged,
16384
+ totals: {
16385
+ memory: memory.total,
16386
+ chat: chat.messages.length
16387
+ }
16388
+ };
16389
+ }
16390
+ function mergeRecallResults(memory, chat, limit) {
16391
+ const nowEpoch = Math.floor(Date.now() / 1000);
16392
+ const scored = [];
16393
+ for (let index = 0;index < memory.length; index++) {
16394
+ const item = memory[index];
16395
+ const base = 1 / (60 + index + 1);
16396
+ const score = base + Math.max(0, item.rank) * 0.08;
16397
+ scored.push({
16398
+ kind: "memory",
16399
+ rank: score,
16400
+ created_at: item.created_at,
16401
+ created_at_epoch: Math.floor(new Date(item.created_at).getTime() / 1000) || undefined,
16402
+ project_name: item.project_name,
16403
+ observation_id: item.id,
16404
+ id: item.id,
16405
+ session_id: null,
16406
+ type: item.type,
16407
+ title: item.title,
16408
+ detail: firstNonEmpty(item.narrative, parseFactsPreview(item.facts), item.files_modified ? `Files: ${item.files_modified}` : null, item.type) ?? item.type
16409
+ });
16410
+ }
16411
+ for (let index = 0;index < chat.length; index++) {
16412
+ const item = chat[index];
16413
+ const base = 1 / (60 + index + 1);
16414
+ const ageHours = Math.max(0, (nowEpoch - item.created_at_epoch) / 3600);
16415
+ const immediacyBoost = ageHours < 1 ? 1 : 0;
16416
+ const recencyBoost = ageHours < 24 ? 0.12 : ageHours < 72 ? 0.05 : 0.02;
16417
+ const sourceBoost = item.source_kind === "transcript" ? 0.06 : 0.03;
16418
+ scored.push({
16419
+ kind: "chat",
16420
+ rank: base + immediacyBoost + recencyBoost + sourceBoost,
16421
+ created_at_epoch: item.created_at_epoch,
16422
+ session_id: item.session_id,
16423
+ id: item.id,
16424
+ role: item.role,
16425
+ source_kind: item.source_kind,
16426
+ title: `${item.role} [${item.source_kind}]`,
16427
+ detail: item.content.replace(/\s+/g, " ").trim()
16428
+ });
16429
+ }
16430
+ return scored.sort((a, b) => {
16431
+ if (b.rank !== a.rank)
16432
+ return b.rank - a.rank;
16433
+ return (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0);
16434
+ }).slice(0, limit);
16435
+ }
16436
+ function parseFactsPreview(facts) {
16437
+ if (!facts)
16438
+ return null;
16439
+ try {
16440
+ const parsed = JSON.parse(facts);
16441
+ if (!Array.isArray(parsed) || parsed.length === 0)
16442
+ return null;
16443
+ const lines = parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
16444
+ return lines.length > 0 ? lines.slice(0, 2).join(" | ") : null;
16445
+ } catch {
16446
+ return facts;
16447
+ }
16448
+ }
16449
+ function firstNonEmpty(...values) {
16450
+ for (const value of values) {
16451
+ if (value && value.trim().length > 0)
16452
+ return value.trim();
16453
+ }
16454
+ return null;
16455
+ }
16456
+
16324
16457
  // src/tools/get.ts
16325
16458
  function getObservations(db, input) {
16326
16459
  if (input.ids.length === 0) {
@@ -16461,8 +16594,8 @@ function getRecentChat(db, input) {
16461
16594
  const messages2 = db.getSessionChatMessages(input.session_id, limit).slice(-limit).reverse();
16462
16595
  return {
16463
16596
  messages: messages2,
16464
- session_count: countDistinctSessions(messages2),
16465
- source_summary: summarizeChatSources(messages2),
16597
+ session_count: countDistinctSessions2(messages2),
16598
+ source_summary: summarizeChatSources2(messages2),
16466
16599
  transcript_backed: messages2.some((message) => message.source_kind === "transcript")
16467
16600
  };
16468
16601
  }
@@ -16479,40 +16612,6 @@ function getRecentChat(db, input) {
16479
16612
  }
16480
16613
  }
16481
16614
  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
16615
  return {
16517
16616
  messages,
16518
16617
  project: projectName,
@@ -16552,6 +16651,8 @@ function getSessionStory(db, input) {
16552
16651
  summary,
16553
16652
  prompts,
16554
16653
  chat_messages: chatMessages,
16654
+ chat_source_summary: summarizeChatSources3(chatMessages),
16655
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
16555
16656
  tool_events: toolEvents,
16556
16657
  observations,
16557
16658
  handoffs,
@@ -16649,6 +16750,12 @@ function collectProvenanceSummary(observations) {
16649
16750
  }
16650
16751
  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
16752
  }
16753
+ function summarizeChatSources3(messages) {
16754
+ return messages.reduce((summary, message) => {
16755
+ summary[message.source_kind] += 1;
16756
+ return summary;
16757
+ }, { transcript: 0, hook: 0 });
16758
+ }
16652
16759
 
16653
16760
  // src/tools/handoffs.ts
16654
16761
  async function createHandoff(db, config2, input) {
@@ -17995,16 +18102,17 @@ function getProjectMemoryIndex(db, input) {
17995
18102
  }).handoffs;
17996
18103
  const rollingHandoffDraftsCount = recentHandoffsCount.filter((handoff) => isDraftHandoff(handoff)).length;
17997
18104
  const savedHandoffsCount = recentHandoffsCount.length - rollingHandoffDraftsCount;
17998
- const recentChatCount = getRecentChat(db, {
18105
+ const recentChat = getRecentChat(db, {
17999
18106
  cwd,
18000
18107
  project_scoped: true,
18001
18108
  user_id: input.user_id,
18002
18109
  limit: 20
18003
- }).messages.length;
18110
+ });
18111
+ const recentChatCount = recentChat.messages.length;
18004
18112
  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
18113
  const captureSummary = summarizeCaptureState(recentSessions);
18006
18114
  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);
18115
+ const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.transcript_backed);
18008
18116
  const estimatedReadTokens = estimateTokens([
18009
18117
  recentOutcomes.join(`
18010
18118
  `),
@@ -18029,6 +18137,9 @@ function getProjectMemoryIndex(db, input) {
18029
18137
  rolling_handoff_drafts_count: rollingHandoffDraftsCount,
18030
18138
  saved_handoffs_count: savedHandoffsCount,
18031
18139
  recent_chat_count: recentChatCount,
18140
+ recent_chat_sessions: recentChat.session_count,
18141
+ chat_source_summary: recentChat.source_summary,
18142
+ chat_coverage_state: recentChat.transcript_backed ? "transcript-backed" : recentChatCount > 0 ? "hook-only" : "none",
18032
18143
  raw_capture_active: recentRequestsCount > 0 || recentToolsCount > 0,
18033
18144
  capture_summary: captureSummary,
18034
18145
  hot_files: hotFiles,
@@ -18101,7 +18212,7 @@ function summarizeCaptureState(sessions) {
18101
18212
  }
18102
18213
  return summary;
18103
18214
  }
18104
- function buildSuggestedTools(sessions, requestCount, toolCount, observationCount) {
18215
+ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, transcriptBackedChat) {
18105
18216
  const suggested = [];
18106
18217
  if (sessions.length > 0) {
18107
18218
  suggested.push("recent_sessions");
@@ -18115,7 +18226,12 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
18115
18226
  if (sessions.length > 0) {
18116
18227
  suggested.push("create_handoff", "recent_handoffs");
18117
18228
  }
18118
- suggested.push("recent_chat");
18229
+ if (recentChatCount > 0 && !transcriptBackedChat) {
18230
+ suggested.push("refresh_chat_recall");
18231
+ }
18232
+ if (recentChatCount > 0) {
18233
+ suggested.push("recent_chat", "search_chat");
18234
+ }
18119
18235
  return Array.from(new Set(suggested)).slice(0, 4);
18120
18236
  }
18121
18237
 
@@ -18162,12 +18278,12 @@ function getMemoryConsole(db, input) {
18162
18278
  project_scoped: projectScoped,
18163
18279
  user_id: input.user_id,
18164
18280
  limit: 6
18165
- }).messages;
18281
+ });
18166
18282
  const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
18167
18283
  cwd,
18168
18284
  user_id: input.user_id
18169
18285
  }) : null;
18170
- const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
18286
+ const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.messages.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
18171
18287
  return {
18172
18288
  project: project?.name,
18173
18289
  capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
@@ -18179,7 +18295,10 @@ function getMemoryConsole(db, input) {
18179
18295
  recent_handoffs: recentHandoffs,
18180
18296
  rolling_handoff_drafts: rollingHandoffDrafts,
18181
18297
  saved_handoffs: savedHandoffs,
18182
- recent_chat: recentChat,
18298
+ recent_chat: recentChat.messages,
18299
+ recent_chat_sessions: projectIndex?.recent_chat_sessions ?? recentChat.session_count,
18300
+ chat_source_summary: projectIndex?.chat_source_summary ?? recentChat.source_summary,
18301
+ chat_coverage_state: projectIndex?.chat_coverage_state ?? (recentChat.transcript_backed ? "transcript-backed" : recentChat.messages.length > 0 ? "hook-only" : "none"),
18183
18302
  observations,
18184
18303
  capture_summary: projectIndex?.capture_summary,
18185
18304
  recent_outcomes: projectIndex?.recent_outcomes ?? [],
@@ -18189,10 +18308,10 @@ function getMemoryConsole(db, input) {
18189
18308
  assistant_checkpoint_types: projectIndex?.assistant_checkpoint_types ?? [],
18190
18309
  top_types: projectIndex?.top_types ?? [],
18191
18310
  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)
18311
+ suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.transcript_backed)
18193
18312
  };
18194
18313
  }
18195
- function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount) {
18314
+ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, transcriptBackedChat) {
18196
18315
  const suggested = [];
18197
18316
  if (sessionCount > 0)
18198
18317
  suggested.push("recent_sessions");
@@ -18204,8 +18323,10 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
18204
18323
  suggested.push("create_handoff", "recent_handoffs");
18205
18324
  if (handoffCount > 0)
18206
18325
  suggested.push("load_handoff");
18326
+ if (chatCount > 0 && !transcriptBackedChat)
18327
+ suggested.push("refresh_chat_recall");
18207
18328
  if (chatCount > 0)
18208
- suggested.push("recent_chat");
18329
+ suggested.push("recent_chat", "search_chat");
18209
18330
  return Array.from(new Set(suggested)).slice(0, 4);
18210
18331
  }
18211
18332
 
@@ -18428,7 +18549,7 @@ function toChatEvent(message) {
18428
18549
  created_at_epoch: message.created_at_epoch,
18429
18550
  session_id: message.session_id,
18430
18551
  id: message.id,
18431
- title: message.role === "user" ? "user" : "assistant",
18552
+ title: `${message.role} [${message.source_kind}]`,
18432
18553
  detail: content.slice(0, 220)
18433
18554
  };
18434
18555
  }
@@ -19007,12 +19128,13 @@ function getSessionContext(db, input) {
19007
19128
  const rollingHandoffDrafts = (context.recentHandoffs ?? []).filter((handoff) => handoff.title.startsWith("Handoff Draft:")).length;
19008
19129
  const savedHandoffs = recentHandoffs - rollingHandoffDrafts;
19009
19130
  const latestHandoffTitle = context.recentHandoffs?.[0]?.title ?? null;
19010
- const recentChatMessages = getRecentChat(db, {
19131
+ const recentChat = getRecentChat(db, {
19011
19132
  cwd,
19012
19133
  project_scoped: true,
19013
19134
  user_id: input.user_id,
19014
19135
  limit: 8
19015
- }).messages.length;
19136
+ });
19137
+ const recentChatMessages = recentChat.messages.length;
19016
19138
  const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
19017
19139
  const hotFiles = buildHotFiles(context);
19018
19140
  const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
@@ -19031,12 +19153,15 @@ function getSessionContext(db, input) {
19031
19153
  saved_handoffs: savedHandoffs,
19032
19154
  latest_handoff_title: latestHandoffTitle,
19033
19155
  recent_chat_messages: recentChatMessages,
19156
+ recent_chat_sessions: recentChat.session_count,
19157
+ chat_source_summary: recentChat.source_summary,
19158
+ chat_coverage_state: recentChat.transcript_backed ? "transcript-backed" : recentChatMessages > 0 ? "hook-only" : "none",
19034
19159
  recent_outcomes: context.recentOutcomes ?? [],
19035
19160
  hot_files: hotFiles,
19036
19161
  capture_state: captureState,
19037
19162
  raw_capture_active: recentRequests > 0 || recentTools > 0,
19038
19163
  estimated_read_tokens: estimateTokens(preview),
19039
- suggested_tools: buildSuggestedTools2(context),
19164
+ suggested_tools: buildSuggestedTools2(context, recentChat.transcript_backed),
19040
19165
  preview
19041
19166
  };
19042
19167
  }
@@ -19059,7 +19184,7 @@ function parseJsonArray3(value) {
19059
19184
  return [];
19060
19185
  }
19061
19186
  }
19062
- function buildSuggestedTools2(context) {
19187
+ function buildSuggestedTools2(context, transcriptBackedChat) {
19063
19188
  const tools = [];
19064
19189
  if ((context.recentSessions?.length ?? 0) > 0) {
19065
19190
  tools.push("recent_sessions");
@@ -19076,7 +19201,12 @@ function buildSuggestedTools2(context) {
19076
19201
  if ((context.recentHandoffs?.length ?? 0) > 0) {
19077
19202
  tools.push("load_handoff");
19078
19203
  }
19079
- tools.push("recent_chat", "search_chat", "refresh_chat_recall");
19204
+ if ((context.recentChatMessages?.length ?? 0) > 0 && !transcriptBackedChat) {
19205
+ tools.push("refresh_chat_recall");
19206
+ }
19207
+ if ((context.recentChatMessages?.length ?? 0) > 0) {
19208
+ tools.push("recent_chat", "search_chat");
19209
+ }
19080
19210
  return Array.from(new Set(tools)).slice(0, 4);
19081
19211
  }
19082
19212
 
@@ -21152,7 +21282,7 @@ process.on("SIGTERM", () => {
21152
21282
  });
21153
21283
  var server = new McpServer({
21154
21284
  name: "engrm",
21155
- version: "0.4.26"
21285
+ version: "0.4.27"
21156
21286
  });
21157
21287
  server.tool("save_observation", "Save an observation to memory", {
21158
21288
  type: exports_external.enum([
@@ -21545,6 +21675,58 @@ ${previews.join(`
21545
21675
  ]
21546
21676
  };
21547
21677
  });
21678
+ server.tool("search_recall", "Search live recall across durable memory and chat together. Best for questions like 'what were we just talking about?'", {
21679
+ query: exports_external.string().describe("Recall query"),
21680
+ project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
21681
+ limit: exports_external.number().optional().describe("Max results (default: 10)"),
21682
+ cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
21683
+ user_id: exports_external.string().optional().describe("Optional user override")
21684
+ }, async (params) => {
21685
+ const result = await searchRecall(db, {
21686
+ query: params.query,
21687
+ project_scoped: params.project_scoped,
21688
+ limit: params.limit,
21689
+ cwd: params.cwd,
21690
+ user_id: params.user_id ?? config2.user_id
21691
+ });
21692
+ if (result.results.length === 0) {
21693
+ return {
21694
+ content: [
21695
+ {
21696
+ type: "text",
21697
+ text: result.project ? `No recall found for "${params.query}" in project ${result.project}` : `No recall found for "${params.query}"`
21698
+ }
21699
+ ]
21700
+ };
21701
+ }
21702
+ const projectLine = result.project ? `Project: ${result.project}
21703
+ ` : "";
21704
+ const summaryLine = `Matches: ${result.results.length} · memory ${result.totals.memory} · chat ${result.totals.chat}
21705
+ `;
21706
+ const rows = result.results.map((item) => {
21707
+ const sourceBits = [item.kind];
21708
+ if (item.type)
21709
+ sourceBits.push(item.type);
21710
+ if (item.role)
21711
+ sourceBits.push(item.role);
21712
+ if (item.source_kind)
21713
+ sourceBits.push(item.source_kind);
21714
+ const idBit = item.observation_id ? `#${item.observation_id}` : item.id ? `chat:${item.id}` : "";
21715
+ const title = `${idBit ? `${idBit} ` : ""}${item.title}${item.project_name ? ` (${item.project_name})` : ""}`;
21716
+ return `- [${sourceBits.join(" · ")}] ${title}
21717
+ ${item.detail.slice(0, 220)}`;
21718
+ }).join(`
21719
+ `);
21720
+ return {
21721
+ content: [
21722
+ {
21723
+ type: "text",
21724
+ text: `${projectLine}${summaryLine}Recall search for "${params.query}":
21725
+ ${rows}`
21726
+ }
21727
+ ]
21728
+ };
21729
+ });
21548
21730
  server.tool("get_observations", "Get observations by ID", {
21549
21731
  ids: exports_external.array(exports_external.number()).describe("Observation IDs")
21550
21732
  }, async (params) => {
@@ -21865,6 +22047,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
21865
22047
  {
21866
22048
  type: "text",
21867
22049
  text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
22050
+ ` + `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
22051
  ` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
21869
22052
  ` : ""}` + `Handoffs: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
21870
22053
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
@@ -22071,6 +22254,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
22071
22254
  ` + `Recent handoffs: ${result.recent_handoffs}
22072
22255
  ` + `Handoff split: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
22073
22256
  ` + `Recent chat messages: ${result.recent_chat_messages}
22257
+ ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
22074
22258
  ` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
22075
22259
  ` + `Raw chronology active: ${result.raw_capture_active ? "yes" : "no"}
22076
22260
 
@@ -22150,6 +22334,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
22150
22334
  ` + `Recent handoffs captured: ${result.recent_handoffs_count}
22151
22335
  ` + `Handoff split: ${result.saved_handoffs_count} saved, ${result.rolling_handoff_drafts_count} rolling drafts
22152
22336
  ` + `Recent chat messages captured: ${result.recent_chat_count}
22337
+ ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
22153
22338
 
22154
22339
  ` + `Raw chronology: ${result.raw_capture_active ? "active" : "observations-only so far"}
22155
22340
 
@@ -22554,7 +22739,7 @@ server.tool("session_story", "Show the full local memory story for one session",
22554
22739
  `) : "(none)";
22555
22740
  const promptLines = result.prompts.length > 0 ? result.prompts.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
22556
22741
  `) : "- (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(`
22742
+ 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
22743
  `) : "- (none)";
22559
22744
  const toolLines = result.tool_events.length > 0 ? result.tool_events.slice(-15).map((tool) => {
22560
22745
  const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
@@ -22596,6 +22781,8 @@ ${summaryLines}
22596
22781
  ` + `Prompts:
22597
22782
  ${promptLines}
22598
22783
 
22784
+ ` + `Chat recall: ${result.chat_coverage_state} (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
22785
+
22599
22786
  ` + `Chat:
22600
22787
  ${chatLines}
22601
22788
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.26",
3
+ "version": "0.4.27",
4
4
  "description": "Shared memory across devices, sessions, and coding agents",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",