engrm 0.4.31 → 0.4.32

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
@@ -226,8 +226,10 @@ The MCP server exposes tools that supported agents can call directly:
226
226
  | `recent_handoffs` | List recent saved handoffs for the current project or workspace |
227
227
  | `load_handoff` | Open a saved handoff as a resume point for a new session |
228
228
  | `refresh_chat_recall` | Rehydrate the separate chat lane from a Claude transcript when a long session feels under-captured |
229
- | `repair_recall` | Rehydrate recent project/session recall from transcript or Claude history fallback when chat feels missing |
230
- | `resume_thread` | Build one clear resume point from handoff, current thread, recent chat, and unified recall |
229
+ | `repair_recall` | Use when continuity feels thin; rehydrate recent recall from transcript or Claude history fallback |
230
+ | `list_recall_items` | Use first when continuity feels fuzzy; list the best current handoffs, threads, chat snippets, and memory entries |
231
+ | `load_recall_item` | Use after `list_recall_items`; load one exact recall item key |
232
+ | `resume_thread` | Use first when you want one direct "where were we?" answer from handoff, current thread, recent chat, and unified recall |
231
233
  | `recent_chat` | Inspect the separate synced chat lane without mixing it into durable memory |
232
234
  | `search_chat` | Search recent chat recall with hybrid lexical + semantic matching, separately from reusable memory observations |
233
235
  | `search_recall` | Search durable memory and chat recall together when you do not want to guess the right lane |
@@ -252,6 +254,10 @@ If you are evaluating Engrm as an MCP server, start with this small set first:
252
254
  - verify which tools are actually producing durable memory and which plugins they exercise
253
255
  - `capture_quality`
254
256
  - check whether raw chronology is healthy across the workspace before judging memory quality
257
+ - `list_recall_items`
258
+ - list the best current handoffs, session threads, chat snippets, and memory entries before opening one exact item
259
+ - `load_recall_item`
260
+ - open one exact recall key from the index without falling back to fuzzy retrieval
255
261
  - `resume_thread`
256
262
  - get one direct “where were we?” resume point with freshness, source, tool trail, and next actions
257
263
  - `repair_recall`
@@ -265,6 +271,22 @@ These are the tools we should be comfortable pointing people to publicly first:
265
271
  - easy local inspection after capture
266
272
  - clear continuity recovery when switching devices or resuming long sessions
267
273
 
274
+ ### Recall Protocol
275
+
276
+ When continuity feels fuzzy, the default path is:
277
+
278
+ 1. `resume_thread`
279
+ 2. `list_recall_items`
280
+ 3. `load_recall_item`
281
+ 4. `repair_recall`
282
+
283
+ How to use it:
284
+
285
+ - `resume_thread` is the fastest "get me back into the live thread" action
286
+ - `list_recall_items` is the deterministic directory-first path when you want to inspect candidates before opening one
287
+ - `load_recall_item` opens an exact handoff, thread, chat, or memory key returned by the index
288
+ - `repair_recall` is the repair step when continuity is still thin, hook-only, or under-captured
289
+
268
290
  ### Thin Tools, Thick Memory
269
291
 
270
292
  Engrm now has a real thin-tool layer, not just a plugin spec.
@@ -382,6 +404,8 @@ What each tool is good for:
382
404
  - `capture_quality` shows whether chat recall is transcript-backed, history-backed, or still hook-only across the workspace
383
405
  - `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
384
406
  - `resume_thread` is the fastest “get me back into the live thread” path when you want freshness, source, next actions, tool trail, and chat in one place
407
+ - `list_recall_items` is the deterministic directory-first path when you want to inspect the best candidate handoffs/threads before opening one exact item
408
+ - `load_recall_item` completes that protocol by letting agents open one exact recall key directly after listing
385
409
  - `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed, history-backed, or only hook-captured
386
410
  - `memory_console`, `project_memory_index`, and `session_context` also expose resume-readiness directly, so you can see whether a repo is `live`, `recent`, or `stale` before drilling deeper
387
411
  - when chat continuity is only partial, the workbench and startup hints now prefer `repair_recall`, and still suggest `refresh_chat_recall` when a single session likely just needs transcript hydration
@@ -3673,6 +3673,14 @@ function formatContextForInjection(context) {
3673
3673
  }
3674
3674
  lines.push("");
3675
3675
  }
3676
+ const recallIndexLines = buildRecallIndexLines(context);
3677
+ if (recallIndexLines.length > 0) {
3678
+ lines.push("## Recall Index");
3679
+ for (const line of recallIndexLines) {
3680
+ lines.push(line);
3681
+ }
3682
+ lines.push("");
3683
+ }
3676
3684
  if (context.recentChatMessages && context.recentChatMessages.length > 0) {
3677
3685
  lines.push("## Recent Chat");
3678
3686
  for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
@@ -3775,6 +3783,25 @@ function formatContextForInjection(context) {
3775
3783
  return lines.join(`
3776
3784
  `);
3777
3785
  }
3786
+ function buildRecallIndexLines(context) {
3787
+ const lines = [];
3788
+ for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
3789
+ const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
3790
+ if (!title)
3791
+ continue;
3792
+ lines.push(`- handoff:${handoff.id} — ${truncateText(title, 140)}`);
3793
+ }
3794
+ for (const session of context.recentSessions?.slice(0, 2) ?? []) {
3795
+ const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
3796
+ if (!title)
3797
+ continue;
3798
+ lines.push(`- session:${session.session_id} — ${truncateText(title.replace(/\s+/g, " ").trim(), 140)}`);
3799
+ }
3800
+ for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
3801
+ lines.push(`- chat:${message.id} — ${truncateText(message.content.replace(/\s+/g, " ").trim(), 140)}`);
3802
+ }
3803
+ return Array.from(new Set(lines)).slice(0, 5);
3804
+ }
3778
3805
  function formatSessionBrief(summary) {
3779
3806
  const lines = [];
3780
3807
  const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
@@ -1943,6 +1943,14 @@ function formatContextForInjection(context) {
1943
1943
  }
1944
1944
  lines.push("");
1945
1945
  }
1946
+ const recallIndexLines = buildRecallIndexLines(context);
1947
+ if (recallIndexLines.length > 0) {
1948
+ lines.push("## Recall Index");
1949
+ for (const line of recallIndexLines) {
1950
+ lines.push(line);
1951
+ }
1952
+ lines.push("");
1953
+ }
1946
1954
  if (context.recentChatMessages && context.recentChatMessages.length > 0) {
1947
1955
  lines.push("## Recent Chat");
1948
1956
  for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
@@ -2045,6 +2053,25 @@ function formatContextForInjection(context) {
2045
2053
  return lines.join(`
2046
2054
  `);
2047
2055
  }
2056
+ function buildRecallIndexLines(context) {
2057
+ const lines = [];
2058
+ for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
2059
+ const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
2060
+ if (!title)
2061
+ continue;
2062
+ lines.push(`- handoff:${handoff.id} — ${truncateText(title, 140)}`);
2063
+ }
2064
+ for (const session of context.recentSessions?.slice(0, 2) ?? []) {
2065
+ const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
2066
+ if (!title)
2067
+ continue;
2068
+ lines.push(`- session:${session.session_id} — ${truncateText(title.replace(/\s+/g, " ").trim(), 140)}`);
2069
+ }
2070
+ for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
2071
+ lines.push(`- chat:${message.id} — ${truncateText(message.content.replace(/\s+/g, " ").trim(), 140)}`);
2072
+ }
2073
+ return Array.from(new Set(lines)).slice(0, 5);
2074
+ }
2048
2075
  function formatSessionBrief(summary) {
2049
2076
  const lines = [];
2050
2077
  const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
@@ -2618,6 +2645,14 @@ function formatContextForInjection2(context) {
2618
2645
  }
2619
2646
  lines.push("");
2620
2647
  }
2648
+ const recallIndexLines = buildRecallIndexLines2(context);
2649
+ if (recallIndexLines.length > 0) {
2650
+ lines.push("## Recall Index");
2651
+ for (const line of recallIndexLines) {
2652
+ lines.push(line);
2653
+ }
2654
+ lines.push("");
2655
+ }
2621
2656
  if (context.recentChatMessages && context.recentChatMessages.length > 0) {
2622
2657
  lines.push("## Recent Chat");
2623
2658
  for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
@@ -2720,6 +2755,25 @@ function formatContextForInjection2(context) {
2720
2755
  return lines.join(`
2721
2756
  `);
2722
2757
  }
2758
+ function buildRecallIndexLines2(context) {
2759
+ const lines = [];
2760
+ for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
2761
+ const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
2762
+ if (!title)
2763
+ continue;
2764
+ lines.push(`- handoff:${handoff.id} — ${truncateText2(title, 140)}`);
2765
+ }
2766
+ for (const session of context.recentSessions?.slice(0, 2) ?? []) {
2767
+ const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
2768
+ if (!title)
2769
+ continue;
2770
+ lines.push(`- session:${session.session_id} — ${truncateText2(title.replace(/\s+/g, " ").trim(), 140)}`);
2771
+ }
2772
+ for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
2773
+ lines.push(`- chat:${message.id} — ${truncateText2(message.content.replace(/\s+/g, " ").trim(), 140)}`);
2774
+ }
2775
+ return Array.from(new Set(lines)).slice(0, 5);
2776
+ }
2723
2777
  function formatSessionBrief2(summary) {
2724
2778
  const lines = [];
2725
2779
  const heading = summary.request ? `### ${truncateText2(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
@@ -3090,7 +3144,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3090
3144
  import { join as join3 } from "node:path";
3091
3145
  import { homedir } from "node:os";
3092
3146
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3093
- var CLIENT_VERSION = "0.4.31";
3147
+ var CLIENT_VERSION = "0.4.32";
3094
3148
  function hashFile(filePath) {
3095
3149
  try {
3096
3150
  if (!existsSync3(filePath))
@@ -5794,6 +5848,8 @@ function formatInspectHints(context, visibleObservationIds = []) {
5794
5848
  hints.push("activity_feed");
5795
5849
  }
5796
5850
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
5851
+ hints.push("list_recall_items");
5852
+ hints.push("load_recall_item");
5797
5853
  hints.push("resume_thread");
5798
5854
  hints.push("search_recall");
5799
5855
  }
@@ -3082,7 +3082,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3082
3082
  sentinel_used: valueSignals.security_findings_count > 0,
3083
3083
  risk_score: riskScore,
3084
3084
  stacks_detected: stacks,
3085
- client_version: "0.4.31",
3085
+ client_version: "0.4.32",
3086
3086
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3087
3087
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3088
3088
  recall_attempts: metrics?.recallAttempts ?? 0,
package/dist/server.js CHANGED
@@ -17976,6 +17976,14 @@ function formatContextForInjection(context) {
17976
17976
  }
17977
17977
  lines.push("");
17978
17978
  }
17979
+ const recallIndexLines = buildRecallIndexLines(context);
17980
+ if (recallIndexLines.length > 0) {
17981
+ lines.push("## Recall Index");
17982
+ for (const line of recallIndexLines) {
17983
+ lines.push(line);
17984
+ }
17985
+ lines.push("");
17986
+ }
17979
17987
  if (context.recentChatMessages && context.recentChatMessages.length > 0) {
17980
17988
  lines.push("## Recent Chat");
17981
17989
  for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
@@ -18078,6 +18086,25 @@ function formatContextForInjection(context) {
18078
18086
  return lines.join(`
18079
18087
  `);
18080
18088
  }
18089
+ function buildRecallIndexLines(context) {
18090
+ const lines = [];
18091
+ for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
18092
+ const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
18093
+ if (!title)
18094
+ continue;
18095
+ lines.push(`- handoff:${handoff.id} — ${truncateText(title, 140)}`);
18096
+ }
18097
+ for (const session of context.recentSessions?.slice(0, 2) ?? []) {
18098
+ const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
18099
+ if (!title)
18100
+ continue;
18101
+ lines.push(`- session:${session.session_id} — ${truncateText(title.replace(/\s+/g, " ").trim(), 140)}`);
18102
+ }
18103
+ for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
18104
+ lines.push(`- chat:${message.id} — ${truncateText(message.content.replace(/\s+/g, " ").trim(), 140)}`);
18105
+ }
18106
+ return Array.from(new Set(lines)).slice(0, 5);
18107
+ }
18081
18108
  function formatSessionBrief(summary) {
18082
18109
  const lines = [];
18083
18110
  const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
@@ -18296,6 +18323,187 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
18296
18323
  return picked;
18297
18324
  }
18298
18325
 
18326
+ // src/tools/list-recall-items.ts
18327
+ function listRecallItems(db, input) {
18328
+ const limit = Math.max(3, Math.min(input.limit ?? 12, 30));
18329
+ const projectScoped = input.project_scoped !== false;
18330
+ let projectName;
18331
+ if (projectScoped) {
18332
+ const cwd = input.cwd ?? process.cwd();
18333
+ const detected = detectProject(cwd);
18334
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
18335
+ projectName = project?.name;
18336
+ }
18337
+ const handoffs = getRecentHandoffs(db, {
18338
+ cwd: input.cwd,
18339
+ project_scoped: projectScoped,
18340
+ user_id: input.user_id,
18341
+ current_device_id: input.current_device_id,
18342
+ limit
18343
+ }).handoffs;
18344
+ const sessions = getRecentSessions(db, {
18345
+ cwd: input.cwd,
18346
+ project_scoped: projectScoped,
18347
+ user_id: input.user_id,
18348
+ limit: Math.min(limit, 8)
18349
+ }).sessions;
18350
+ const chat = getRecentChat(db, {
18351
+ cwd: input.cwd,
18352
+ project_scoped: projectScoped,
18353
+ user_id: input.user_id,
18354
+ limit: Math.min(limit * 2, 20)
18355
+ }).messages;
18356
+ const observations = getRecentActivity(db, {
18357
+ cwd: input.cwd,
18358
+ project_scoped: projectScoped,
18359
+ user_id: input.user_id,
18360
+ limit: Math.min(limit * 2, 24)
18361
+ }).observations;
18362
+ const items = [
18363
+ ...handoffs.map((handoff) => ({
18364
+ key: `handoff:${handoff.id}`,
18365
+ kind: "handoff",
18366
+ title: stripHandoffPrefix(handoff.title),
18367
+ detail: summarizeHandoffDetail(handoff.narrative),
18368
+ created_at_epoch: handoff.created_at_epoch,
18369
+ freshness: classifyFreshness(handoff.created_at_epoch),
18370
+ session_id: handoff.session_id,
18371
+ source_device_id: handoff.device_id ?? null
18372
+ })),
18373
+ ...sessions.filter((session) => Boolean(session.request || session.completed || session.current_thread)).map((session) => ({
18374
+ key: `session:${session.session_id}`,
18375
+ kind: "thread",
18376
+ title: session.current_thread ?? session.request ?? session.completed ?? session.session_id,
18377
+ detail: buildSessionDetail(session),
18378
+ created_at_epoch: session.completed_at_epoch ?? session.started_at_epoch ?? 0,
18379
+ freshness: classifyFreshness(session.completed_at_epoch ?? session.started_at_epoch ?? null),
18380
+ session_id: session.session_id,
18381
+ source_device_id: session.device_id ?? null
18382
+ })),
18383
+ ...dedupeChatIndex(chat).map((message) => {
18384
+ const origin = getChatCaptureOrigin(message);
18385
+ return {
18386
+ key: `chat:${message.id}`,
18387
+ kind: "chat",
18388
+ title: `${message.role} [${origin}]`,
18389
+ detail: truncateInline(message.content.replace(/\s+/g, " ").trim(), 180),
18390
+ created_at_epoch: message.created_at_epoch,
18391
+ freshness: classifyFreshness(message.created_at_epoch),
18392
+ session_id: message.session_id,
18393
+ source_device_id: message.device_id ?? null
18394
+ };
18395
+ }),
18396
+ ...observations.filter((obs) => obs.type !== "message").filter((obs) => !looksLikeFileOperationTitle3(obs.title)).map((obs) => ({
18397
+ key: `obs:${obs.id}`,
18398
+ kind: "memory",
18399
+ title: `[${obs.type}] ${obs.title}`,
18400
+ detail: truncateInline(firstNonEmpty2(obs.narrative, previewFacts(obs.facts), obs.project_name ?? "") ?? obs.type, 180),
18401
+ created_at_epoch: obs.created_at_epoch,
18402
+ freshness: classifyFreshness(obs.created_at_epoch),
18403
+ session_id: obs.session_id ?? null,
18404
+ source_device_id: obs.device_id ?? null
18405
+ }))
18406
+ ];
18407
+ const deduped = dedupeRecallItems(items).sort((a, b) => compareRecallItems(a, b, input.current_device_id)).slice(0, limit);
18408
+ return {
18409
+ project: projectName,
18410
+ continuity_mode: deduped.some((item) => item.kind === "handoff" || item.kind === "thread") ? "direct" : "indexed",
18411
+ items: deduped
18412
+ };
18413
+ }
18414
+ function compareRecallItems(a, b, currentDeviceId) {
18415
+ const priority = (item) => {
18416
+ const freshness = item.freshness === "live" ? 0 : item.freshness === "recent" ? 1 : 2;
18417
+ const kind = item.kind === "handoff" ? 0 : item.kind === "thread" ? 1 : item.kind === "chat" ? 2 : 3;
18418
+ const remoteBoost = currentDeviceId && item.source_device_id && item.source_device_id !== currentDeviceId ? -0.5 : 0;
18419
+ const draftPenalty = item.kind === "handoff" && /draft/i.test(item.title) ? 0.25 : 0;
18420
+ return freshness * 10 + kind + remoteBoost + draftPenalty;
18421
+ };
18422
+ const priorityDiff = priority(a) - priority(b);
18423
+ if (priorityDiff !== 0)
18424
+ return priorityDiff;
18425
+ return b.created_at_epoch - a.created_at_epoch;
18426
+ }
18427
+ function dedupeRecallItems(items) {
18428
+ const best = new Map;
18429
+ for (const item of items) {
18430
+ const key = `${item.kind}::${normalize(item.title)}::${normalize(item.detail)}`;
18431
+ const existing = best.get(key);
18432
+ if (!existing || compareRecallItems(item, existing) < 0) {
18433
+ best.set(key, item);
18434
+ }
18435
+ }
18436
+ return Array.from(best.values());
18437
+ }
18438
+ function dedupeChatIndex(messages) {
18439
+ const byKey = new Map;
18440
+ for (const message of messages) {
18441
+ const key = `${message.session_id}::${message.role}::${normalize(message.content)}`;
18442
+ const existing = byKey.get(key);
18443
+ if (!existing || message.created_at_epoch > existing.created_at_epoch) {
18444
+ byKey.set(key, message);
18445
+ }
18446
+ }
18447
+ return Array.from(byKey.values());
18448
+ }
18449
+ function summarizeHandoffDetail(narrative) {
18450
+ if (!narrative)
18451
+ return "";
18452
+ const line = narrative.split(/\n+/).map((item) => item.trim()).find((item) => /^Current thread:|^Completed:|^Next Steps:/i.test(item));
18453
+ return line ? truncateInline(line.replace(/^(Current thread:|Completed:|Next Steps:)\s*/i, ""), 180) : truncateInline(narrative.replace(/\s+/g, " ").trim(), 180);
18454
+ }
18455
+ function buildSessionDetail(session) {
18456
+ const pieces = [
18457
+ session.request,
18458
+ session.completed,
18459
+ session.current_thread
18460
+ ].filter((item) => Boolean(item && item.trim())).map((item) => item.replace(/\s+/g, " ").trim());
18461
+ return truncateInline(pieces[0] ?? `prompts ${session.prompt_count}, tools ${session.tool_event_count}`, 180);
18462
+ }
18463
+ function stripHandoffPrefix(value) {
18464
+ return value.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
18465
+ }
18466
+ function classifyFreshness(createdAtEpoch) {
18467
+ if (!createdAtEpoch)
18468
+ return "stale";
18469
+ const ageMs = Date.now() - createdAtEpoch * 1000;
18470
+ if (ageMs <= 15 * 60 * 1000)
18471
+ return "live";
18472
+ if (ageMs <= 3 * 24 * 60 * 60 * 1000)
18473
+ return "recent";
18474
+ return "stale";
18475
+ }
18476
+ function normalize(value) {
18477
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
18478
+ }
18479
+ function truncateInline(text, maxLen) {
18480
+ if (text.length <= maxLen)
18481
+ return text;
18482
+ return `${text.slice(0, maxLen - 1).trimEnd()}…`;
18483
+ }
18484
+ function firstNonEmpty2(...values) {
18485
+ for (const value of values) {
18486
+ if (value && value.trim())
18487
+ return value.trim();
18488
+ }
18489
+ return null;
18490
+ }
18491
+ function previewFacts(facts) {
18492
+ if (!facts)
18493
+ return null;
18494
+ try {
18495
+ const parsed = JSON.parse(facts);
18496
+ if (!Array.isArray(parsed) || parsed.length === 0)
18497
+ return null;
18498
+ return parsed.filter((item) => typeof item === "string" && item.trim().length > 0).slice(0, 2).join(" | ");
18499
+ } catch {
18500
+ return facts;
18501
+ }
18502
+ }
18503
+ function looksLikeFileOperationTitle3(value) {
18504
+ return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
18505
+ }
18506
+
18299
18507
  // src/tools/project-memory-index.ts
18300
18508
  function getProjectMemoryIndex(db, input) {
18301
18509
  const cwd = input.cwd ?? process.cwd();
@@ -18371,9 +18579,15 @@ function getProjectMemoryIndex(db, input) {
18371
18579
  limit: 20
18372
18580
  });
18373
18581
  const recentChatCount = recentChat.messages.length;
18582
+ const recallIndex = listRecallItems(db, {
18583
+ cwd,
18584
+ project_scoped: true,
18585
+ user_id: input.user_id,
18586
+ limit: 10
18587
+ });
18374
18588
  const latestSession = recentSessions[0] ?? null;
18375
18589
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
18376
- 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);
18590
+ const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !looksLikeFileOperationTitle4(title)).slice(0, 8);
18377
18591
  const captureSummary = summarizeCaptureState(recentSessions);
18378
18592
  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);
18379
18593
  const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.coverage_state);
@@ -18393,6 +18607,14 @@ function getProjectMemoryIndex(db, input) {
18393
18607
  canonical_id: project.canonical_id,
18394
18608
  continuity_state: continuityState,
18395
18609
  continuity_summary: describeContinuityState(continuityState),
18610
+ recall_mode: recallIndex.continuity_mode,
18611
+ recall_items_ready: recallIndex.items.length,
18612
+ recall_index_preview: recallIndex.items.slice(0, 3).map((item) => ({
18613
+ key: item.key,
18614
+ kind: item.kind,
18615
+ freshness: item.freshness,
18616
+ title: item.title
18617
+ })),
18396
18618
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18397
18619
  resume_source_session_id: latestSession?.session_id ?? null,
18398
18620
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18475,7 +18697,7 @@ function extractPaths(value) {
18475
18697
  return [];
18476
18698
  }
18477
18699
  }
18478
- function looksLikeFileOperationTitle3(value) {
18700
+ function looksLikeFileOperationTitle4(value) {
18479
18701
  return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
18480
18702
  }
18481
18703
  function summarizeCaptureState(sessions) {
@@ -18512,6 +18734,8 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
18512
18734
  suggested.push("activity_feed");
18513
18735
  }
18514
18736
  if (requestCount > 0 || recentChatCount > 0 || observationCount > 0) {
18737
+ suggested.push("list_recall_items");
18738
+ suggested.push("load_recall_item");
18515
18739
  suggested.push("resume_thread");
18516
18740
  suggested.push("search_recall");
18517
18741
  }
@@ -18577,6 +18801,12 @@ function getMemoryConsole(db, input) {
18577
18801
  user_id: input.user_id,
18578
18802
  limit: 6
18579
18803
  });
18804
+ const recallIndex = listRecallItems(db, {
18805
+ cwd,
18806
+ project_scoped: projectScoped,
18807
+ user_id: input.user_id,
18808
+ limit: 10
18809
+ });
18580
18810
  const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
18581
18811
  cwd,
18582
18812
  user_id: input.user_id
@@ -18587,6 +18817,14 @@ function getMemoryConsole(db, input) {
18587
18817
  capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
18588
18818
  continuity_state: continuityState,
18589
18819
  continuity_summary: projectIndex?.continuity_summary ?? describeContinuityState(continuityState),
18820
+ recall_mode: projectIndex?.recall_mode ?? recallIndex.continuity_mode,
18821
+ recall_items_ready: projectIndex?.recall_items_ready ?? recallIndex.items.length,
18822
+ recall_index_preview: projectIndex?.recall_index_preview ?? recallIndex.items.slice(0, 3).map((item) => ({
18823
+ key: item.key,
18824
+ kind: item.kind,
18825
+ freshness: item.freshness,
18826
+ title: item.title
18827
+ })),
18590
18828
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18591
18829
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18592
18830
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -18620,7 +18858,9 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
18620
18858
  if (requestCount > 0 || toolCount > 0)
18621
18859
  suggested.push("activity_feed");
18622
18860
  if (requestCount > 0 || chatCount > 0 || observationCount > 0)
18623
- suggested.push("resume_thread", "search_recall");
18861
+ suggested.push("load_recall_item", "resume_thread", "search_recall");
18862
+ if (requestCount > 0 || chatCount > 0 || observationCount > 0)
18863
+ suggested.unshift("list_recall_items");
18624
18864
  if ((sessionCount > 0 || chatCount > 0) && chatCoverageState !== "transcript-backed")
18625
18865
  suggested.push("repair_recall");
18626
18866
  if (observationCount > 0)
@@ -19467,6 +19707,13 @@ function getSessionContext(db, input) {
19467
19707
  limit: 8
19468
19708
  });
19469
19709
  const recentChatMessages = recentChat.messages.length;
19710
+ const recallIndex = listRecallItems(db, {
19711
+ cwd,
19712
+ project_scoped: true,
19713
+ user_id: input.user_id,
19714
+ current_device_id: input.current_device_id,
19715
+ limit: 10
19716
+ });
19470
19717
  const latestSession = context.recentSessions?.[0] ?? null;
19471
19718
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
19472
19719
  const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
@@ -19479,6 +19726,14 @@ function getSessionContext(db, input) {
19479
19726
  canonical_id: context.canonical_id,
19480
19727
  continuity_state: continuityState,
19481
19728
  continuity_summary: describeContinuityState(continuityState),
19729
+ recall_mode: recallIndex.continuity_mode,
19730
+ recall_items_ready: recallIndex.items.length,
19731
+ recall_index_preview: recallIndex.items.slice(0, 3).map((item) => ({
19732
+ key: item.key,
19733
+ kind: item.kind,
19734
+ freshness: item.freshness,
19735
+ title: item.title
19736
+ })),
19482
19737
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
19483
19738
  resume_source_session_id: latestSession?.session_id ?? null,
19484
19739
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -19541,6 +19796,8 @@ function buildSuggestedTools2(context, chatCoverageState) {
19541
19796
  tools.push("activity_feed");
19542
19797
  }
19543
19798
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
19799
+ tools.push("list_recall_items");
19800
+ tools.push("load_recall_item");
19544
19801
  tools.push("resume_thread");
19545
19802
  tools.push("search_recall");
19546
19803
  }
@@ -19565,6 +19822,139 @@ function buildSuggestedTools2(context, chatCoverageState) {
19565
19822
  return Array.from(new Set(tools)).slice(0, 5);
19566
19823
  }
19567
19824
 
19825
+ // src/tools/load-recall-item.ts
19826
+ function loadRecallItem(db, input) {
19827
+ const [kind, rawId] = input.key.split(":", 2);
19828
+ if (!kind || !rawId) {
19829
+ return {
19830
+ kind: "unknown",
19831
+ key: input.key,
19832
+ title: null,
19833
+ detail: "Malformed recall key",
19834
+ session_id: null,
19835
+ source_device_id: null,
19836
+ payload: null
19837
+ };
19838
+ }
19839
+ if (kind === "handoff") {
19840
+ const id = Number.parseInt(rawId, 10);
19841
+ const result = loadHandoff(db, {
19842
+ id,
19843
+ cwd: input.cwd,
19844
+ user_id: input.user_id,
19845
+ current_device_id: input.current_device_id
19846
+ });
19847
+ if (!result.handoff) {
19848
+ return missing(input.key, "handoff");
19849
+ }
19850
+ return {
19851
+ kind: "handoff",
19852
+ key: input.key,
19853
+ title: result.handoff.title,
19854
+ detail: summarizeNarrative(result.handoff.narrative),
19855
+ session_id: result.handoff.session_id ?? null,
19856
+ source_device_id: result.handoff.device_id ?? null,
19857
+ payload: {
19858
+ type: "handoff",
19859
+ handoff_id: result.handoff.id,
19860
+ narrative: result.handoff.narrative ?? null
19861
+ }
19862
+ };
19863
+ }
19864
+ if (kind === "session") {
19865
+ const story = getSessionStory(db, { session_id: rawId });
19866
+ if (!story.session) {
19867
+ return missing(input.key, "thread");
19868
+ }
19869
+ return {
19870
+ kind: "thread",
19871
+ key: input.key,
19872
+ title: story.summary?.current_thread ?? story.latest_request ?? story.summary?.completed ?? story.session.session_id,
19873
+ detail: story.summary?.next_steps ?? story.summary?.completed ?? null,
19874
+ session_id: story.session.session_id,
19875
+ source_device_id: story.session.device_id ?? null,
19876
+ payload: {
19877
+ type: "thread",
19878
+ latest_request: story.latest_request,
19879
+ current_thread: story.summary?.current_thread ?? null,
19880
+ recent_outcomes: story.recent_outcomes,
19881
+ hot_files: story.hot_files
19882
+ }
19883
+ };
19884
+ }
19885
+ if (kind === "chat") {
19886
+ const id = Number.parseInt(rawId, 10);
19887
+ const messages = getRecentChat(db, {
19888
+ cwd: input.cwd,
19889
+ project_scoped: false,
19890
+ user_id: input.user_id,
19891
+ limit: 200
19892
+ }).messages;
19893
+ const message = messages.find((item) => item.id === id);
19894
+ if (!message) {
19895
+ return missing(input.key, "chat");
19896
+ }
19897
+ const source = message.source_kind === "transcript" ? "transcript" : message.remote_source_id?.startsWith("history:") ? "history" : "hook";
19898
+ return {
19899
+ kind: "chat",
19900
+ key: input.key,
19901
+ title: `${message.role} [${source}]`,
19902
+ detail: message.content,
19903
+ session_id: message.session_id,
19904
+ source_device_id: message.device_id ?? null,
19905
+ payload: {
19906
+ type: "chat",
19907
+ role: message.role,
19908
+ content: message.content,
19909
+ source
19910
+ }
19911
+ };
19912
+ }
19913
+ if (kind === "obs") {
19914
+ const id = Number.parseInt(rawId, 10);
19915
+ const result = getObservations(db, {
19916
+ ids: [id],
19917
+ user_id: input.user_id
19918
+ });
19919
+ const obs = result.observations[0];
19920
+ if (!obs) {
19921
+ return missing(input.key, "memory");
19922
+ }
19923
+ return {
19924
+ kind: "memory",
19925
+ key: input.key,
19926
+ title: obs.title,
19927
+ detail: obs.narrative ?? obs.facts ?? null,
19928
+ session_id: obs.session_id ?? null,
19929
+ source_device_id: obs.device_id ?? null,
19930
+ payload: {
19931
+ type: "memory",
19932
+ observation_id: obs.id,
19933
+ observation_type: obs.type,
19934
+ narrative: obs.narrative ?? null,
19935
+ facts: obs.facts ?? null
19936
+ }
19937
+ };
19938
+ }
19939
+ return missing(input.key, "unknown");
19940
+ }
19941
+ function summarizeNarrative(value) {
19942
+ if (!value)
19943
+ return null;
19944
+ return value.split(/\n+/).map((line) => line.trim()).find(Boolean) ?? null;
19945
+ }
19946
+ function missing(key, kind) {
19947
+ return {
19948
+ kind,
19949
+ key,
19950
+ title: null,
19951
+ detail: "Recall item not found",
19952
+ session_id: null,
19953
+ source_device_id: null,
19954
+ payload: null
19955
+ };
19956
+ }
19957
+
19568
19958
  // src/tools/capture-git-worktree.ts
19569
19959
  import { execSync as execSync2 } from "node:child_process";
19570
19960
  import { existsSync as existsSync4 } from "node:fs";
@@ -21613,7 +22003,7 @@ var REFACTOR_HINTS = [
21613
22003
  "extract",
21614
22004
  "move"
21615
22005
  ];
21616
- function normalize(text) {
22006
+ function normalize2(text) {
21617
22007
  return text.toLowerCase().replace(/\s+/g, " ").trim();
21618
22008
  }
21619
22009
  function uniq(items) {
@@ -21642,7 +22032,7 @@ function countLinesByPrefix(diff, prefix) {
21642
22032
  `).filter((line) => line.startsWith(prefix) && !line.startsWith(`${prefix}${prefix}${prefix}`)).length;
21643
22033
  }
21644
22034
  function detectType(summary, diff, files) {
21645
- const corpus = normalize([summary ?? "", diff, files.join(" ")].join(" "));
22035
+ const corpus = normalize2([summary ?? "", diff, files.join(" ")].join(" "));
21646
22036
  if (BUGFIX_HINTS.some((hint) => corpus.includes(hint)))
21647
22037
  return "bugfix";
21648
22038
  if (DECISION_HINTS.some((hint) => corpus.includes(hint)))
@@ -21702,7 +22092,7 @@ function buildFacts(diff, files) {
21702
22092
  if (added > 0 || removed > 0) {
21703
22093
  facts.push(`Diff footprint: +${added} / -${removed}`);
21704
22094
  }
21705
- const lower = normalize(diff);
22095
+ const lower = normalize2(diff);
21706
22096
  if (lower.includes("auth") || lower.includes("token") || lower.includes("oauth")) {
21707
22097
  facts.push("Touches authentication or credential flow");
21708
22098
  }
@@ -22039,7 +22429,7 @@ process.on("SIGTERM", () => {
22039
22429
  });
22040
22430
  var server = new McpServer({
22041
22431
  name: "engrm",
22042
- version: "0.4.31"
22432
+ version: "0.4.32"
22043
22433
  });
22044
22434
  server.tool("save_observation", "Save an observation to memory", {
22045
22435
  type: exports_external.enum([
@@ -22484,7 +22874,138 @@ ${rows}`
22484
22874
  ]
22485
22875
  };
22486
22876
  });
22487
- server.tool("resume_thread", "Build a clear resume point for the current project by combining handoff, live recall, current thread, and recent chat continuity.", {
22877
+ server.tool("list_recall_items", "USE FIRST when continuity feels fuzzy. List the best current handoffs, session threads, chat snippets, and memory entries before opening one exact item.", {
22878
+ cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
22879
+ project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
22880
+ user_id: exports_external.string().optional().describe("Optional user override"),
22881
+ limit: exports_external.number().optional().describe("Max recall items to list")
22882
+ }, async (params) => {
22883
+ const result = listRecallItems(db, {
22884
+ cwd: params.cwd ?? process.cwd(),
22885
+ project_scoped: params.project_scoped,
22886
+ user_id: params.user_id ?? config2.user_id,
22887
+ current_device_id: config2.device_id,
22888
+ limit: params.limit
22889
+ });
22890
+ if (result.items.length === 0) {
22891
+ return {
22892
+ content: [
22893
+ {
22894
+ type: "text",
22895
+ text: result.project ? `No recall items found yet for project ${result.project}` : "No recall items found yet."
22896
+ }
22897
+ ]
22898
+ };
22899
+ }
22900
+ const projectLine = result.project ? `Project: ${result.project}
22901
+ ` : "";
22902
+ const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
22903
+ ${item.detail}`).join(`
22904
+ `);
22905
+ return {
22906
+ content: [
22907
+ {
22908
+ type: "text",
22909
+ text: `${projectLine}Recall index (${result.continuity_mode} mode):
22910
+ ` + `${rows}
22911
+
22912
+ ` + `Suggested next step: use load_handoff for handoff:* items, get_observations([...]) for obs:* items, or resume_thread when you want one merged resume point.`
22913
+ }
22914
+ ]
22915
+ };
22916
+ });
22917
+ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact recall item key so you can inspect a specific handoff, thread, chat message, or memory entry without fuzzy recall guessing.", {
22918
+ key: exports_external.string().describe("Exact recall key from list_recall_items, such as handoff:12, session:sess-1, chat:55, or obs:402"),
22919
+ cwd: exports_external.string().optional().describe("Optional cwd override"),
22920
+ user_id: exports_external.string().optional().describe("Optional user override")
22921
+ }, async (params) => {
22922
+ const result = loadRecallItem(db, {
22923
+ key: params.key,
22924
+ cwd: params.cwd ?? process.cwd(),
22925
+ user_id: params.user_id ?? config2.user_id,
22926
+ current_device_id: config2.device_id
22927
+ });
22928
+ if (!result.payload) {
22929
+ return {
22930
+ content: [
22931
+ {
22932
+ type: "text",
22933
+ text: `No recall item found for ${params.key}`
22934
+ }
22935
+ ]
22936
+ };
22937
+ }
22938
+ if (result.payload.type === "handoff") {
22939
+ return {
22940
+ content: [
22941
+ {
22942
+ type: "text",
22943
+ text: `Recall item ${result.key} [handoff]
22944
+ ` + `Title: ${result.title}
22945
+ ` + `Session: ${result.session_id ?? "(unknown)"}
22946
+ ` + `Source: ${result.source_device_id ?? "(unknown)"}
22947
+
22948
+ ` + `${result.payload.narrative ?? "(no narrative)"}`
22949
+ }
22950
+ ]
22951
+ };
22952
+ }
22953
+ if (result.payload.type === "thread") {
22954
+ const outcomes = result.payload.recent_outcomes.length > 0 ? result.payload.recent_outcomes.map((item) => `- ${item}`).join(`
22955
+ `) : "- (none)";
22956
+ const hotFiles = result.payload.hot_files.length > 0 ? result.payload.hot_files.map((item) => `- ${item.path}${item.count > 1 ? ` (${item.count})` : ""}`).join(`
22957
+ `) : "- (none)";
22958
+ return {
22959
+ content: [
22960
+ {
22961
+ type: "text",
22962
+ text: `Recall item ${result.key} [thread]
22963
+ ` + `Title: ${result.title}
22964
+ ` + `Session: ${result.session_id ?? "(unknown)"}
22965
+ ` + `Source: ${result.source_device_id ?? "(unknown)"}
22966
+ ` + `Latest request: ${result.payload.latest_request ?? "(none)"}
22967
+ ` + `Current thread: ${result.payload.current_thread ?? "(none)"}
22968
+
22969
+ ` + `Recent outcomes:
22970
+ ${outcomes}
22971
+
22972
+ ` + `Hot files:
22973
+ ${hotFiles}`
22974
+ }
22975
+ ]
22976
+ };
22977
+ }
22978
+ if (result.payload.type === "chat") {
22979
+ return {
22980
+ content: [
22981
+ {
22982
+ type: "text",
22983
+ text: `Recall item ${result.key} [chat]
22984
+ ` + `Title: ${result.title}
22985
+ ` + `Session: ${result.session_id ?? "(unknown)"}
22986
+ ` + `Source: ${result.source_device_id ?? "(unknown)"}
22987
+
22988
+ ` + `${result.payload.content}`
22989
+ }
22990
+ ]
22991
+ };
22992
+ }
22993
+ return {
22994
+ content: [
22995
+ {
22996
+ type: "text",
22997
+ text: `Recall item ${result.key} [memory]
22998
+ ` + `Title: ${result.title}
22999
+ ` + `Session: ${result.session_id ?? "(unknown)"}
23000
+ ` + `Source: ${result.source_device_id ?? "(unknown)"}
23001
+ ` + `Type: ${result.payload.observation_type}
23002
+
23003
+ ` + `${result.payload.narrative ?? result.payload.facts ?? "(no detail)"}`
23004
+ }
23005
+ ]
23006
+ };
23007
+ });
23008
+ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?' answer. Build a clear resume point for the current project by combining handoff, live recall, current thread, and recent chat continuity.", {
22488
23009
  cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
22489
23010
  limit: exports_external.number().optional().describe("Max recall hits/chat snippets to include"),
22490
23011
  user_id: exports_external.string().optional().describe("Optional user override"),
@@ -22871,6 +23392,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
22871
23392
  const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
22872
23393
  `) : "- (none)";
22873
23394
  const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
23395
+ `) : "- (none)";
23396
+ const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
22874
23397
  `) : "- (none)";
22875
23398
  const projectLine = result.project ? `Project: ${result.project}
22876
23399
 
@@ -22885,6 +23408,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
22885
23408
  {
22886
23409
  type: "text",
22887
23410
  text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23411
+ ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
22888
23412
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
22889
23413
  ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat.length} messages across ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, history ${result.chat_source_summary.history}, hook ${result.chat_source_summary.hook})
22890
23414
  ` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
@@ -22892,6 +23416,9 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
22892
23416
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
22893
23417
  ` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
22894
23418
 
23419
+ ` + `Recall preview:
23420
+ ${recallPreviewLines}
23421
+
22895
23422
  ` + `Next actions:
22896
23423
  ${result.resume_next_actions.length > 0 ? result.resume_next_actions.map((item) => `- ${item}`).join(`
22897
23424
  `) : "- (none)"}
@@ -23089,6 +23616,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23089
23616
  text: `Project: ${result.project_name}
23090
23617
  ` + `Canonical ID: ${result.canonical_id}
23091
23618
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23619
+ ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23092
23620
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23093
23621
  ` + `Loaded observations: ${result.session_count}
23094
23622
  ` + `Searchable total: ${result.total_active}
@@ -23101,6 +23629,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23101
23629
  ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, history ${result.chat_source_summary.history}, hook ${result.chat_source_summary.hook})
23102
23630
  ` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
23103
23631
  ` + `Next actions: ${result.resume_next_actions.length > 0 ? result.resume_next_actions.join(" | ") : "(none)"}
23632
+ ` + `Recall preview: ${result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => item.key).join(", ") : "(none)"}
23104
23633
  ` + `Raw chronology active: ${result.raw_capture_active ? "yes" : "no"}
23105
23634
 
23106
23635
  ` + result.preview
@@ -23165,6 +23694,8 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23165
23694
  const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
23166
23695
  `) : "- (none)";
23167
23696
  const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
23697
+ `) : "- (none)";
23698
+ const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
23168
23699
  `) : "- (none)";
23169
23700
  return {
23170
23701
  content: [
@@ -23173,6 +23704,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23173
23704
  text: `Project: ${result.project}
23174
23705
  ` + `Canonical ID: ${result.canonical_id}
23175
23706
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23707
+ ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23176
23708
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23177
23709
  ` + `Recent requests captured: ${result.recent_requests_count}
23178
23710
  ` + `Recent tools captured: ${result.recent_tools_count}
@@ -23188,6 +23720,9 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23188
23720
  ` + `Estimated read cost: ~${result.estimated_read_tokens}t
23189
23721
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23190
23722
 
23723
+ ` + `Recall preview:
23724
+ ${recallPreviewLines}
23725
+
23191
23726
  ` + `Next actions:
23192
23727
  ${result.resume_next_actions.length > 0 ? result.resume_next_actions.map((item) => `- ${item}`).join(`
23193
23728
  `) : "- (none)"}
@@ -23368,7 +23903,7 @@ Transcript messages seen: ${result.total}`
23368
23903
  ]
23369
23904
  };
23370
23905
  });
23371
- server.tool("repair_recall", "Rehydrate recent session recall for the current project from Claude transcripts or history fallback when chat feels missing or under-captured", {
23906
+ server.tool("repair_recall", "USE WHEN recall feels thin or under-captured. Rehydrate recent session recall for the current project from Claude transcripts or history fallback before resuming.", {
23372
23907
  session_id: exports_external.string().optional().describe("Optional single session ID to repair instead of scanning recent project sessions"),
23373
23908
  cwd: exports_external.string().optional().describe("Project directory used to resolve project sessions and Claude history/transcript files"),
23374
23909
  limit: exports_external.number().optional().describe("How many recent sessions to inspect when repairing a project"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "description": "Shared memory across devices, sessions, and agents, with thin MCP tools for durable capture and live continuity",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",