engrm 0.4.30 → 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,14 @@ 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
261
+ - `resume_thread`
262
+ - get one direct “where were we?” resume point with freshness, source, tool trail, and next actions
263
+ - `repair_recall`
264
+ - repair weak project recall from transcript or Claude history fallback before you give up on continuity
255
265
 
256
266
  These are the tools we should be comfortable pointing people to publicly first:
257
267
 
@@ -259,6 +269,23 @@ These are the tools we should be comfortable pointing people to publicly first:
259
269
  - local-first execution
260
270
  - durable memory output instead of raw transcript dumping
261
271
  - easy local inspection after capture
272
+ - clear continuity recovery when switching devices or resuming long sessions
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
262
289
 
263
290
  ### Thin Tools, Thick Memory
264
291
 
@@ -339,6 +366,8 @@ For long sessions, Engrm now also supports transcript-backed chat hydration:
339
366
  - `resume_thread`
340
367
  - gives OpenClaw or Claude one direct “where were we?” action
341
368
  - combines the best handoff, the current thread, recent outcomes, recent chat, and unified recall
369
+ - reports whether the resume point is `strong`, `usable`, or `thin`
370
+ - can attempt recall repair first when continuity is still weak
342
371
  - makes Engrm usable as the primary live continuity layer instead of forcing agents to choose between low-level recall tools
343
372
 
344
373
  Before Claude compacts, Engrm now also:
@@ -359,13 +388,14 @@ Recommended flow:
359
388
  ```text
360
389
  1. capture_status
361
390
  2. memory_console
362
- 3. activity_feed
363
- 4. recent_sessions
364
- 5. session_story
365
- 6. tool_memory_index
366
- 7. session_tool_memory
367
- 8. project_memory_index
368
- 9. workspace_memory_index
391
+ 3. resume_thread
392
+ 4. activity_feed
393
+ 5. recent_sessions
394
+ 6. session_story
395
+ 7. tool_memory_index
396
+ 8. session_tool_memory
397
+ 9. project_memory_index
398
+ 10. workspace_memory_index
369
399
  ```
370
400
 
371
401
  What each tool is good for:
@@ -373,9 +403,12 @@ What each tool is good for:
373
403
  - `capture_status` tells you whether prompt/tool hooks are live on this machine
374
404
  - `capture_quality` shows whether chat recall is transcript-backed, history-backed, or still hook-only across the workspace
375
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
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
376
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
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
377
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
378
- - `resume_thread` is now the fastest “get me back into the live thread” path when you do not want to think about which continuity lane to inspect
379
412
  - the workbench and startup hints now also prefer `search_recall` as the first “what were we just talking about?” path when recent prompts/chat/observations exist
380
413
  - `search_chat` now uses hybrid lexical + semantic ranking when sqlite-vec and local embeddings are available, so recent conversation recall is less dependent on exact wording
381
414
  - `activity_feed` shows the merged chronology across prompts, tools, chat, handoffs, observations, and summaries
@@ -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)}`;
@@ -2939,6 +2993,16 @@ function getRecentOutcomes2(db, projectId, userId, recentSessions) {
2939
2993
  }
2940
2994
 
2941
2995
  // src/tools/project-memory-index.ts
2996
+ function classifyResumeFreshness(sourceTimestamp) {
2997
+ if (!sourceTimestamp)
2998
+ return "stale";
2999
+ const ageMs = Date.now() - sourceTimestamp * 1000;
3000
+ if (ageMs <= 15 * 60 * 1000)
3001
+ return "live";
3002
+ if (ageMs <= 3 * 24 * 60 * 60 * 1000)
3003
+ return "recent";
3004
+ return "stale";
3005
+ }
2942
3006
  function classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount, recentChatCount, recentSessions, recentOutcomesCount) {
2943
3007
  const hasRaw = recentRequestsCount > 0 || recentToolsCount > 0;
2944
3008
  const hasResume = recentHandoffsCount > 0 || recentChatCount > 0;
@@ -3080,7 +3144,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3080
3144
  import { join as join3 } from "node:path";
3081
3145
  import { homedir } from "node:os";
3082
3146
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3083
- var CLIENT_VERSION = "0.4.30";
3147
+ var CLIENT_VERSION = "0.4.32";
3084
3148
  function hashFile(filePath) {
3085
3149
  try {
3086
3150
  if (!existsSync3(filePath))
@@ -5590,6 +5654,10 @@ function formatVisibleStartupBrief(context) {
5590
5654
  }
5591
5655
  }
5592
5656
  lines.push(`${c2.cyan}Continuity:${c2.reset} ${continuityState} \u2014 ${truncateInline(describeContinuityState(continuityState), 160)}`);
5657
+ const resumeReadiness = buildResumeReadinessLine(context);
5658
+ if (resumeReadiness) {
5659
+ lines.push(`${c2.cyan}Resume:${c2.reset} ${truncateInline(resumeReadiness, 160)}`);
5660
+ }
5593
5661
  if (promptLines.length > 0) {
5594
5662
  lines.push(`${c2.cyan}Asked recently:${c2.reset}`);
5595
5663
  for (const item of promptLines) {
@@ -5710,6 +5778,23 @@ function buildLatestHandoffLines(context) {
5710
5778
  }
5711
5779
  return Array.from(new Set(lines.filter(Boolean))).slice(0, 2);
5712
5780
  }
5781
+ function buildResumeReadinessLine(context) {
5782
+ const latestSession = context.recentSessions?.[0] ?? null;
5783
+ const latestHandoff = context.recentHandoffs?.[0] ?? null;
5784
+ const latestChatEpoch = (context.recentChatMessages ?? []).length > 0 ? context.recentChatMessages?.[context.recentChatMessages.length - 1]?.created_at_epoch ?? null : null;
5785
+ const sourceTimestamp = latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? latestHandoff?.created_at_epoch ?? null;
5786
+ if (!sourceTimestamp)
5787
+ return null;
5788
+ const freshness = classifyResumeFreshness(sourceTimestamp);
5789
+ const sourceSessionId = latestSession?.session_id ?? latestHandoff?.session_id ?? null;
5790
+ const sourceDevice = latestSession?.device_id ?? latestHandoff?.device_id ?? null;
5791
+ const bits = [freshness];
5792
+ if (sourceDevice)
5793
+ bits.push(`from ${sourceDevice}`);
5794
+ if (sourceSessionId)
5795
+ bits.push(sourceSessionId);
5796
+ return bits.join(" \xB7 ");
5797
+ }
5713
5798
  function formatContextEconomics(data) {
5714
5799
  const totalMemories = Math.max(0, data.loaded + data.available);
5715
5800
  const parts = [];
@@ -5763,6 +5848,8 @@ function formatInspectHints(context, visibleObservationIds = []) {
5763
5848
  hints.push("activity_feed");
5764
5849
  }
5765
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");
5766
5853
  hints.push("resume_thread");
5767
5854
  hints.push("search_recall");
5768
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.30",
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,