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 +18 -4
- package/dist/hooks/post-tool-use.js +17 -7
- package/dist/hooks/pre-compact.js +17 -7
- package/dist/hooks/session-start.js +22 -10
- package/dist/hooks/stop.js +18 -8
- package/dist/hooks/user-prompt-submit.js +17 -7
- package/dist/server.js +711 -445
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
}
|
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.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
|
|
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
|
|
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) {
|