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 +43 -10
- package/dist/hooks/pre-compact.js +27 -0
- package/dist/hooks/session-start.js +88 -1
- package/dist/hooks/stop.js +1 -1
- package/dist/server.js +853 -54
- package/package.json +2 -2
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` |
|
|
230
|
-
| `
|
|
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.
|
|
363
|
-
4.
|
|
364
|
-
5.
|
|
365
|
-
6.
|
|
366
|
-
7.
|
|
367
|
-
8.
|
|
368
|
-
9.
|
|
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.
|
|
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
|
}
|
package/dist/hooks/stop.js
CHANGED
|
@@ -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.
|
|
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,
|