engrm 0.4.25 → 0.4.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/hooks/post-tool-use.js +8 -0
- package/dist/hooks/pre-compact.js +55 -11
- package/dist/hooks/session-start.js +909 -133
- package/dist/hooks/stop.js +9 -1
- package/dist/hooks/user-prompt-submit.js +8 -0
- package/dist/server.js +415 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -212,8 +212,8 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
212
212
|
| `memory_stats` | View high-level capture and sync health |
|
|
213
213
|
| `capture_status` | Check whether local hooks are registered and raw prompt/tool chronology is actually being captured |
|
|
214
214
|
| `activity_feed` | Inspect one chronological local feed across prompts, tools, chat, handoffs, observations, and summaries |
|
|
215
|
-
| `memory_console` | Show a high-signal local memory console for the current project |
|
|
216
|
-
| `project_memory_index` | Show typed local memory by project, including hot files
|
|
215
|
+
| `memory_console` | Show a high-signal local memory console for the current project, including continuity state |
|
|
216
|
+
| `project_memory_index` | Show typed local memory by project, including hot files, recent sessions, and continuity state |
|
|
217
217
|
| `workspace_memory_index` | Show cross-project local memory coverage across the whole workspace |
|
|
218
218
|
| `tool_memory_index` | Show which source tools and plugins are creating durable memory |
|
|
219
219
|
| `session_tool_memory` | Show which tools in one session produced reusable memory and which produced none |
|
|
@@ -228,6 +228,7 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
228
228
|
| `refresh_chat_recall` | Rehydrate the separate chat lane from a Claude transcript when a long session feels under-captured |
|
|
229
229
|
| `recent_chat` | Inspect the separate synced chat lane without mixing it into durable memory |
|
|
230
230
|
| `search_chat` | Search recent chat recall separately from reusable memory observations |
|
|
231
|
+
| `search_recall` | Search durable memory and chat recall together when you do not want to guess the right lane |
|
|
231
232
|
| `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
|
|
232
233
|
| `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
|
|
233
234
|
| `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
|
|
@@ -357,14 +358,18 @@ Recommended flow:
|
|
|
357
358
|
What each tool is good for:
|
|
358
359
|
|
|
359
360
|
- `capture_status` tells you whether prompt/tool hooks are live on this machine
|
|
360
|
-
- `
|
|
361
|
+
- `capture_quality` shows whether chat recall is transcript-backed or still hook-only across the workspace
|
|
362
|
+
- `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
|
|
363
|
+
- `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed or only hook-captured
|
|
364
|
+
- when chat continuity is only hook-captured, the workbench and startup hints now prefer `refresh_chat_recall`
|
|
361
365
|
- `activity_feed` shows the merged chronology across prompts, tools, chat, handoffs, observations, and summaries
|
|
362
366
|
- `recent_sessions` helps you pick a session worth opening
|
|
363
367
|
- `session_story` reconstructs one session in detail, including handoffs and chat recall
|
|
364
368
|
- `tool_memory_index` shows which tools and plugins are actually producing durable memory
|
|
365
369
|
- `session_tool_memory` shows which tool calls in one session turned into reusable memory and which did not
|
|
366
|
-
- `project_memory_index` shows typed memory by repo
|
|
370
|
+
- `project_memory_index` shows typed memory by repo, including continuity state and hot files
|
|
367
371
|
- `workspace_memory_index` shows coverage across all repos on the machine
|
|
372
|
+
- `recent_chat` / `search_chat` now report transcript-vs-hook coverage too, so weak OpenClaw recall is easier to diagnose and refresh
|
|
368
373
|
|
|
369
374
|
### Thin Tool Workflow
|
|
370
375
|
|
|
@@ -3831,6 +3831,8 @@ function getSessionStory(db, input) {
|
|
|
3831
3831
|
summary,
|
|
3832
3832
|
prompts,
|
|
3833
3833
|
chat_messages: chatMessages,
|
|
3834
|
+
chat_source_summary: summarizeChatSources(chatMessages),
|
|
3835
|
+
chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
|
|
3834
3836
|
tool_events: toolEvents,
|
|
3835
3837
|
observations,
|
|
3836
3838
|
handoffs,
|
|
@@ -3928,6 +3930,12 @@ function collectProvenanceSummary(observations) {
|
|
|
3928
3930
|
}
|
|
3929
3931
|
return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
3930
3932
|
}
|
|
3933
|
+
function summarizeChatSources(messages) {
|
|
3934
|
+
return messages.reduce((summary, message) => {
|
|
3935
|
+
summary[message.source_kind] += 1;
|
|
3936
|
+
return summary;
|
|
3937
|
+
}, { transcript: 0, hook: 0 });
|
|
3938
|
+
}
|
|
3931
3939
|
|
|
3932
3940
|
// src/tools/handoffs.ts
|
|
3933
3941
|
async function upsertRollingHandoff(db, config, input) {
|
|
@@ -2154,6 +2154,8 @@ function getSessionStory(db, input) {
|
|
|
2154
2154
|
summary,
|
|
2155
2155
|
prompts,
|
|
2156
2156
|
chat_messages: chatMessages,
|
|
2157
|
+
chat_source_summary: summarizeChatSources(chatMessages),
|
|
2158
|
+
chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
|
|
2157
2159
|
tool_events: toolEvents,
|
|
2158
2160
|
observations,
|
|
2159
2161
|
handoffs,
|
|
@@ -2251,6 +2253,12 @@ function collectProvenanceSummary(observations) {
|
|
|
2251
2253
|
}
|
|
2252
2254
|
return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
2253
2255
|
}
|
|
2256
|
+
function summarizeChatSources(messages) {
|
|
2257
|
+
return messages.reduce((summary, message) => {
|
|
2258
|
+
summary[message.source_kind] += 1;
|
|
2259
|
+
return summary;
|
|
2260
|
+
}, { transcript: 0, hook: 0 });
|
|
2261
|
+
}
|
|
2254
2262
|
|
|
2255
2263
|
// src/tools/save.ts
|
|
2256
2264
|
import { relative, isAbsolute } from "node:path";
|
|
@@ -3225,6 +3233,7 @@ function compactLine(value) {
|
|
|
3225
3233
|
}
|
|
3226
3234
|
|
|
3227
3235
|
// src/context/inject.ts
|
|
3236
|
+
var FRESH_CONTINUITY_WINDOW_DAYS = 3;
|
|
3228
3237
|
function tokenizeProjectHint(text) {
|
|
3229
3238
|
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
3230
3239
|
}
|
|
@@ -3364,20 +3373,21 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
3364
3373
|
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
3365
3374
|
if (maxCount !== undefined) {
|
|
3366
3375
|
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
3367
|
-
|
|
3368
|
-
const recentPrompts2 =
|
|
3369
|
-
const recentToolEvents2 =
|
|
3376
|
+
let all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
3377
|
+
const recentPrompts2 = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
3378
|
+
const recentToolEvents2 = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
3370
3379
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
3371
3380
|
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
3372
3381
|
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
|
|
3373
|
-
const recentHandoffs2 = getRecentHandoffs(db, {
|
|
3382
|
+
const recentHandoffs2 = isNewProject ? [] : getRecentHandoffs(db, {
|
|
3374
3383
|
cwd,
|
|
3375
|
-
project_scoped:
|
|
3384
|
+
project_scoped: true,
|
|
3376
3385
|
user_id: opts.userId,
|
|
3377
3386
|
current_device_id: opts.currentDeviceId,
|
|
3378
3387
|
limit: 3
|
|
3379
3388
|
}).handoffs;
|
|
3380
3389
|
const recentChatMessages2 = !isNewProject && project ? db.getRecentChatMessages(project.id, 4, opts.userId) : [];
|
|
3390
|
+
all = filterAutoLoadedObservationsForContinuity(all, pinned, isNewProject, recentPrompts2, recentToolEvents2, recentSessions2, recentHandoffs2, recentChatMessages2, summariesFromRecentSessions(db, projectId, recentSessions2));
|
|
3381
3391
|
return {
|
|
3382
3392
|
project_name: projectName,
|
|
3383
3393
|
canonical_id: canonicalId,
|
|
@@ -3413,19 +3423,20 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
3413
3423
|
selected.push(obs);
|
|
3414
3424
|
}
|
|
3415
3425
|
const summaries = isNewProject ? [] : db.getRecentSummaries(projectId, 5);
|
|
3416
|
-
const recentPrompts =
|
|
3417
|
-
const recentToolEvents =
|
|
3426
|
+
const recentPrompts = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
3427
|
+
const recentToolEvents = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
3418
3428
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
3419
3429
|
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
3420
3430
|
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
|
|
3421
|
-
const recentHandoffs = getRecentHandoffs(db, {
|
|
3431
|
+
const recentHandoffs = isNewProject ? [] : getRecentHandoffs(db, {
|
|
3422
3432
|
cwd,
|
|
3423
|
-
project_scoped:
|
|
3433
|
+
project_scoped: true,
|
|
3424
3434
|
user_id: opts.userId,
|
|
3425
3435
|
current_device_id: opts.currentDeviceId,
|
|
3426
3436
|
limit: 3
|
|
3427
3437
|
}).handoffs;
|
|
3428
3438
|
const recentChatMessages = !isNewProject ? db.getRecentChatMessages(projectId, 4, opts.userId) : [];
|
|
3439
|
+
const filteredSelected = filterAutoLoadedObservationsForContinuity(selected, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries);
|
|
3429
3440
|
let securityFindings = [];
|
|
3430
3441
|
if (!isNewProject) {
|
|
3431
3442
|
try {
|
|
@@ -3473,8 +3484,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
3473
3484
|
return {
|
|
3474
3485
|
project_name: projectName,
|
|
3475
3486
|
canonical_id: canonicalId,
|
|
3476
|
-
observations:
|
|
3477
|
-
session_count:
|
|
3487
|
+
observations: filteredSelected.map(toContextObservation),
|
|
3488
|
+
session_count: filteredSelected.length,
|
|
3478
3489
|
total_active: totalActive,
|
|
3479
3490
|
summaries: summaries.length > 0 ? summaries : undefined,
|
|
3480
3491
|
securityFindings: securityFindings.length > 0 ? securityFindings : undefined,
|
|
@@ -3489,6 +3500,39 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
3489
3500
|
recentChatMessages: recentChatMessages.length > 0 ? recentChatMessages : undefined
|
|
3490
3501
|
};
|
|
3491
3502
|
}
|
|
3503
|
+
function filterAutoLoadedObservationsForContinuity(observations, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
3504
|
+
if (isNewProject)
|
|
3505
|
+
return observations;
|
|
3506
|
+
if (hasFreshProjectContinuity(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries)) {
|
|
3507
|
+
return observations;
|
|
3508
|
+
}
|
|
3509
|
+
const pinnedIds = new Set(pinned.map((obs) => obs.id));
|
|
3510
|
+
return observations.filter((obs) => {
|
|
3511
|
+
if (pinnedIds.has(obs.id))
|
|
3512
|
+
return true;
|
|
3513
|
+
return observationAgeDays(obs.created_at_epoch) <= FRESH_CONTINUITY_WINDOW_DAYS;
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
function hasFreshProjectContinuity(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
3517
|
+
const freshEnough = (epoch) => typeof epoch === "number" && observationAgeDays(epoch) <= FRESH_CONTINUITY_WINDOW_DAYS;
|
|
3518
|
+
return recentPrompts.some((item) => freshEnough(item.created_at_epoch)) || recentToolEvents.some((item) => freshEnough(item.created_at_epoch)) || recentSessions.some((item) => freshEnough(item.completed_at_epoch ?? item.started_at_epoch)) || recentHandoffs.some((item) => freshEnough(item.created_at_epoch)) || recentChatMessages.some((item) => freshEnough(item.created_at_epoch)) || summaries.some((item) => freshEnough(item.created_at_epoch));
|
|
3519
|
+
}
|
|
3520
|
+
function summariesFromRecentSessions(db, projectId, recentSessions) {
|
|
3521
|
+
const seen = new Set;
|
|
3522
|
+
const rows = [];
|
|
3523
|
+
for (const session of recentSessions) {
|
|
3524
|
+
if (seen.has(session.session_id))
|
|
3525
|
+
continue;
|
|
3526
|
+
seen.add(session.session_id);
|
|
3527
|
+
const summary = db.getSessionSummary(session.session_id);
|
|
3528
|
+
if (summary && summary.project_id === projectId)
|
|
3529
|
+
rows.push(summary);
|
|
3530
|
+
}
|
|
3531
|
+
return rows;
|
|
3532
|
+
}
|
|
3533
|
+
function observationAgeDays(createdAtEpoch) {
|
|
3534
|
+
return Math.max(0, (Math.floor(Date.now() / 1000) - createdAtEpoch) / 86400);
|
|
3535
|
+
}
|
|
3492
3536
|
function estimateObservationTokens(obs, index) {
|
|
3493
3537
|
const DETAILED_THRESHOLD = 5;
|
|
3494
3538
|
const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|