engrm 0.4.32 → 0.4.33

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
@@ -403,9 +403,10 @@ What each tool is good for:
403
403
  - `capture_status` tells you whether prompt/tool hooks are live on this machine
404
404
  - `capture_quality` shows whether chat recall is transcript-backed, history-backed, or still hook-only across the workspace
405
405
  - `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
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
406
+ - `resume_thread` is the fastest “get me back into the live thread” path when you want freshness, source, next actions, tool trail, chat, and one exact `load_recall_item(...)` suggestion in one place
407
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
408
  - `load_recall_item` completes that protocol by letting agents open one exact recall key directly after listing
409
+ - `memory_console`, `project_memory_index`, and `session_context` now also surface one best exact `load_recall_item(...)` jump, so the workbench can hand you the right deterministic next step instead of only showing recall counts
409
410
  - `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed, history-backed, or only hook-captured
410
411
  - `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
411
412
  - 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
@@ -3144,7 +3144,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3144
3144
  import { join as join3 } from "node:path";
3145
3145
  import { homedir } from "node:os";
3146
3146
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3147
- var CLIENT_VERSION = "0.4.32";
3147
+ var CLIENT_VERSION = "0.4.33";
3148
3148
  function hashFile(filePath) {
3149
3149
  try {
3150
3150
  if (!existsSync3(filePath))
@@ -5617,7 +5617,15 @@ function formatSplashScreen(data) {
5617
5617
  lines.push(` ${line}`);
5618
5618
  }
5619
5619
  }
5620
- const inspectHints = formatInspectHints(data.context, contextIndex.observationIds);
5620
+ const recallItems = buildStartupRecallItems(data.context);
5621
+ const recallPreview = formatStartupRecallPreview(recallItems);
5622
+ if (recallPreview.length > 0) {
5623
+ lines.push("");
5624
+ for (const line of recallPreview) {
5625
+ lines.push(` ${line}`);
5626
+ }
5627
+ }
5628
+ const inspectHints = formatInspectHints(data.context, contextIndex.observationIds, recallItems);
5621
5629
  if (inspectHints.length > 0) {
5622
5630
  lines.push("");
5623
5631
  for (const line of inspectHints) {
@@ -5836,7 +5844,7 @@ function formatContextIndex(context, shownItems) {
5836
5844
  observationIds: selected.map((obs) => obs.id)
5837
5845
  };
5838
5846
  }
5839
- function formatInspectHints(context, visibleObservationIds = []) {
5847
+ function formatInspectHints(context, visibleObservationIds = [], recallItems = []) {
5840
5848
  const hints = [];
5841
5849
  const continuityState = getStartupContinuityState(context);
5842
5850
  if ((context.recentSessions?.length ?? 0) > 0) {
@@ -5875,12 +5883,90 @@ function formatInspectHints(context, visibleObservationIds = []) {
5875
5883
  if (unique.length === 0)
5876
5884
  return [];
5877
5885
  const ids = visibleObservationIds.slice(0, 5);
5886
+ const openNowItem = recallItems.find((item) => item.kind !== "memory") ?? null;
5878
5887
  const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
5879
5888
  return [
5880
5889
  `${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
5890
+ ...openNowItem ? [`${c2.dim}Open now:${c2.reset} load_recall_item("${openNowItem.key}")`] : [],
5881
5891
  ...fetchHint ? [`${c2.dim}Pull detail:${c2.reset} ${fetchHint}`] : []
5882
5892
  ];
5883
5893
  }
5894
+ function formatStartupRecallPreview(recallItems) {
5895
+ const items = recallItems.slice(0, 3);
5896
+ if (items.length === 0)
5897
+ return [];
5898
+ return [
5899
+ `${c2.dim}Recall preview:${c2.reset} exact keys you can open now`,
5900
+ ...items.map((item) => `${item.key} [${item.kind} \xB7 ${item.freshness}] ${truncateInline(item.title, 110)}`)
5901
+ ];
5902
+ }
5903
+ function buildStartupRecallItems(context) {
5904
+ const items = [];
5905
+ for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
5906
+ const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+\u00B7\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
5907
+ if (!title)
5908
+ continue;
5909
+ const freshness = classifyResumeFreshness(handoff.created_at_epoch);
5910
+ items.push({
5911
+ key: `handoff:${handoff.id}`,
5912
+ kind: "handoff",
5913
+ freshness,
5914
+ title,
5915
+ score: freshnessScore(freshness) + 40
5916
+ });
5917
+ }
5918
+ for (const session of context.recentSessions?.slice(0, 2) ?? []) {
5919
+ const title = chooseMeaningfulSessionSummary(session.request, session.completed);
5920
+ if (!title)
5921
+ continue;
5922
+ const sourceEpoch = session.completed_at_epoch ?? session.started_at_epoch ?? null;
5923
+ const freshness = classifyResumeFreshness(sourceEpoch);
5924
+ items.push({
5925
+ key: `session:${session.session_id}`,
5926
+ kind: "thread",
5927
+ freshness,
5928
+ title,
5929
+ score: freshnessScore(freshness) + 30
5930
+ });
5931
+ }
5932
+ for (const message of context.recentChatMessages?.slice(-2) ?? []) {
5933
+ if (!message.content.trim())
5934
+ continue;
5935
+ const freshness = classifyResumeFreshness(message.created_at_epoch);
5936
+ items.push({
5937
+ key: `chat:${message.id}`,
5938
+ kind: "chat",
5939
+ freshness,
5940
+ title: `[${message.role}] ${message.content.replace(/\s+/g, " ").trim()}`,
5941
+ score: freshnessScore(freshness) + 20
5942
+ });
5943
+ }
5944
+ for (const obs of pickContextIndexObservations(context).slice(0, 2)) {
5945
+ const createdAtEpoch = Math.floor(new Date(obs.created_at).getTime() / 1000);
5946
+ const freshness = classifyResumeFreshness(Number.isFinite(createdAtEpoch) ? createdAtEpoch : null);
5947
+ items.push({
5948
+ key: `obs:${obs.id}`,
5949
+ kind: "memory",
5950
+ freshness,
5951
+ title: obs.title,
5952
+ score: freshnessScore(freshness) + 10
5953
+ });
5954
+ }
5955
+ const seen = new Set;
5956
+ return items.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)).filter((item) => {
5957
+ if (seen.has(item.key))
5958
+ return false;
5959
+ seen.add(item.key);
5960
+ return true;
5961
+ }).map(({ score: _score, ...item }) => item);
5962
+ }
5963
+ function freshnessScore(freshness) {
5964
+ if (freshness === "live")
5965
+ return 3;
5966
+ if (freshness === "recent")
5967
+ return 2;
5968
+ return 1;
5969
+ }
5884
5970
  function rememberShownItem(shown, value) {
5885
5971
  if (!value)
5886
5972
  return;
@@ -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.32",
3085
+ client_version: "0.4.33",
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
@@ -18602,6 +18602,7 @@ function getProjectMemoryIndex(db, input) {
18602
18602
  `));
18603
18603
  const continuityState = classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount.length, recentChatCount, recentSessions, recentOutcomes.length);
18604
18604
  const sourceTimestamp = pickResumeSourceTimestamp(latestSession, recentChat.messages);
18605
+ const bestRecallItem = pickBestRecallItem(recallIndex.items);
18605
18606
  return {
18606
18607
  project: project.name,
18607
18608
  canonical_id: project.canonical_id,
@@ -18615,6 +18616,9 @@ function getProjectMemoryIndex(db, input) {
18615
18616
  freshness: item.freshness,
18616
18617
  title: item.title
18617
18618
  })),
18619
+ best_recall_key: bestRecallItem?.key ?? null,
18620
+ best_recall_title: bestRecallItem?.title ?? null,
18621
+ best_recall_kind: bestRecallItem?.kind ?? null,
18618
18622
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18619
18623
  resume_source_session_id: latestSession?.session_id ?? null,
18620
18624
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18643,6 +18647,9 @@ function getProjectMemoryIndex(db, input) {
18643
18647
  suggested_tools: suggestedTools
18644
18648
  };
18645
18649
  }
18650
+ function pickBestRecallItem(items) {
18651
+ return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
18652
+ }
18646
18653
  function pickResumeSourceTimestamp(latestSession, messages) {
18647
18654
  const latestChatEpoch = messages.length > 0 ? messages[messages.length - 1]?.created_at_epoch ?? null : null;
18648
18655
  return latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
@@ -18825,6 +18832,9 @@ function getMemoryConsole(db, input) {
18825
18832
  freshness: item.freshness,
18826
18833
  title: item.title
18827
18834
  })),
18835
+ best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
18836
+ best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
18837
+ best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.kind ?? null,
18828
18838
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18829
18839
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18830
18840
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -19721,6 +19731,7 @@ function getSessionContext(db, input) {
19721
19731
  const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
19722
19732
  const latestChatEpoch = recentChat.messages.length > 0 ? recentChat.messages[recentChat.messages.length - 1]?.created_at_epoch ?? null : null;
19723
19733
  const resumeTimestamp = latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
19734
+ const bestRecallItem = recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null;
19724
19735
  return {
19725
19736
  project_name: context.project_name,
19726
19737
  canonical_id: context.canonical_id,
@@ -19734,6 +19745,9 @@ function getSessionContext(db, input) {
19734
19745
  freshness: item.freshness,
19735
19746
  title: item.title
19736
19747
  })),
19748
+ best_recall_key: bestRecallItem?.key ?? null,
19749
+ best_recall_title: bestRecallItem?.title ?? null,
19750
+ best_recall_kind: bestRecallItem?.kind ?? null,
19737
19751
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
19738
19752
  resume_source_session_id: latestSession?.session_id ?? null,
19739
19753
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -20390,6 +20404,7 @@ async function resumeThread(db, config2, input = {}) {
20390
20404
  }
20391
20405
  }
20392
20406
  const { context, handoff, recentChat, recentSessions, recall } = snapshot;
20407
+ const bestRecallItem = pickBestRecallItem2(snapshot.recallIndex.items);
20393
20408
  const latestSession = recentSessions[0] ?? null;
20394
20409
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
20395
20410
  const inferredRequest = latestSession?.request?.trim() || null;
@@ -20418,6 +20433,7 @@ async function resumeThread(db, config2, input = {}) {
20418
20433
  currentThread
20419
20434
  });
20420
20435
  const suggestedTools = Array.from(new Set([
20436
+ ...bestRecallItem ? ["load_recall_item"] : [],
20421
20437
  "search_recall",
20422
20438
  ...recentChat.coverage_state !== "transcript-backed" && recentChat.messages.length > 0 ? ["repair_recall", "refresh_chat_recall"] : [],
20423
20439
  ...handoff ? ["load_handoff"] : [],
@@ -20432,6 +20448,9 @@ async function resumeThread(db, config2, input = {}) {
20432
20448
  resume_source_device_id: handoff?.device_id ?? latestSession?.device_id ?? null,
20433
20449
  resume_confidence: resumeConfidence,
20434
20450
  resume_basis: resumeBasis,
20451
+ best_recall_key: bestRecallItem?.key ?? null,
20452
+ best_recall_title: bestRecallItem?.title ?? null,
20453
+ best_recall_kind: bestRecallItem?.kind ?? null,
20435
20454
  repair_attempted: shouldRepair,
20436
20455
  repair_result: repairResult ? {
20437
20456
  imported_chat_messages: repairResult.imported_chat_messages,
@@ -20490,14 +20509,25 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20490
20509
  user_id: userId,
20491
20510
  limit
20492
20511
  });
20512
+ const recallIndex = listRecallItems(db, {
20513
+ cwd,
20514
+ project_scoped: true,
20515
+ user_id: userId,
20516
+ current_device_id: currentDeviceId,
20517
+ limit
20518
+ });
20493
20519
  return {
20494
20520
  context,
20495
20521
  handoff: handoffResult.handoff,
20496
20522
  recentChat,
20497
20523
  recentSessions,
20498
- recall
20524
+ recall,
20525
+ recallIndex
20499
20526
  };
20500
20527
  }
20528
+ function pickBestRecallItem2(items) {
20529
+ return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
20530
+ }
20501
20531
  function extractCurrentThread(handoff) {
20502
20532
  const narrative = handoff?.narrative ?? "";
20503
20533
  const match = narrative.match(/Current thread:\s*(.+)/i);
@@ -22429,7 +22459,7 @@ process.on("SIGTERM", () => {
22429
22459
  });
22430
22460
  var server = new McpServer({
22431
22461
  name: "engrm",
22432
- version: "0.4.32"
22462
+ version: "0.4.33"
22433
22463
  });
22434
22464
  server.tool("save_observation", "Save an observation to memory", {
22435
22465
  type: exports_external.enum([
@@ -23023,6 +23053,8 @@ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?
23023
23053
  const handoffLine = result.handoff ? `Handoff: #${result.handoff.id} ${result.handoff.title}${result.handoff.source ? ` (${result.handoff.source})` : ""}
23024
23054
  ` : `Handoff: (none)
23025
23055
  `;
23056
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23057
+ ` : "";
23026
23058
  const basisLines = result.resume_basis.length > 0 ? result.resume_basis.map((item) => `- ${item}`).join(`
23027
23059
  `) : "- (none)";
23028
23060
  const toolTrailLines = result.tool_trail.length > 0 ? result.tool_trail.map((item) => `- ${item}`).join(`
@@ -23057,7 +23089,7 @@ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?
23057
23089
  ` + `Freshness: ${result.resume_freshness}
23058
23090
  ` + `Source: ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23059
23091
  ` + `Resume confidence: ${result.resume_confidence}
23060
- ` + repairLine + `Current thread: ${result.current_thread ?? "(unknown)"}
23092
+ ` + repairLine + openExactLine + `Current thread: ${result.current_thread ?? "(unknown)"}
23061
23093
  ` + `Latest request: ${result.latest_request ?? "(none)"}
23062
23094
  ` + `${handoffLine}` + `Chat recall: ${result.chat_coverage_state}
23063
23095
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
@@ -23395,6 +23427,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23395
23427
  `) : "- (none)";
23396
23428
  const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
23397
23429
  `) : "- (none)";
23430
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23431
+ ` : "";
23398
23432
  const projectLine = result.project ? `Project: ${result.project}
23399
23433
 
23400
23434
  ` : "";
@@ -23416,7 +23450,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23416
23450
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
23417
23451
  ` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23418
23452
 
23419
- ` + `Recall preview:
23453
+ ` + openExactLine + `Recall preview:
23420
23454
  ${recallPreviewLines}
23421
23455
 
23422
23456
  ` + `Next actions:
@@ -23617,6 +23651,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23617
23651
  ` + `Canonical ID: ${result.canonical_id}
23618
23652
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23619
23653
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23654
+ ` + `Open exact: ${result.best_recall_key ? `load_recall_item("${result.best_recall_key}")` : "(none)"}
23620
23655
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23621
23656
  ` + `Loaded observations: ${result.session_count}
23622
23657
  ` + `Searchable total: ${result.total_active}
@@ -23697,6 +23732,8 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23697
23732
  `) : "- (none)";
23698
23733
  const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
23699
23734
  `) : "- (none)";
23735
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23736
+ ` : "";
23700
23737
  return {
23701
23738
  content: [
23702
23739
  {
@@ -23720,7 +23757,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23720
23757
  ` + `Estimated read cost: ~${result.estimated_read_tokens}t
23721
23758
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23722
23759
 
23723
- ` + `Recall preview:
23760
+ ` + openExactLine + `Recall preview:
23724
23761
  ${recallPreviewLines}
23725
23762
 
23726
23763
  ` + `Next actions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.32",
3
+ "version": "0.4.33",
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",