engrm 0.4.29 → 0.4.30

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,6 +226,8 @@ 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
231
  | `recent_chat` | Inspect the separate synced chat lane without mixing it into durable memory |
230
232
  | `search_chat` | Search recent chat recall with hybrid lexical + semantic matching, separately from reusable memory observations |
231
233
  | `search_recall` | Search durable memory and chat recall together when you do not want to guess the right lane |
@@ -328,6 +330,17 @@ For long sessions, Engrm now also supports transcript-backed chat hydration:
328
330
  - fills gaps in the separate chat lane with transcript-backed messages
329
331
  - keeps those rows marked separately from hook-edge chat so recall can prefer the fuller thread
330
332
 
333
+ - `repair_recall`
334
+ - scans recent sessions for the current project
335
+ - rehydrates recall from transcript files when they exist
336
+ - falls back to Claude `history.jsonl` when transcript/session alignment is missing
337
+ - reports whether recovered chat is `transcript-backed`, `history-backed`, or still only `hook-only`
338
+
339
+ - `resume_thread`
340
+ - gives OpenClaw or Claude one direct “where were we?” action
341
+ - combines the best handoff, the current thread, recent outcomes, recent chat, and unified recall
342
+ - makes Engrm usable as the primary live continuity layer instead of forcing agents to choose between low-level recall tools
343
+
331
344
  Before Claude compacts, Engrm now also:
332
345
 
333
346
  - refreshes transcript-backed chat recall for the active session
@@ -358,10 +371,11 @@ Recommended flow:
358
371
  What each tool is good for:
359
372
 
360
373
  - `capture_status` tells you whether prompt/tool hooks are live on this machine
361
- - `capture_quality` shows whether chat recall is transcript-backed or still hook-only across the workspace
374
+ - `capture_quality` shows whether chat recall is transcript-backed, history-backed, or still hook-only across the workspace
362
375
  - `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`
376
+ - `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed, history-backed, or only hook-captured
377
+ - 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
365
379
  - 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
366
380
  - `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
367
381
  - `activity_feed` shows the merged chronology across prompts, tools, chat, handoffs, observations, and summaries
@@ -371,7 +385,7 @@ What each tool is good for:
371
385
  - `session_tool_memory` shows which tool calls in one session turned into reusable memory and which did not
372
386
  - `project_memory_index` shows typed memory by repo, including continuity state and hot files
373
387
  - `workspace_memory_index` shows coverage across all repos on the machine
374
- - `recent_chat` / `search_chat` now report transcript-vs-hook coverage too, and `search_chat` will also mark when semantic ranking was available, so weak OpenClaw recall is easier to diagnose and refresh
388
+ - `recent_chat` / `search_chat` now report transcript-vs-history-vs-hook coverage too, and `search_chat` will also mark when semantic ranking was available, so weak OpenClaw recall is easier to diagnose and repair
375
389
 
376
390
  ### Thin Tool Workflow
377
391
 
@@ -4007,6 +4007,22 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4007
4007
  }
4008
4008
 
4009
4009
  // src/tools/recent-chat.ts
4010
+ function summarizeChatSources(messages) {
4011
+ return messages.reduce((summary, message) => {
4012
+ summary[getChatCaptureOrigin(message)] += 1;
4013
+ return summary;
4014
+ }, { transcript: 0, history: 0, hook: 0 });
4015
+ }
4016
+ function getChatCoverageState(messagesOrSummary) {
4017
+ const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
4018
+ if (summary.transcript > 0)
4019
+ return "transcript-backed";
4020
+ if (summary.history > 0)
4021
+ return "history-backed";
4022
+ if (summary.hook > 0)
4023
+ return "hook-only";
4024
+ return "none";
4025
+ }
4010
4026
  function getChatCaptureOrigin(message) {
4011
4027
  if (message.source_kind === "transcript")
4012
4028
  return "transcript";
@@ -4038,7 +4054,7 @@ function getSessionStory(db, input) {
4038
4054
  prompts,
4039
4055
  chat_messages: chatMessages,
4040
4056
  chat_source_summary: summarizeChatSources(chatMessages),
4041
- chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
4057
+ chat_coverage_state: getChatCoverageState(chatMessages),
4042
4058
  tool_events: toolEvents,
4043
4059
  observations,
4044
4060
  handoffs,
@@ -4136,12 +4152,6 @@ function collectProvenanceSummary(observations) {
4136
4152
  }
4137
4153
  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);
4138
4154
  }
4139
- function summarizeChatSources(messages) {
4140
- return messages.reduce((summary, message) => {
4141
- summary[getChatCaptureOrigin(message)] += 1;
4142
- return summary;
4143
- }, { transcript: 0, history: 0, hook: 0 });
4144
- }
4145
4155
 
4146
4156
  // src/tools/handoffs.ts
4147
4157
  async function upsertRollingHandoff(db, config, input) {
@@ -2204,6 +2204,22 @@ function computeObservationPriority(obs, nowEpoch) {
2204
2204
  }
2205
2205
 
2206
2206
  // src/tools/recent-chat.ts
2207
+ function summarizeChatSources(messages) {
2208
+ return messages.reduce((summary, message) => {
2209
+ summary[getChatCaptureOrigin(message)] += 1;
2210
+ return summary;
2211
+ }, { transcript: 0, history: 0, hook: 0 });
2212
+ }
2213
+ function getChatCoverageState(messagesOrSummary) {
2214
+ const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
2215
+ if (summary.transcript > 0)
2216
+ return "transcript-backed";
2217
+ if (summary.history > 0)
2218
+ return "history-backed";
2219
+ if (summary.hook > 0)
2220
+ return "hook-only";
2221
+ return "none";
2222
+ }
2207
2223
  function getChatCaptureOrigin(message) {
2208
2224
  if (message.source_kind === "transcript")
2209
2225
  return "transcript";
@@ -2235,7 +2251,7 @@ function getSessionStory(db, input) {
2235
2251
  prompts,
2236
2252
  chat_messages: chatMessages,
2237
2253
  chat_source_summary: summarizeChatSources(chatMessages),
2238
- chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
2254
+ chat_coverage_state: getChatCoverageState(chatMessages),
2239
2255
  tool_events: toolEvents,
2240
2256
  observations,
2241
2257
  handoffs,
@@ -2333,12 +2349,6 @@ function collectProvenanceSummary(observations) {
2333
2349
  }
2334
2350
  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);
2335
2351
  }
2336
- function summarizeChatSources(messages) {
2337
- return messages.reduce((summary, message) => {
2338
- summary[getChatCaptureOrigin(message)] += 1;
2339
- return summary;
2340
- }, { transcript: 0, history: 0, hook: 0 });
2341
- }
2342
2352
 
2343
2353
  // src/tools/save.ts
2344
2354
  import { relative, isAbsolute } from "node:path";
@@ -474,6 +474,22 @@ function normalizeItem(value) {
474
474
  }
475
475
 
476
476
  // src/tools/recent-chat.ts
477
+ function summarizeChatSources(messages) {
478
+ return messages.reduce((summary, message) => {
479
+ summary[getChatCaptureOrigin(message)] += 1;
480
+ return summary;
481
+ }, { transcript: 0, history: 0, hook: 0 });
482
+ }
483
+ function getChatCoverageState(messagesOrSummary) {
484
+ const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
485
+ if (summary.transcript > 0)
486
+ return "transcript-backed";
487
+ if (summary.history > 0)
488
+ return "history-backed";
489
+ if (summary.hook > 0)
490
+ return "hook-only";
491
+ return "none";
492
+ }
477
493
  function getChatCaptureOrigin(message) {
478
494
  if (message.source_kind === "transcript")
479
495
  return "transcript";
@@ -505,7 +521,7 @@ function getSessionStory(db, input) {
505
521
  prompts,
506
522
  chat_messages: chatMessages,
507
523
  chat_source_summary: summarizeChatSources(chatMessages),
508
- chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
524
+ chat_coverage_state: getChatCoverageState(chatMessages),
509
525
  tool_events: toolEvents,
510
526
  observations,
511
527
  handoffs,
@@ -603,12 +619,6 @@ function collectProvenanceSummary(observations) {
603
619
  }
604
620
  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);
605
621
  }
606
- function summarizeChatSources(messages) {
607
- return messages.reduce((summary, message) => {
608
- summary[getChatCaptureOrigin(message)] += 1;
609
- return summary;
610
- }, { transcript: 0, history: 0, hook: 0 });
611
- }
612
622
 
613
623
  // src/tools/save.ts
614
624
  import { relative, isAbsolute } from "node:path";
@@ -3070,7 +3080,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3070
3080
  import { join as join3 } from "node:path";
3071
3081
  import { homedir } from "node:os";
3072
3082
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3073
- var CLIENT_VERSION = "0.4.29";
3083
+ var CLIENT_VERSION = "0.4.30";
3074
3084
  function hashFile(filePath) {
3075
3085
  try {
3076
3086
  if (!existsSync3(filePath))
@@ -5753,6 +5763,7 @@ function formatInspectHints(context, visibleObservationIds = []) {
5753
5763
  hints.push("activity_feed");
5754
5764
  }
5755
5765
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
5766
+ hints.push("resume_thread");
5756
5767
  hints.push("search_recall");
5757
5768
  }
5758
5769
  if (context.observations.length > 0) {
@@ -5765,8 +5776,9 @@ function formatInspectHints(context, visibleObservationIds = []) {
5765
5776
  if ((context.recentChatMessages?.length ?? 0) > 0) {
5766
5777
  hints.push("recent_chat");
5767
5778
  }
5768
- if (hasHookOnlyRecentChat(context)) {
5779
+ if (hasNonTranscriptRecentChat(context)) {
5769
5780
  hints.push("refresh_chat_recall");
5781
+ hints.push("repair_recall");
5770
5782
  }
5771
5783
  if (continuityState !== "fresh") {
5772
5784
  hints.push("recent_chat");
@@ -6240,7 +6252,7 @@ function hasFreshContinuitySignal(context) {
6240
6252
  function getStartupContinuityState(context) {
6241
6253
  return classifyContinuityState(context.recentPrompts?.length ?? 0, context.recentToolEvents?.length ?? 0, context.recentHandoffs?.length ?? 0, context.recentChatMessages?.length ?? 0, context.recentSessions ?? [], context.recentOutcomes?.length ?? 0);
6242
6254
  }
6243
- function hasHookOnlyRecentChat(context) {
6255
+ function hasNonTranscriptRecentChat(context) {
6244
6256
  const recentChat = context.recentChatMessages ?? [];
6245
6257
  return recentChat.length > 0 && !recentChat.some((message) => message.source_kind === "transcript");
6246
6258
  }
@@ -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.29",
3085
+ client_version: "0.4.30",
3086
3086
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3087
3087
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3088
3088
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -4216,6 +4216,22 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4216
4216
  }
4217
4217
 
4218
4218
  // src/tools/recent-chat.ts
4219
+ function summarizeChatSources(messages) {
4220
+ return messages.reduce((summary, message) => {
4221
+ summary[getChatCaptureOrigin(message)] += 1;
4222
+ return summary;
4223
+ }, { transcript: 0, history: 0, hook: 0 });
4224
+ }
4225
+ function getChatCoverageState(messagesOrSummary) {
4226
+ const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
4227
+ if (summary.transcript > 0)
4228
+ return "transcript-backed";
4229
+ if (summary.history > 0)
4230
+ return "history-backed";
4231
+ if (summary.hook > 0)
4232
+ return "hook-only";
4233
+ return "none";
4234
+ }
4219
4235
  function getChatCaptureOrigin(message) {
4220
4236
  if (message.source_kind === "transcript")
4221
4237
  return "transcript";
@@ -4247,7 +4263,7 @@ function getSessionStory(db, input) {
4247
4263
  prompts,
4248
4264
  chat_messages: chatMessages,
4249
4265
  chat_source_summary: summarizeChatSources(chatMessages),
4250
- chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
4266
+ chat_coverage_state: getChatCoverageState(chatMessages),
4251
4267
  tool_events: toolEvents,
4252
4268
  observations,
4253
4269
  handoffs,
@@ -4345,12 +4361,6 @@ function collectProvenanceSummary(observations) {
4345
4361
  }
4346
4362
  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);
4347
4363
  }
4348
- function summarizeChatSources(messages) {
4349
- return messages.reduce((summary, message) => {
4350
- summary[getChatCaptureOrigin(message)] += 1;
4351
- return summary;
4352
- }, { transcript: 0, history: 0, hook: 0 });
4353
- }
4354
4364
 
4355
4365
  // src/tools/handoffs.ts
4356
4366
  async function upsertRollingHandoff(db, config, input) {
@@ -3100,6 +3100,22 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
3100
3100
  }
3101
3101
 
3102
3102
  // src/tools/recent-chat.ts
3103
+ function summarizeChatSources(messages) {
3104
+ return messages.reduce((summary, message) => {
3105
+ summary[getChatCaptureOrigin(message)] += 1;
3106
+ return summary;
3107
+ }, { transcript: 0, history: 0, hook: 0 });
3108
+ }
3109
+ function getChatCoverageState(messagesOrSummary) {
3110
+ const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
3111
+ if (summary.transcript > 0)
3112
+ return "transcript-backed";
3113
+ if (summary.history > 0)
3114
+ return "history-backed";
3115
+ if (summary.hook > 0)
3116
+ return "hook-only";
3117
+ return "none";
3118
+ }
3103
3119
  function getChatCaptureOrigin(message) {
3104
3120
  if (message.source_kind === "transcript")
3105
3121
  return "transcript";
@@ -3131,7 +3147,7 @@ function getSessionStory(db, input) {
3131
3147
  prompts,
3132
3148
  chat_messages: chatMessages,
3133
3149
  chat_source_summary: summarizeChatSources(chatMessages),
3134
- chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
3150
+ chat_coverage_state: getChatCoverageState(chatMessages),
3135
3151
  tool_events: toolEvents,
3136
3152
  observations,
3137
3153
  handoffs,
@@ -3229,12 +3245,6 @@ function collectProvenanceSummary(observations) {
3229
3245
  }
3230
3246
  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);
3231
3247
  }
3232
- function summarizeChatSources(messages) {
3233
- return messages.reduce((summary, message) => {
3234
- summary[getChatCaptureOrigin(message)] += 1;
3235
- return summary;
3236
- }, { transcript: 0, history: 0, hook: 0 });
3237
- }
3238
3248
 
3239
3249
  // src/tools/handoffs.ts
3240
3250
  async function upsertRollingHandoff(db, config, input) {