engrm 0.4.26 → 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 +3 -0
- package/dist/hooks/post-tool-use.js +8 -0
- package/dist/hooks/pre-compact.js +8 -0
- package/dist/hooks/session-start.js +16 -2
- package/dist/hooks/stop.js +9 -1
- package/dist/hooks/user-prompt-submit.js +8 -0
- package/dist/server.js +242 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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 |
|
|
@@ -359,6 +360,8 @@ What each tool is good for:
|
|
|
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
|
|
361
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`
|
|
362
365
|
- `activity_feed` shows the merged chronology across prompts, tools, chat, handoffs, observations, and summaries
|
|
363
366
|
- `recent_sessions` helps you pick a session worth opening
|
|
364
367
|
- `session_story` reconstructs one session in detail, including handoffs and chat recall
|
|
@@ -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";
|
|
@@ -494,6 +494,8 @@ function getSessionStory(db, input) {
|
|
|
494
494
|
summary,
|
|
495
495
|
prompts,
|
|
496
496
|
chat_messages: chatMessages,
|
|
497
|
+
chat_source_summary: summarizeChatSources(chatMessages),
|
|
498
|
+
chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
|
|
497
499
|
tool_events: toolEvents,
|
|
498
500
|
observations,
|
|
499
501
|
handoffs,
|
|
@@ -591,6 +593,12 @@ function collectProvenanceSummary(observations) {
|
|
|
591
593
|
}
|
|
592
594
|
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);
|
|
593
595
|
}
|
|
596
|
+
function summarizeChatSources(messages) {
|
|
597
|
+
return messages.reduce((summary, message) => {
|
|
598
|
+
summary[message.source_kind] += 1;
|
|
599
|
+
return summary;
|
|
600
|
+
}, { transcript: 0, hook: 0 });
|
|
601
|
+
}
|
|
594
602
|
|
|
595
603
|
// src/tools/save.ts
|
|
596
604
|
import { relative, isAbsolute } from "node:path";
|
|
@@ -3049,7 +3057,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
3049
3057
|
import { join as join3 } from "node:path";
|
|
3050
3058
|
import { homedir } from "node:os";
|
|
3051
3059
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
3052
|
-
var CLIENT_VERSION = "0.4.
|
|
3060
|
+
var CLIENT_VERSION = "0.4.27";
|
|
3053
3061
|
function hashFile(filePath) {
|
|
3054
3062
|
try {
|
|
3055
3063
|
if (!existsSync3(filePath))
|
|
@@ -5671,10 +5679,12 @@ function formatInspectHints(context, visibleObservationIds = []) {
|
|
|
5671
5679
|
if ((context.recentChatMessages?.length ?? 0) > 0) {
|
|
5672
5680
|
hints.push("recent_chat");
|
|
5673
5681
|
}
|
|
5682
|
+
if (hasHookOnlyRecentChat(context)) {
|
|
5683
|
+
hints.push("refresh_chat_recall");
|
|
5684
|
+
}
|
|
5674
5685
|
if (continuityState !== "fresh") {
|
|
5675
5686
|
hints.push("recent_chat");
|
|
5676
5687
|
hints.push("recent_handoffs");
|
|
5677
|
-
hints.push("refresh_chat_recall");
|
|
5678
5688
|
}
|
|
5679
5689
|
const unique = Array.from(new Set(hints)).slice(0, 4);
|
|
5680
5690
|
if (unique.length === 0)
|
|
@@ -6144,6 +6154,10 @@ function hasFreshContinuitySignal(context) {
|
|
|
6144
6154
|
function getStartupContinuityState(context) {
|
|
6145
6155
|
return classifyContinuityState(context.recentPrompts?.length ?? 0, context.recentToolEvents?.length ?? 0, context.recentHandoffs?.length ?? 0, context.recentChatMessages?.length ?? 0, context.recentSessions ?? [], context.recentOutcomes?.length ?? 0);
|
|
6146
6156
|
}
|
|
6157
|
+
function hasHookOnlyRecentChat(context) {
|
|
6158
|
+
const recentChat = context.recentChatMessages ?? [];
|
|
6159
|
+
return recentChat.length > 0 && !recentChat.some((message) => message.source_kind === "transcript");
|
|
6160
|
+
}
|
|
6147
6161
|
function observationAgeDays3(obs) {
|
|
6148
6162
|
const createdAt = new Date(obs.created_at).getTime();
|
|
6149
6163
|
if (!Number.isFinite(createdAt))
|
package/dist/hooks/stop.js
CHANGED
|
@@ -3009,7 +3009,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
3009
3009
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
3010
3010
|
risk_score: riskScore,
|
|
3011
3011
|
stacks_detected: stacks,
|
|
3012
|
-
client_version: "0.4.
|
|
3012
|
+
client_version: "0.4.27",
|
|
3013
3013
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3014
3014
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3015
3015
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -4040,6 +4040,8 @@ function getSessionStory(db, input) {
|
|
|
4040
4040
|
summary,
|
|
4041
4041
|
prompts,
|
|
4042
4042
|
chat_messages: chatMessages,
|
|
4043
|
+
chat_source_summary: summarizeChatSources(chatMessages),
|
|
4044
|
+
chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
|
|
4043
4045
|
tool_events: toolEvents,
|
|
4044
4046
|
observations,
|
|
4045
4047
|
handoffs,
|
|
@@ -4137,6 +4139,12 @@ function collectProvenanceSummary(observations) {
|
|
|
4137
4139
|
}
|
|
4138
4140
|
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);
|
|
4139
4141
|
}
|
|
4142
|
+
function summarizeChatSources(messages) {
|
|
4143
|
+
return messages.reduce((summary, message) => {
|
|
4144
|
+
summary[message.source_kind] += 1;
|
|
4145
|
+
return summary;
|
|
4146
|
+
}, { transcript: 0, hook: 0 });
|
|
4147
|
+
}
|
|
4140
4148
|
|
|
4141
4149
|
// src/tools/handoffs.ts
|
|
4142
4150
|
async function upsertRollingHandoff(db, config, input) {
|
|
@@ -2924,6 +2924,8 @@ function getSessionStory(db, input) {
|
|
|
2924
2924
|
summary,
|
|
2925
2925
|
prompts,
|
|
2926
2926
|
chat_messages: chatMessages,
|
|
2927
|
+
chat_source_summary: summarizeChatSources(chatMessages),
|
|
2928
|
+
chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
|
|
2927
2929
|
tool_events: toolEvents,
|
|
2928
2930
|
observations,
|
|
2929
2931
|
handoffs,
|
|
@@ -3021,6 +3023,12 @@ function collectProvenanceSummary(observations) {
|
|
|
3021
3023
|
}
|
|
3022
3024
|
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);
|
|
3023
3025
|
}
|
|
3026
|
+
function summarizeChatSources(messages) {
|
|
3027
|
+
return messages.reduce((summary, message) => {
|
|
3028
|
+
summary[message.source_kind] += 1;
|
|
3029
|
+
return summary;
|
|
3030
|
+
}, { transcript: 0, hook: 0 });
|
|
3031
|
+
}
|
|
3024
3032
|
|
|
3025
3033
|
// src/tools/handoffs.ts
|
|
3026
3034
|
async function upsertRollingHandoff(db, config, input) {
|
package/dist/server.js
CHANGED
|
@@ -16321,6 +16321,139 @@ function sanitizeFtsQuery(query) {
|
|
|
16321
16321
|
return safe;
|
|
16322
16322
|
}
|
|
16323
16323
|
|
|
16324
|
+
// src/tools/search-chat.ts
|
|
16325
|
+
function searchChat(db, input) {
|
|
16326
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16327
|
+
const projectScoped = input.project_scoped !== false;
|
|
16328
|
+
let projectId = null;
|
|
16329
|
+
let projectName;
|
|
16330
|
+
if (projectScoped) {
|
|
16331
|
+
const cwd = input.cwd ?? process.cwd();
|
|
16332
|
+
const detected = detectProject(cwd);
|
|
16333
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16334
|
+
if (project) {
|
|
16335
|
+
projectId = project.id;
|
|
16336
|
+
projectName = project.name;
|
|
16337
|
+
}
|
|
16338
|
+
}
|
|
16339
|
+
const messages = db.searchChatMessages(input.query, projectId, limit, input.user_id);
|
|
16340
|
+
return {
|
|
16341
|
+
messages,
|
|
16342
|
+
project: projectName,
|
|
16343
|
+
session_count: countDistinctSessions(messages),
|
|
16344
|
+
source_summary: summarizeChatSources(messages),
|
|
16345
|
+
transcript_backed: messages.some((message) => message.source_kind === "transcript")
|
|
16346
|
+
};
|
|
16347
|
+
}
|
|
16348
|
+
function summarizeChatSources(messages) {
|
|
16349
|
+
return messages.reduce((summary, message) => {
|
|
16350
|
+
summary[message.source_kind] += 1;
|
|
16351
|
+
return summary;
|
|
16352
|
+
}, { transcript: 0, hook: 0 });
|
|
16353
|
+
}
|
|
16354
|
+
function countDistinctSessions(messages) {
|
|
16355
|
+
return new Set(messages.map((message) => message.session_id)).size;
|
|
16356
|
+
}
|
|
16357
|
+
|
|
16358
|
+
// src/tools/search-recall.ts
|
|
16359
|
+
async function searchRecall(db, input) {
|
|
16360
|
+
const query = input.query.trim();
|
|
16361
|
+
if (!query) {
|
|
16362
|
+
return {
|
|
16363
|
+
query,
|
|
16364
|
+
results: [],
|
|
16365
|
+
totals: { memory: 0, chat: 0 }
|
|
16366
|
+
};
|
|
16367
|
+
}
|
|
16368
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
|
|
16369
|
+
const [memory, chat] = await Promise.all([
|
|
16370
|
+
searchObservations(db, input),
|
|
16371
|
+
Promise.resolve(searchChat(db, {
|
|
16372
|
+
query,
|
|
16373
|
+
limit: limit * 2,
|
|
16374
|
+
project_scoped: input.project_scoped,
|
|
16375
|
+
cwd: input.cwd,
|
|
16376
|
+
user_id: input.user_id
|
|
16377
|
+
}))
|
|
16378
|
+
]);
|
|
16379
|
+
const merged = mergeRecallResults(memory.observations, chat.messages, limit);
|
|
16380
|
+
return {
|
|
16381
|
+
query,
|
|
16382
|
+
project: memory.project ?? chat.project,
|
|
16383
|
+
results: merged,
|
|
16384
|
+
totals: {
|
|
16385
|
+
memory: memory.total,
|
|
16386
|
+
chat: chat.messages.length
|
|
16387
|
+
}
|
|
16388
|
+
};
|
|
16389
|
+
}
|
|
16390
|
+
function mergeRecallResults(memory, chat, limit) {
|
|
16391
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
16392
|
+
const scored = [];
|
|
16393
|
+
for (let index = 0;index < memory.length; index++) {
|
|
16394
|
+
const item = memory[index];
|
|
16395
|
+
const base = 1 / (60 + index + 1);
|
|
16396
|
+
const score = base + Math.max(0, item.rank) * 0.08;
|
|
16397
|
+
scored.push({
|
|
16398
|
+
kind: "memory",
|
|
16399
|
+
rank: score,
|
|
16400
|
+
created_at: item.created_at,
|
|
16401
|
+
created_at_epoch: Math.floor(new Date(item.created_at).getTime() / 1000) || undefined,
|
|
16402
|
+
project_name: item.project_name,
|
|
16403
|
+
observation_id: item.id,
|
|
16404
|
+
id: item.id,
|
|
16405
|
+
session_id: null,
|
|
16406
|
+
type: item.type,
|
|
16407
|
+
title: item.title,
|
|
16408
|
+
detail: firstNonEmpty(item.narrative, parseFactsPreview(item.facts), item.files_modified ? `Files: ${item.files_modified}` : null, item.type) ?? item.type
|
|
16409
|
+
});
|
|
16410
|
+
}
|
|
16411
|
+
for (let index = 0;index < chat.length; index++) {
|
|
16412
|
+
const item = chat[index];
|
|
16413
|
+
const base = 1 / (60 + index + 1);
|
|
16414
|
+
const ageHours = Math.max(0, (nowEpoch - item.created_at_epoch) / 3600);
|
|
16415
|
+
const immediacyBoost = ageHours < 1 ? 1 : 0;
|
|
16416
|
+
const recencyBoost = ageHours < 24 ? 0.12 : ageHours < 72 ? 0.05 : 0.02;
|
|
16417
|
+
const sourceBoost = item.source_kind === "transcript" ? 0.06 : 0.03;
|
|
16418
|
+
scored.push({
|
|
16419
|
+
kind: "chat",
|
|
16420
|
+
rank: base + immediacyBoost + recencyBoost + sourceBoost,
|
|
16421
|
+
created_at_epoch: item.created_at_epoch,
|
|
16422
|
+
session_id: item.session_id,
|
|
16423
|
+
id: item.id,
|
|
16424
|
+
role: item.role,
|
|
16425
|
+
source_kind: item.source_kind,
|
|
16426
|
+
title: `${item.role} [${item.source_kind}]`,
|
|
16427
|
+
detail: item.content.replace(/\s+/g, " ").trim()
|
|
16428
|
+
});
|
|
16429
|
+
}
|
|
16430
|
+
return scored.sort((a, b) => {
|
|
16431
|
+
if (b.rank !== a.rank)
|
|
16432
|
+
return b.rank - a.rank;
|
|
16433
|
+
return (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0);
|
|
16434
|
+
}).slice(0, limit);
|
|
16435
|
+
}
|
|
16436
|
+
function parseFactsPreview(facts) {
|
|
16437
|
+
if (!facts)
|
|
16438
|
+
return null;
|
|
16439
|
+
try {
|
|
16440
|
+
const parsed = JSON.parse(facts);
|
|
16441
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
16442
|
+
return null;
|
|
16443
|
+
const lines = parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
16444
|
+
return lines.length > 0 ? lines.slice(0, 2).join(" | ") : null;
|
|
16445
|
+
} catch {
|
|
16446
|
+
return facts;
|
|
16447
|
+
}
|
|
16448
|
+
}
|
|
16449
|
+
function firstNonEmpty(...values) {
|
|
16450
|
+
for (const value of values) {
|
|
16451
|
+
if (value && value.trim().length > 0)
|
|
16452
|
+
return value.trim();
|
|
16453
|
+
}
|
|
16454
|
+
return null;
|
|
16455
|
+
}
|
|
16456
|
+
|
|
16324
16457
|
// src/tools/get.ts
|
|
16325
16458
|
function getObservations(db, input) {
|
|
16326
16459
|
if (input.ids.length === 0) {
|
|
@@ -16461,8 +16594,8 @@ function getRecentChat(db, input) {
|
|
|
16461
16594
|
const messages2 = db.getSessionChatMessages(input.session_id, limit).slice(-limit).reverse();
|
|
16462
16595
|
return {
|
|
16463
16596
|
messages: messages2,
|
|
16464
|
-
session_count:
|
|
16465
|
-
source_summary:
|
|
16597
|
+
session_count: countDistinctSessions2(messages2),
|
|
16598
|
+
source_summary: summarizeChatSources2(messages2),
|
|
16466
16599
|
transcript_backed: messages2.some((message) => message.source_kind === "transcript")
|
|
16467
16600
|
};
|
|
16468
16601
|
}
|
|
@@ -16479,40 +16612,6 @@ function getRecentChat(db, input) {
|
|
|
16479
16612
|
}
|
|
16480
16613
|
}
|
|
16481
16614
|
const messages = db.getRecentChatMessages(projectId, limit, input.user_id);
|
|
16482
|
-
return {
|
|
16483
|
-
messages,
|
|
16484
|
-
project: projectName,
|
|
16485
|
-
session_count: countDistinctSessions(messages),
|
|
16486
|
-
source_summary: summarizeChatSources(messages),
|
|
16487
|
-
transcript_backed: messages.some((message) => message.source_kind === "transcript")
|
|
16488
|
-
};
|
|
16489
|
-
}
|
|
16490
|
-
function summarizeChatSources(messages) {
|
|
16491
|
-
return messages.reduce((summary, message) => {
|
|
16492
|
-
summary[message.source_kind] += 1;
|
|
16493
|
-
return summary;
|
|
16494
|
-
}, { transcript: 0, hook: 0 });
|
|
16495
|
-
}
|
|
16496
|
-
function countDistinctSessions(messages) {
|
|
16497
|
-
return new Set(messages.map((message) => message.session_id)).size;
|
|
16498
|
-
}
|
|
16499
|
-
|
|
16500
|
-
// src/tools/search-chat.ts
|
|
16501
|
-
function searchChat(db, input) {
|
|
16502
|
-
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16503
|
-
const projectScoped = input.project_scoped !== false;
|
|
16504
|
-
let projectId = null;
|
|
16505
|
-
let projectName;
|
|
16506
|
-
if (projectScoped) {
|
|
16507
|
-
const cwd = input.cwd ?? process.cwd();
|
|
16508
|
-
const detected = detectProject(cwd);
|
|
16509
|
-
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16510
|
-
if (project) {
|
|
16511
|
-
projectId = project.id;
|
|
16512
|
-
projectName = project.name;
|
|
16513
|
-
}
|
|
16514
|
-
}
|
|
16515
|
-
const messages = db.searchChatMessages(input.query, projectId, limit, input.user_id);
|
|
16516
16615
|
return {
|
|
16517
16616
|
messages,
|
|
16518
16617
|
project: projectName,
|
|
@@ -16552,6 +16651,8 @@ function getSessionStory(db, input) {
|
|
|
16552
16651
|
summary,
|
|
16553
16652
|
prompts,
|
|
16554
16653
|
chat_messages: chatMessages,
|
|
16654
|
+
chat_source_summary: summarizeChatSources3(chatMessages),
|
|
16655
|
+
chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
|
|
16555
16656
|
tool_events: toolEvents,
|
|
16556
16657
|
observations,
|
|
16557
16658
|
handoffs,
|
|
@@ -16649,6 +16750,12 @@ function collectProvenanceSummary(observations) {
|
|
|
16649
16750
|
}
|
|
16650
16751
|
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);
|
|
16651
16752
|
}
|
|
16753
|
+
function summarizeChatSources3(messages) {
|
|
16754
|
+
return messages.reduce((summary, message) => {
|
|
16755
|
+
summary[message.source_kind] += 1;
|
|
16756
|
+
return summary;
|
|
16757
|
+
}, { transcript: 0, hook: 0 });
|
|
16758
|
+
}
|
|
16652
16759
|
|
|
16653
16760
|
// src/tools/handoffs.ts
|
|
16654
16761
|
async function createHandoff(db, config2, input) {
|
|
@@ -17995,16 +18102,17 @@ function getProjectMemoryIndex(db, input) {
|
|
|
17995
18102
|
}).handoffs;
|
|
17996
18103
|
const rollingHandoffDraftsCount = recentHandoffsCount.filter((handoff) => isDraftHandoff(handoff)).length;
|
|
17997
18104
|
const savedHandoffsCount = recentHandoffsCount.length - rollingHandoffDraftsCount;
|
|
17998
|
-
const
|
|
18105
|
+
const recentChat = getRecentChat(db, {
|
|
17999
18106
|
cwd,
|
|
18000
18107
|
project_scoped: true,
|
|
18001
18108
|
user_id: input.user_id,
|
|
18002
18109
|
limit: 20
|
|
18003
|
-
})
|
|
18110
|
+
});
|
|
18111
|
+
const recentChatCount = recentChat.messages.length;
|
|
18004
18112
|
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !looksLikeFileOperationTitle3(title)).slice(0, 8);
|
|
18005
18113
|
const captureSummary = summarizeCaptureState(recentSessions);
|
|
18006
18114
|
const topTypes = Object.entries(counts).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
|
|
18007
|
-
const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length);
|
|
18115
|
+
const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.transcript_backed);
|
|
18008
18116
|
const estimatedReadTokens = estimateTokens([
|
|
18009
18117
|
recentOutcomes.join(`
|
|
18010
18118
|
`),
|
|
@@ -18029,6 +18137,9 @@ function getProjectMemoryIndex(db, input) {
|
|
|
18029
18137
|
rolling_handoff_drafts_count: rollingHandoffDraftsCount,
|
|
18030
18138
|
saved_handoffs_count: savedHandoffsCount,
|
|
18031
18139
|
recent_chat_count: recentChatCount,
|
|
18140
|
+
recent_chat_sessions: recentChat.session_count,
|
|
18141
|
+
chat_source_summary: recentChat.source_summary,
|
|
18142
|
+
chat_coverage_state: recentChat.transcript_backed ? "transcript-backed" : recentChatCount > 0 ? "hook-only" : "none",
|
|
18032
18143
|
raw_capture_active: recentRequestsCount > 0 || recentToolsCount > 0,
|
|
18033
18144
|
capture_summary: captureSummary,
|
|
18034
18145
|
hot_files: hotFiles,
|
|
@@ -18101,7 +18212,7 @@ function summarizeCaptureState(sessions) {
|
|
|
18101
18212
|
}
|
|
18102
18213
|
return summary;
|
|
18103
18214
|
}
|
|
18104
|
-
function buildSuggestedTools(sessions, requestCount, toolCount, observationCount) {
|
|
18215
|
+
function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, transcriptBackedChat) {
|
|
18105
18216
|
const suggested = [];
|
|
18106
18217
|
if (sessions.length > 0) {
|
|
18107
18218
|
suggested.push("recent_sessions");
|
|
@@ -18115,7 +18226,12 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
|
|
|
18115
18226
|
if (sessions.length > 0) {
|
|
18116
18227
|
suggested.push("create_handoff", "recent_handoffs");
|
|
18117
18228
|
}
|
|
18118
|
-
|
|
18229
|
+
if (recentChatCount > 0 && !transcriptBackedChat) {
|
|
18230
|
+
suggested.push("refresh_chat_recall");
|
|
18231
|
+
}
|
|
18232
|
+
if (recentChatCount > 0) {
|
|
18233
|
+
suggested.push("recent_chat", "search_chat");
|
|
18234
|
+
}
|
|
18119
18235
|
return Array.from(new Set(suggested)).slice(0, 4);
|
|
18120
18236
|
}
|
|
18121
18237
|
|
|
@@ -18162,12 +18278,12 @@ function getMemoryConsole(db, input) {
|
|
|
18162
18278
|
project_scoped: projectScoped,
|
|
18163
18279
|
user_id: input.user_id,
|
|
18164
18280
|
limit: 6
|
|
18165
|
-
})
|
|
18281
|
+
});
|
|
18166
18282
|
const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
|
|
18167
18283
|
cwd,
|
|
18168
18284
|
user_id: input.user_id
|
|
18169
18285
|
}) : null;
|
|
18170
|
-
const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
|
|
18286
|
+
const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.messages.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
|
|
18171
18287
|
return {
|
|
18172
18288
|
project: project?.name,
|
|
18173
18289
|
capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
|
|
@@ -18179,7 +18295,10 @@ function getMemoryConsole(db, input) {
|
|
|
18179
18295
|
recent_handoffs: recentHandoffs,
|
|
18180
18296
|
rolling_handoff_drafts: rollingHandoffDrafts,
|
|
18181
18297
|
saved_handoffs: savedHandoffs,
|
|
18182
|
-
recent_chat: recentChat,
|
|
18298
|
+
recent_chat: recentChat.messages,
|
|
18299
|
+
recent_chat_sessions: projectIndex?.recent_chat_sessions ?? recentChat.session_count,
|
|
18300
|
+
chat_source_summary: projectIndex?.chat_source_summary ?? recentChat.source_summary,
|
|
18301
|
+
chat_coverage_state: projectIndex?.chat_coverage_state ?? (recentChat.transcript_backed ? "transcript-backed" : recentChat.messages.length > 0 ? "hook-only" : "none"),
|
|
18183
18302
|
observations,
|
|
18184
18303
|
capture_summary: projectIndex?.capture_summary,
|
|
18185
18304
|
recent_outcomes: projectIndex?.recent_outcomes ?? [],
|
|
@@ -18189,10 +18308,10 @@ function getMemoryConsole(db, input) {
|
|
|
18189
18308
|
assistant_checkpoint_types: projectIndex?.assistant_checkpoint_types ?? [],
|
|
18190
18309
|
top_types: projectIndex?.top_types ?? [],
|
|
18191
18310
|
estimated_read_tokens: projectIndex?.estimated_read_tokens,
|
|
18192
|
-
suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.length)
|
|
18311
|
+
suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.transcript_backed)
|
|
18193
18312
|
};
|
|
18194
18313
|
}
|
|
18195
|
-
function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount) {
|
|
18314
|
+
function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, transcriptBackedChat) {
|
|
18196
18315
|
const suggested = [];
|
|
18197
18316
|
if (sessionCount > 0)
|
|
18198
18317
|
suggested.push("recent_sessions");
|
|
@@ -18204,8 +18323,10 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
|
|
|
18204
18323
|
suggested.push("create_handoff", "recent_handoffs");
|
|
18205
18324
|
if (handoffCount > 0)
|
|
18206
18325
|
suggested.push("load_handoff");
|
|
18326
|
+
if (chatCount > 0 && !transcriptBackedChat)
|
|
18327
|
+
suggested.push("refresh_chat_recall");
|
|
18207
18328
|
if (chatCount > 0)
|
|
18208
|
-
suggested.push("recent_chat");
|
|
18329
|
+
suggested.push("recent_chat", "search_chat");
|
|
18209
18330
|
return Array.from(new Set(suggested)).slice(0, 4);
|
|
18210
18331
|
}
|
|
18211
18332
|
|
|
@@ -18428,7 +18549,7 @@ function toChatEvent(message) {
|
|
|
18428
18549
|
created_at_epoch: message.created_at_epoch,
|
|
18429
18550
|
session_id: message.session_id,
|
|
18430
18551
|
id: message.id,
|
|
18431
|
-
title: message.role
|
|
18552
|
+
title: `${message.role} [${message.source_kind}]`,
|
|
18432
18553
|
detail: content.slice(0, 220)
|
|
18433
18554
|
};
|
|
18434
18555
|
}
|
|
@@ -19007,12 +19128,13 @@ function getSessionContext(db, input) {
|
|
|
19007
19128
|
const rollingHandoffDrafts = (context.recentHandoffs ?? []).filter((handoff) => handoff.title.startsWith("Handoff Draft:")).length;
|
|
19008
19129
|
const savedHandoffs = recentHandoffs - rollingHandoffDrafts;
|
|
19009
19130
|
const latestHandoffTitle = context.recentHandoffs?.[0]?.title ?? null;
|
|
19010
|
-
const
|
|
19131
|
+
const recentChat = getRecentChat(db, {
|
|
19011
19132
|
cwd,
|
|
19012
19133
|
project_scoped: true,
|
|
19013
19134
|
user_id: input.user_id,
|
|
19014
19135
|
limit: 8
|
|
19015
|
-
})
|
|
19136
|
+
});
|
|
19137
|
+
const recentChatMessages = recentChat.messages.length;
|
|
19016
19138
|
const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
|
|
19017
19139
|
const hotFiles = buildHotFiles(context);
|
|
19018
19140
|
const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
|
|
@@ -19031,12 +19153,15 @@ function getSessionContext(db, input) {
|
|
|
19031
19153
|
saved_handoffs: savedHandoffs,
|
|
19032
19154
|
latest_handoff_title: latestHandoffTitle,
|
|
19033
19155
|
recent_chat_messages: recentChatMessages,
|
|
19156
|
+
recent_chat_sessions: recentChat.session_count,
|
|
19157
|
+
chat_source_summary: recentChat.source_summary,
|
|
19158
|
+
chat_coverage_state: recentChat.transcript_backed ? "transcript-backed" : recentChatMessages > 0 ? "hook-only" : "none",
|
|
19034
19159
|
recent_outcomes: context.recentOutcomes ?? [],
|
|
19035
19160
|
hot_files: hotFiles,
|
|
19036
19161
|
capture_state: captureState,
|
|
19037
19162
|
raw_capture_active: recentRequests > 0 || recentTools > 0,
|
|
19038
19163
|
estimated_read_tokens: estimateTokens(preview),
|
|
19039
|
-
suggested_tools: buildSuggestedTools2(context),
|
|
19164
|
+
suggested_tools: buildSuggestedTools2(context, recentChat.transcript_backed),
|
|
19040
19165
|
preview
|
|
19041
19166
|
};
|
|
19042
19167
|
}
|
|
@@ -19059,7 +19184,7 @@ function parseJsonArray3(value) {
|
|
|
19059
19184
|
return [];
|
|
19060
19185
|
}
|
|
19061
19186
|
}
|
|
19062
|
-
function buildSuggestedTools2(context) {
|
|
19187
|
+
function buildSuggestedTools2(context, transcriptBackedChat) {
|
|
19063
19188
|
const tools = [];
|
|
19064
19189
|
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
19065
19190
|
tools.push("recent_sessions");
|
|
@@ -19076,7 +19201,12 @@ function buildSuggestedTools2(context) {
|
|
|
19076
19201
|
if ((context.recentHandoffs?.length ?? 0) > 0) {
|
|
19077
19202
|
tools.push("load_handoff");
|
|
19078
19203
|
}
|
|
19079
|
-
|
|
19204
|
+
if ((context.recentChatMessages?.length ?? 0) > 0 && !transcriptBackedChat) {
|
|
19205
|
+
tools.push("refresh_chat_recall");
|
|
19206
|
+
}
|
|
19207
|
+
if ((context.recentChatMessages?.length ?? 0) > 0) {
|
|
19208
|
+
tools.push("recent_chat", "search_chat");
|
|
19209
|
+
}
|
|
19080
19210
|
return Array.from(new Set(tools)).slice(0, 4);
|
|
19081
19211
|
}
|
|
19082
19212
|
|
|
@@ -21152,7 +21282,7 @@ process.on("SIGTERM", () => {
|
|
|
21152
21282
|
});
|
|
21153
21283
|
var server = new McpServer({
|
|
21154
21284
|
name: "engrm",
|
|
21155
|
-
version: "0.4.
|
|
21285
|
+
version: "0.4.27"
|
|
21156
21286
|
});
|
|
21157
21287
|
server.tool("save_observation", "Save an observation to memory", {
|
|
21158
21288
|
type: exports_external.enum([
|
|
@@ -21545,6 +21675,58 @@ ${previews.join(`
|
|
|
21545
21675
|
]
|
|
21546
21676
|
};
|
|
21547
21677
|
});
|
|
21678
|
+
server.tool("search_recall", "Search live recall across durable memory and chat together. Best for questions like 'what were we just talking about?'", {
|
|
21679
|
+
query: exports_external.string().describe("Recall query"),
|
|
21680
|
+
project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
|
|
21681
|
+
limit: exports_external.number().optional().describe("Max results (default: 10)"),
|
|
21682
|
+
cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
|
|
21683
|
+
user_id: exports_external.string().optional().describe("Optional user override")
|
|
21684
|
+
}, async (params) => {
|
|
21685
|
+
const result = await searchRecall(db, {
|
|
21686
|
+
query: params.query,
|
|
21687
|
+
project_scoped: params.project_scoped,
|
|
21688
|
+
limit: params.limit,
|
|
21689
|
+
cwd: params.cwd,
|
|
21690
|
+
user_id: params.user_id ?? config2.user_id
|
|
21691
|
+
});
|
|
21692
|
+
if (result.results.length === 0) {
|
|
21693
|
+
return {
|
|
21694
|
+
content: [
|
|
21695
|
+
{
|
|
21696
|
+
type: "text",
|
|
21697
|
+
text: result.project ? `No recall found for "${params.query}" in project ${result.project}` : `No recall found for "${params.query}"`
|
|
21698
|
+
}
|
|
21699
|
+
]
|
|
21700
|
+
};
|
|
21701
|
+
}
|
|
21702
|
+
const projectLine = result.project ? `Project: ${result.project}
|
|
21703
|
+
` : "";
|
|
21704
|
+
const summaryLine = `Matches: ${result.results.length} · memory ${result.totals.memory} · chat ${result.totals.chat}
|
|
21705
|
+
`;
|
|
21706
|
+
const rows = result.results.map((item) => {
|
|
21707
|
+
const sourceBits = [item.kind];
|
|
21708
|
+
if (item.type)
|
|
21709
|
+
sourceBits.push(item.type);
|
|
21710
|
+
if (item.role)
|
|
21711
|
+
sourceBits.push(item.role);
|
|
21712
|
+
if (item.source_kind)
|
|
21713
|
+
sourceBits.push(item.source_kind);
|
|
21714
|
+
const idBit = item.observation_id ? `#${item.observation_id}` : item.id ? `chat:${item.id}` : "";
|
|
21715
|
+
const title = `${idBit ? `${idBit} ` : ""}${item.title}${item.project_name ? ` (${item.project_name})` : ""}`;
|
|
21716
|
+
return `- [${sourceBits.join(" · ")}] ${title}
|
|
21717
|
+
${item.detail.slice(0, 220)}`;
|
|
21718
|
+
}).join(`
|
|
21719
|
+
`);
|
|
21720
|
+
return {
|
|
21721
|
+
content: [
|
|
21722
|
+
{
|
|
21723
|
+
type: "text",
|
|
21724
|
+
text: `${projectLine}${summaryLine}Recall search for "${params.query}":
|
|
21725
|
+
${rows}`
|
|
21726
|
+
}
|
|
21727
|
+
]
|
|
21728
|
+
};
|
|
21729
|
+
});
|
|
21548
21730
|
server.tool("get_observations", "Get observations by ID", {
|
|
21549
21731
|
ids: exports_external.array(exports_external.number()).describe("Observation IDs")
|
|
21550
21732
|
}, async (params) => {
|
|
@@ -21865,6 +22047,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
21865
22047
|
{
|
|
21866
22048
|
type: "text",
|
|
21867
22049
|
text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
|
|
22050
|
+
` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat.length} messages across ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
|
|
21868
22051
|
` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
|
|
21869
22052
|
` : ""}` + `Handoffs: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
|
|
21870
22053
|
` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
|
|
@@ -22071,6 +22254,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
|
|
|
22071
22254
|
` + `Recent handoffs: ${result.recent_handoffs}
|
|
22072
22255
|
` + `Handoff split: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
|
|
22073
22256
|
` + `Recent chat messages: ${result.recent_chat_messages}
|
|
22257
|
+
` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
|
|
22074
22258
|
` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
|
|
22075
22259
|
` + `Raw chronology active: ${result.raw_capture_active ? "yes" : "no"}
|
|
22076
22260
|
|
|
@@ -22150,6 +22334,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
22150
22334
|
` + `Recent handoffs captured: ${result.recent_handoffs_count}
|
|
22151
22335
|
` + `Handoff split: ${result.saved_handoffs_count} saved, ${result.rolling_handoff_drafts_count} rolling drafts
|
|
22152
22336
|
` + `Recent chat messages captured: ${result.recent_chat_count}
|
|
22337
|
+
` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
|
|
22153
22338
|
|
|
22154
22339
|
` + `Raw chronology: ${result.raw_capture_active ? "active" : "observations-only so far"}
|
|
22155
22340
|
|
|
@@ -22554,7 +22739,7 @@ server.tool("session_story", "Show the full local memory story for one session",
|
|
|
22554
22739
|
`) : "(none)";
|
|
22555
22740
|
const promptLines = result.prompts.length > 0 ? result.prompts.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
|
|
22556
22741
|
`) : "- (none)";
|
|
22557
|
-
const chatLines = result.chat_messages.length > 0 ? result.chat_messages.slice(-12).map((msg) => `- [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`).join(`
|
|
22742
|
+
const chatLines = result.chat_messages.length > 0 ? result.chat_messages.slice(-12).map((msg) => `- [${msg.role}] [${msg.source_kind}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`).join(`
|
|
22558
22743
|
`) : "- (none)";
|
|
22559
22744
|
const toolLines = result.tool_events.length > 0 ? result.tool_events.slice(-15).map((tool) => {
|
|
22560
22745
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
@@ -22596,6 +22781,8 @@ ${summaryLines}
|
|
|
22596
22781
|
` + `Prompts:
|
|
22597
22782
|
${promptLines}
|
|
22598
22783
|
|
|
22784
|
+
` + `Chat recall: ${result.chat_coverage_state} (transcript ${result.chat_source_summary.transcript}, hook ${result.chat_source_summary.hook})
|
|
22785
|
+
|
|
22599
22786
|
` + `Chat:
|
|
22600
22787
|
${chatLines}
|
|
22601
22788
|
|