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 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 and recent sessions |
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
- - `memory_console` gives the quickest project snapshot
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
- const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
3368
- const recentPrompts2 = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
3369
- const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
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: !isNewProject,
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 = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
3417
- const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
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: !isNewProject,
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: selected.map(toContextObservation),
3477
- session_count: selected.length,
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)`);