engrm 0.4.31 → 0.4.32
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 +26 -2
- package/dist/hooks/pre-compact.js +27 -0
- package/dist/hooks/session-start.js +57 -1
- package/dist/hooks/stop.js +1 -1
- package/dist/server.js +544 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -226,8 +226,10 @@ 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` |
|
|
230
|
-
| `
|
|
229
|
+
| `repair_recall` | Use when continuity feels thin; rehydrate recent recall from transcript or Claude history fallback |
|
|
230
|
+
| `list_recall_items` | Use first when continuity feels fuzzy; list the best current handoffs, threads, chat snippets, and memory entries |
|
|
231
|
+
| `load_recall_item` | Use after `list_recall_items`; load one exact recall item key |
|
|
232
|
+
| `resume_thread` | Use first when you want one direct "where were we?" answer from handoff, current thread, recent chat, and unified recall |
|
|
231
233
|
| `recent_chat` | Inspect the separate synced chat lane without mixing it into durable memory |
|
|
232
234
|
| `search_chat` | Search recent chat recall with hybrid lexical + semantic matching, separately from reusable memory observations |
|
|
233
235
|
| `search_recall` | Search durable memory and chat recall together when you do not want to guess the right lane |
|
|
@@ -252,6 +254,10 @@ If you are evaluating Engrm as an MCP server, start with this small set first:
|
|
|
252
254
|
- verify which tools are actually producing durable memory and which plugins they exercise
|
|
253
255
|
- `capture_quality`
|
|
254
256
|
- check whether raw chronology is healthy across the workspace before judging memory quality
|
|
257
|
+
- `list_recall_items`
|
|
258
|
+
- list the best current handoffs, session threads, chat snippets, and memory entries before opening one exact item
|
|
259
|
+
- `load_recall_item`
|
|
260
|
+
- open one exact recall key from the index without falling back to fuzzy retrieval
|
|
255
261
|
- `resume_thread`
|
|
256
262
|
- get one direct “where were we?” resume point with freshness, source, tool trail, and next actions
|
|
257
263
|
- `repair_recall`
|
|
@@ -265,6 +271,22 @@ These are the tools we should be comfortable pointing people to publicly first:
|
|
|
265
271
|
- easy local inspection after capture
|
|
266
272
|
- clear continuity recovery when switching devices or resuming long sessions
|
|
267
273
|
|
|
274
|
+
### Recall Protocol
|
|
275
|
+
|
|
276
|
+
When continuity feels fuzzy, the default path is:
|
|
277
|
+
|
|
278
|
+
1. `resume_thread`
|
|
279
|
+
2. `list_recall_items`
|
|
280
|
+
3. `load_recall_item`
|
|
281
|
+
4. `repair_recall`
|
|
282
|
+
|
|
283
|
+
How to use it:
|
|
284
|
+
|
|
285
|
+
- `resume_thread` is the fastest "get me back into the live thread" action
|
|
286
|
+
- `list_recall_items` is the deterministic directory-first path when you want to inspect candidates before opening one
|
|
287
|
+
- `load_recall_item` opens an exact handoff, thread, chat, or memory key returned by the index
|
|
288
|
+
- `repair_recall` is the repair step when continuity is still thin, hook-only, or under-captured
|
|
289
|
+
|
|
268
290
|
### Thin Tools, Thick Memory
|
|
269
291
|
|
|
270
292
|
Engrm now has a real thin-tool layer, not just a plugin spec.
|
|
@@ -382,6 +404,8 @@ What each tool is good for:
|
|
|
382
404
|
- `capture_quality` shows whether chat recall is transcript-backed, history-backed, or still hook-only across the workspace
|
|
383
405
|
- `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
|
|
384
406
|
- `resume_thread` is the fastest “get me back into the live thread” path when you want freshness, source, next actions, tool trail, and chat in one place
|
|
407
|
+
- `list_recall_items` is the deterministic directory-first path when you want to inspect the best candidate handoffs/threads before opening one exact item
|
|
408
|
+
- `load_recall_item` completes that protocol by letting agents open one exact recall key directly after listing
|
|
385
409
|
- `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed, history-backed, or only hook-captured
|
|
386
410
|
- `memory_console`, `project_memory_index`, and `session_context` also expose resume-readiness directly, so you can see whether a repo is `live`, `recent`, or `stale` before drilling deeper
|
|
387
411
|
- 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
|
|
@@ -3673,6 +3673,14 @@ function formatContextForInjection(context) {
|
|
|
3673
3673
|
}
|
|
3674
3674
|
lines.push("");
|
|
3675
3675
|
}
|
|
3676
|
+
const recallIndexLines = buildRecallIndexLines(context);
|
|
3677
|
+
if (recallIndexLines.length > 0) {
|
|
3678
|
+
lines.push("## Recall Index");
|
|
3679
|
+
for (const line of recallIndexLines) {
|
|
3680
|
+
lines.push(line);
|
|
3681
|
+
}
|
|
3682
|
+
lines.push("");
|
|
3683
|
+
}
|
|
3676
3684
|
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
3677
3685
|
lines.push("## Recent Chat");
|
|
3678
3686
|
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
@@ -3775,6 +3783,25 @@ function formatContextForInjection(context) {
|
|
|
3775
3783
|
return lines.join(`
|
|
3776
3784
|
`);
|
|
3777
3785
|
}
|
|
3786
|
+
function buildRecallIndexLines(context) {
|
|
3787
|
+
const lines = [];
|
|
3788
|
+
for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
|
|
3789
|
+
const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
3790
|
+
if (!title)
|
|
3791
|
+
continue;
|
|
3792
|
+
lines.push(`- handoff:${handoff.id} — ${truncateText(title, 140)}`);
|
|
3793
|
+
}
|
|
3794
|
+
for (const session of context.recentSessions?.slice(0, 2) ?? []) {
|
|
3795
|
+
const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
|
|
3796
|
+
if (!title)
|
|
3797
|
+
continue;
|
|
3798
|
+
lines.push(`- session:${session.session_id} — ${truncateText(title.replace(/\s+/g, " ").trim(), 140)}`);
|
|
3799
|
+
}
|
|
3800
|
+
for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
|
|
3801
|
+
lines.push(`- chat:${message.id} — ${truncateText(message.content.replace(/\s+/g, " ").trim(), 140)}`);
|
|
3802
|
+
}
|
|
3803
|
+
return Array.from(new Set(lines)).slice(0, 5);
|
|
3804
|
+
}
|
|
3778
3805
|
function formatSessionBrief(summary) {
|
|
3779
3806
|
const lines = [];
|
|
3780
3807
|
const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
@@ -1943,6 +1943,14 @@ function formatContextForInjection(context) {
|
|
|
1943
1943
|
}
|
|
1944
1944
|
lines.push("");
|
|
1945
1945
|
}
|
|
1946
|
+
const recallIndexLines = buildRecallIndexLines(context);
|
|
1947
|
+
if (recallIndexLines.length > 0) {
|
|
1948
|
+
lines.push("## Recall Index");
|
|
1949
|
+
for (const line of recallIndexLines) {
|
|
1950
|
+
lines.push(line);
|
|
1951
|
+
}
|
|
1952
|
+
lines.push("");
|
|
1953
|
+
}
|
|
1946
1954
|
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
1947
1955
|
lines.push("## Recent Chat");
|
|
1948
1956
|
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
@@ -2045,6 +2053,25 @@ function formatContextForInjection(context) {
|
|
|
2045
2053
|
return lines.join(`
|
|
2046
2054
|
`);
|
|
2047
2055
|
}
|
|
2056
|
+
function buildRecallIndexLines(context) {
|
|
2057
|
+
const lines = [];
|
|
2058
|
+
for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
|
|
2059
|
+
const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
2060
|
+
if (!title)
|
|
2061
|
+
continue;
|
|
2062
|
+
lines.push(`- handoff:${handoff.id} — ${truncateText(title, 140)}`);
|
|
2063
|
+
}
|
|
2064
|
+
for (const session of context.recentSessions?.slice(0, 2) ?? []) {
|
|
2065
|
+
const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
|
|
2066
|
+
if (!title)
|
|
2067
|
+
continue;
|
|
2068
|
+
lines.push(`- session:${session.session_id} — ${truncateText(title.replace(/\s+/g, " ").trim(), 140)}`);
|
|
2069
|
+
}
|
|
2070
|
+
for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
|
|
2071
|
+
lines.push(`- chat:${message.id} — ${truncateText(message.content.replace(/\s+/g, " ").trim(), 140)}`);
|
|
2072
|
+
}
|
|
2073
|
+
return Array.from(new Set(lines)).slice(0, 5);
|
|
2074
|
+
}
|
|
2048
2075
|
function formatSessionBrief(summary) {
|
|
2049
2076
|
const lines = [];
|
|
2050
2077
|
const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
@@ -2618,6 +2645,14 @@ function formatContextForInjection2(context) {
|
|
|
2618
2645
|
}
|
|
2619
2646
|
lines.push("");
|
|
2620
2647
|
}
|
|
2648
|
+
const recallIndexLines = buildRecallIndexLines2(context);
|
|
2649
|
+
if (recallIndexLines.length > 0) {
|
|
2650
|
+
lines.push("## Recall Index");
|
|
2651
|
+
for (const line of recallIndexLines) {
|
|
2652
|
+
lines.push(line);
|
|
2653
|
+
}
|
|
2654
|
+
lines.push("");
|
|
2655
|
+
}
|
|
2621
2656
|
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
2622
2657
|
lines.push("## Recent Chat");
|
|
2623
2658
|
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
@@ -2720,6 +2755,25 @@ function formatContextForInjection2(context) {
|
|
|
2720
2755
|
return lines.join(`
|
|
2721
2756
|
`);
|
|
2722
2757
|
}
|
|
2758
|
+
function buildRecallIndexLines2(context) {
|
|
2759
|
+
const lines = [];
|
|
2760
|
+
for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
|
|
2761
|
+
const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
2762
|
+
if (!title)
|
|
2763
|
+
continue;
|
|
2764
|
+
lines.push(`- handoff:${handoff.id} — ${truncateText2(title, 140)}`);
|
|
2765
|
+
}
|
|
2766
|
+
for (const session of context.recentSessions?.slice(0, 2) ?? []) {
|
|
2767
|
+
const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
|
|
2768
|
+
if (!title)
|
|
2769
|
+
continue;
|
|
2770
|
+
lines.push(`- session:${session.session_id} — ${truncateText2(title.replace(/\s+/g, " ").trim(), 140)}`);
|
|
2771
|
+
}
|
|
2772
|
+
for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
|
|
2773
|
+
lines.push(`- chat:${message.id} — ${truncateText2(message.content.replace(/\s+/g, " ").trim(), 140)}`);
|
|
2774
|
+
}
|
|
2775
|
+
return Array.from(new Set(lines)).slice(0, 5);
|
|
2776
|
+
}
|
|
2723
2777
|
function formatSessionBrief2(summary) {
|
|
2724
2778
|
const lines = [];
|
|
2725
2779
|
const heading = summary.request ? `### ${truncateText2(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
@@ -3090,7 +3144,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
3090
3144
|
import { join as join3 } from "node:path";
|
|
3091
3145
|
import { homedir } from "node:os";
|
|
3092
3146
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
3093
|
-
var CLIENT_VERSION = "0.4.
|
|
3147
|
+
var CLIENT_VERSION = "0.4.32";
|
|
3094
3148
|
function hashFile(filePath) {
|
|
3095
3149
|
try {
|
|
3096
3150
|
if (!existsSync3(filePath))
|
|
@@ -5794,6 +5848,8 @@ function formatInspectHints(context, visibleObservationIds = []) {
|
|
|
5794
5848
|
hints.push("activity_feed");
|
|
5795
5849
|
}
|
|
5796
5850
|
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
|
|
5851
|
+
hints.push("list_recall_items");
|
|
5852
|
+
hints.push("load_recall_item");
|
|
5797
5853
|
hints.push("resume_thread");
|
|
5798
5854
|
hints.push("search_recall");
|
|
5799
5855
|
}
|
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.32",
|
|
3086
3086
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
3087
3087
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
3088
3088
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
package/dist/server.js
CHANGED
|
@@ -17976,6 +17976,14 @@ function formatContextForInjection(context) {
|
|
|
17976
17976
|
}
|
|
17977
17977
|
lines.push("");
|
|
17978
17978
|
}
|
|
17979
|
+
const recallIndexLines = buildRecallIndexLines(context);
|
|
17980
|
+
if (recallIndexLines.length > 0) {
|
|
17981
|
+
lines.push("## Recall Index");
|
|
17982
|
+
for (const line of recallIndexLines) {
|
|
17983
|
+
lines.push(line);
|
|
17984
|
+
}
|
|
17985
|
+
lines.push("");
|
|
17986
|
+
}
|
|
17979
17987
|
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
17980
17988
|
lines.push("## Recent Chat");
|
|
17981
17989
|
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
@@ -18078,6 +18086,25 @@ function formatContextForInjection(context) {
|
|
|
18078
18086
|
return lines.join(`
|
|
18079
18087
|
`);
|
|
18080
18088
|
}
|
|
18089
|
+
function buildRecallIndexLines(context) {
|
|
18090
|
+
const lines = [];
|
|
18091
|
+
for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
|
|
18092
|
+
const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
18093
|
+
if (!title)
|
|
18094
|
+
continue;
|
|
18095
|
+
lines.push(`- handoff:${handoff.id} — ${truncateText(title, 140)}`);
|
|
18096
|
+
}
|
|
18097
|
+
for (const session of context.recentSessions?.slice(0, 2) ?? []) {
|
|
18098
|
+
const title = session.current_thread ?? session.request ?? session.completed ?? session.session_id;
|
|
18099
|
+
if (!title)
|
|
18100
|
+
continue;
|
|
18101
|
+
lines.push(`- session:${session.session_id} — ${truncateText(title.replace(/\s+/g, " ").trim(), 140)}`);
|
|
18102
|
+
}
|
|
18103
|
+
for (const message of context.recentChatMessages?.slice(0, 2) ?? []) {
|
|
18104
|
+
lines.push(`- chat:${message.id} — ${truncateText(message.content.replace(/\s+/g, " ").trim(), 140)}`);
|
|
18105
|
+
}
|
|
18106
|
+
return Array.from(new Set(lines)).slice(0, 5);
|
|
18107
|
+
}
|
|
18081
18108
|
function formatSessionBrief(summary) {
|
|
18082
18109
|
const lines = [];
|
|
18083
18110
|
const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
@@ -18296,6 +18323,187 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
18296
18323
|
return picked;
|
|
18297
18324
|
}
|
|
18298
18325
|
|
|
18326
|
+
// src/tools/list-recall-items.ts
|
|
18327
|
+
function listRecallItems(db, input) {
|
|
18328
|
+
const limit = Math.max(3, Math.min(input.limit ?? 12, 30));
|
|
18329
|
+
const projectScoped = input.project_scoped !== false;
|
|
18330
|
+
let projectName;
|
|
18331
|
+
if (projectScoped) {
|
|
18332
|
+
const cwd = input.cwd ?? process.cwd();
|
|
18333
|
+
const detected = detectProject(cwd);
|
|
18334
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
18335
|
+
projectName = project?.name;
|
|
18336
|
+
}
|
|
18337
|
+
const handoffs = getRecentHandoffs(db, {
|
|
18338
|
+
cwd: input.cwd,
|
|
18339
|
+
project_scoped: projectScoped,
|
|
18340
|
+
user_id: input.user_id,
|
|
18341
|
+
current_device_id: input.current_device_id,
|
|
18342
|
+
limit
|
|
18343
|
+
}).handoffs;
|
|
18344
|
+
const sessions = getRecentSessions(db, {
|
|
18345
|
+
cwd: input.cwd,
|
|
18346
|
+
project_scoped: projectScoped,
|
|
18347
|
+
user_id: input.user_id,
|
|
18348
|
+
limit: Math.min(limit, 8)
|
|
18349
|
+
}).sessions;
|
|
18350
|
+
const chat = getRecentChat(db, {
|
|
18351
|
+
cwd: input.cwd,
|
|
18352
|
+
project_scoped: projectScoped,
|
|
18353
|
+
user_id: input.user_id,
|
|
18354
|
+
limit: Math.min(limit * 2, 20)
|
|
18355
|
+
}).messages;
|
|
18356
|
+
const observations = getRecentActivity(db, {
|
|
18357
|
+
cwd: input.cwd,
|
|
18358
|
+
project_scoped: projectScoped,
|
|
18359
|
+
user_id: input.user_id,
|
|
18360
|
+
limit: Math.min(limit * 2, 24)
|
|
18361
|
+
}).observations;
|
|
18362
|
+
const items = [
|
|
18363
|
+
...handoffs.map((handoff) => ({
|
|
18364
|
+
key: `handoff:${handoff.id}`,
|
|
18365
|
+
kind: "handoff",
|
|
18366
|
+
title: stripHandoffPrefix(handoff.title),
|
|
18367
|
+
detail: summarizeHandoffDetail(handoff.narrative),
|
|
18368
|
+
created_at_epoch: handoff.created_at_epoch,
|
|
18369
|
+
freshness: classifyFreshness(handoff.created_at_epoch),
|
|
18370
|
+
session_id: handoff.session_id,
|
|
18371
|
+
source_device_id: handoff.device_id ?? null
|
|
18372
|
+
})),
|
|
18373
|
+
...sessions.filter((session) => Boolean(session.request || session.completed || session.current_thread)).map((session) => ({
|
|
18374
|
+
key: `session:${session.session_id}`,
|
|
18375
|
+
kind: "thread",
|
|
18376
|
+
title: session.current_thread ?? session.request ?? session.completed ?? session.session_id,
|
|
18377
|
+
detail: buildSessionDetail(session),
|
|
18378
|
+
created_at_epoch: session.completed_at_epoch ?? session.started_at_epoch ?? 0,
|
|
18379
|
+
freshness: classifyFreshness(session.completed_at_epoch ?? session.started_at_epoch ?? null),
|
|
18380
|
+
session_id: session.session_id,
|
|
18381
|
+
source_device_id: session.device_id ?? null
|
|
18382
|
+
})),
|
|
18383
|
+
...dedupeChatIndex(chat).map((message) => {
|
|
18384
|
+
const origin = getChatCaptureOrigin(message);
|
|
18385
|
+
return {
|
|
18386
|
+
key: `chat:${message.id}`,
|
|
18387
|
+
kind: "chat",
|
|
18388
|
+
title: `${message.role} [${origin}]`,
|
|
18389
|
+
detail: truncateInline(message.content.replace(/\s+/g, " ").trim(), 180),
|
|
18390
|
+
created_at_epoch: message.created_at_epoch,
|
|
18391
|
+
freshness: classifyFreshness(message.created_at_epoch),
|
|
18392
|
+
session_id: message.session_id,
|
|
18393
|
+
source_device_id: message.device_id ?? null
|
|
18394
|
+
};
|
|
18395
|
+
}),
|
|
18396
|
+
...observations.filter((obs) => obs.type !== "message").filter((obs) => !looksLikeFileOperationTitle3(obs.title)).map((obs) => ({
|
|
18397
|
+
key: `obs:${obs.id}`,
|
|
18398
|
+
kind: "memory",
|
|
18399
|
+
title: `[${obs.type}] ${obs.title}`,
|
|
18400
|
+
detail: truncateInline(firstNonEmpty2(obs.narrative, previewFacts(obs.facts), obs.project_name ?? "") ?? obs.type, 180),
|
|
18401
|
+
created_at_epoch: obs.created_at_epoch,
|
|
18402
|
+
freshness: classifyFreshness(obs.created_at_epoch),
|
|
18403
|
+
session_id: obs.session_id ?? null,
|
|
18404
|
+
source_device_id: obs.device_id ?? null
|
|
18405
|
+
}))
|
|
18406
|
+
];
|
|
18407
|
+
const deduped = dedupeRecallItems(items).sort((a, b) => compareRecallItems(a, b, input.current_device_id)).slice(0, limit);
|
|
18408
|
+
return {
|
|
18409
|
+
project: projectName,
|
|
18410
|
+
continuity_mode: deduped.some((item) => item.kind === "handoff" || item.kind === "thread") ? "direct" : "indexed",
|
|
18411
|
+
items: deduped
|
|
18412
|
+
};
|
|
18413
|
+
}
|
|
18414
|
+
function compareRecallItems(a, b, currentDeviceId) {
|
|
18415
|
+
const priority = (item) => {
|
|
18416
|
+
const freshness = item.freshness === "live" ? 0 : item.freshness === "recent" ? 1 : 2;
|
|
18417
|
+
const kind = item.kind === "handoff" ? 0 : item.kind === "thread" ? 1 : item.kind === "chat" ? 2 : 3;
|
|
18418
|
+
const remoteBoost = currentDeviceId && item.source_device_id && item.source_device_id !== currentDeviceId ? -0.5 : 0;
|
|
18419
|
+
const draftPenalty = item.kind === "handoff" && /draft/i.test(item.title) ? 0.25 : 0;
|
|
18420
|
+
return freshness * 10 + kind + remoteBoost + draftPenalty;
|
|
18421
|
+
};
|
|
18422
|
+
const priorityDiff = priority(a) - priority(b);
|
|
18423
|
+
if (priorityDiff !== 0)
|
|
18424
|
+
return priorityDiff;
|
|
18425
|
+
return b.created_at_epoch - a.created_at_epoch;
|
|
18426
|
+
}
|
|
18427
|
+
function dedupeRecallItems(items) {
|
|
18428
|
+
const best = new Map;
|
|
18429
|
+
for (const item of items) {
|
|
18430
|
+
const key = `${item.kind}::${normalize(item.title)}::${normalize(item.detail)}`;
|
|
18431
|
+
const existing = best.get(key);
|
|
18432
|
+
if (!existing || compareRecallItems(item, existing) < 0) {
|
|
18433
|
+
best.set(key, item);
|
|
18434
|
+
}
|
|
18435
|
+
}
|
|
18436
|
+
return Array.from(best.values());
|
|
18437
|
+
}
|
|
18438
|
+
function dedupeChatIndex(messages) {
|
|
18439
|
+
const byKey = new Map;
|
|
18440
|
+
for (const message of messages) {
|
|
18441
|
+
const key = `${message.session_id}::${message.role}::${normalize(message.content)}`;
|
|
18442
|
+
const existing = byKey.get(key);
|
|
18443
|
+
if (!existing || message.created_at_epoch > existing.created_at_epoch) {
|
|
18444
|
+
byKey.set(key, message);
|
|
18445
|
+
}
|
|
18446
|
+
}
|
|
18447
|
+
return Array.from(byKey.values());
|
|
18448
|
+
}
|
|
18449
|
+
function summarizeHandoffDetail(narrative) {
|
|
18450
|
+
if (!narrative)
|
|
18451
|
+
return "";
|
|
18452
|
+
const line = narrative.split(/\n+/).map((item) => item.trim()).find((item) => /^Current thread:|^Completed:|^Next Steps:/i.test(item));
|
|
18453
|
+
return line ? truncateInline(line.replace(/^(Current thread:|Completed:|Next Steps:)\s*/i, ""), 180) : truncateInline(narrative.replace(/\s+/g, " ").trim(), 180);
|
|
18454
|
+
}
|
|
18455
|
+
function buildSessionDetail(session) {
|
|
18456
|
+
const pieces = [
|
|
18457
|
+
session.request,
|
|
18458
|
+
session.completed,
|
|
18459
|
+
session.current_thread
|
|
18460
|
+
].filter((item) => Boolean(item && item.trim())).map((item) => item.replace(/\s+/g, " ").trim());
|
|
18461
|
+
return truncateInline(pieces[0] ?? `prompts ${session.prompt_count}, tools ${session.tool_event_count}`, 180);
|
|
18462
|
+
}
|
|
18463
|
+
function stripHandoffPrefix(value) {
|
|
18464
|
+
return value.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
18465
|
+
}
|
|
18466
|
+
function classifyFreshness(createdAtEpoch) {
|
|
18467
|
+
if (!createdAtEpoch)
|
|
18468
|
+
return "stale";
|
|
18469
|
+
const ageMs = Date.now() - createdAtEpoch * 1000;
|
|
18470
|
+
if (ageMs <= 15 * 60 * 1000)
|
|
18471
|
+
return "live";
|
|
18472
|
+
if (ageMs <= 3 * 24 * 60 * 60 * 1000)
|
|
18473
|
+
return "recent";
|
|
18474
|
+
return "stale";
|
|
18475
|
+
}
|
|
18476
|
+
function normalize(value) {
|
|
18477
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
18478
|
+
}
|
|
18479
|
+
function truncateInline(text, maxLen) {
|
|
18480
|
+
if (text.length <= maxLen)
|
|
18481
|
+
return text;
|
|
18482
|
+
return `${text.slice(0, maxLen - 1).trimEnd()}…`;
|
|
18483
|
+
}
|
|
18484
|
+
function firstNonEmpty2(...values) {
|
|
18485
|
+
for (const value of values) {
|
|
18486
|
+
if (value && value.trim())
|
|
18487
|
+
return value.trim();
|
|
18488
|
+
}
|
|
18489
|
+
return null;
|
|
18490
|
+
}
|
|
18491
|
+
function previewFacts(facts) {
|
|
18492
|
+
if (!facts)
|
|
18493
|
+
return null;
|
|
18494
|
+
try {
|
|
18495
|
+
const parsed = JSON.parse(facts);
|
|
18496
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
18497
|
+
return null;
|
|
18498
|
+
return parsed.filter((item) => typeof item === "string" && item.trim().length > 0).slice(0, 2).join(" | ");
|
|
18499
|
+
} catch {
|
|
18500
|
+
return facts;
|
|
18501
|
+
}
|
|
18502
|
+
}
|
|
18503
|
+
function looksLikeFileOperationTitle3(value) {
|
|
18504
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
18505
|
+
}
|
|
18506
|
+
|
|
18299
18507
|
// src/tools/project-memory-index.ts
|
|
18300
18508
|
function getProjectMemoryIndex(db, input) {
|
|
18301
18509
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -18371,9 +18579,15 @@ function getProjectMemoryIndex(db, input) {
|
|
|
18371
18579
|
limit: 20
|
|
18372
18580
|
});
|
|
18373
18581
|
const recentChatCount = recentChat.messages.length;
|
|
18582
|
+
const recallIndex = listRecallItems(db, {
|
|
18583
|
+
cwd,
|
|
18584
|
+
project_scoped: true,
|
|
18585
|
+
user_id: input.user_id,
|
|
18586
|
+
limit: 10
|
|
18587
|
+
});
|
|
18374
18588
|
const latestSession = recentSessions[0] ?? null;
|
|
18375
18589
|
const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
|
|
18376
|
-
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !
|
|
18590
|
+
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !looksLikeFileOperationTitle4(title)).slice(0, 8);
|
|
18377
18591
|
const captureSummary = summarizeCaptureState(recentSessions);
|
|
18378
18592
|
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);
|
|
18379
18593
|
const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.coverage_state);
|
|
@@ -18393,6 +18607,14 @@ function getProjectMemoryIndex(db, input) {
|
|
|
18393
18607
|
canonical_id: project.canonical_id,
|
|
18394
18608
|
continuity_state: continuityState,
|
|
18395
18609
|
continuity_summary: describeContinuityState(continuityState),
|
|
18610
|
+
recall_mode: recallIndex.continuity_mode,
|
|
18611
|
+
recall_items_ready: recallIndex.items.length,
|
|
18612
|
+
recall_index_preview: recallIndex.items.slice(0, 3).map((item) => ({
|
|
18613
|
+
key: item.key,
|
|
18614
|
+
kind: item.kind,
|
|
18615
|
+
freshness: item.freshness,
|
|
18616
|
+
title: item.title
|
|
18617
|
+
})),
|
|
18396
18618
|
resume_freshness: classifyResumeFreshness(sourceTimestamp),
|
|
18397
18619
|
resume_source_session_id: latestSession?.session_id ?? null,
|
|
18398
18620
|
resume_source_device_id: latestSession?.device_id ?? null,
|
|
@@ -18475,7 +18697,7 @@ function extractPaths(value) {
|
|
|
18475
18697
|
return [];
|
|
18476
18698
|
}
|
|
18477
18699
|
}
|
|
18478
|
-
function
|
|
18700
|
+
function looksLikeFileOperationTitle4(value) {
|
|
18479
18701
|
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
18480
18702
|
}
|
|
18481
18703
|
function summarizeCaptureState(sessions) {
|
|
@@ -18512,6 +18734,8 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
|
|
|
18512
18734
|
suggested.push("activity_feed");
|
|
18513
18735
|
}
|
|
18514
18736
|
if (requestCount > 0 || recentChatCount > 0 || observationCount > 0) {
|
|
18737
|
+
suggested.push("list_recall_items");
|
|
18738
|
+
suggested.push("load_recall_item");
|
|
18515
18739
|
suggested.push("resume_thread");
|
|
18516
18740
|
suggested.push("search_recall");
|
|
18517
18741
|
}
|
|
@@ -18577,6 +18801,12 @@ function getMemoryConsole(db, input) {
|
|
|
18577
18801
|
user_id: input.user_id,
|
|
18578
18802
|
limit: 6
|
|
18579
18803
|
});
|
|
18804
|
+
const recallIndex = listRecallItems(db, {
|
|
18805
|
+
cwd,
|
|
18806
|
+
project_scoped: projectScoped,
|
|
18807
|
+
user_id: input.user_id,
|
|
18808
|
+
limit: 10
|
|
18809
|
+
});
|
|
18580
18810
|
const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
|
|
18581
18811
|
cwd,
|
|
18582
18812
|
user_id: input.user_id
|
|
@@ -18587,6 +18817,14 @@ function getMemoryConsole(db, input) {
|
|
|
18587
18817
|
capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
|
|
18588
18818
|
continuity_state: continuityState,
|
|
18589
18819
|
continuity_summary: projectIndex?.continuity_summary ?? describeContinuityState(continuityState),
|
|
18820
|
+
recall_mode: projectIndex?.recall_mode ?? recallIndex.continuity_mode,
|
|
18821
|
+
recall_items_ready: projectIndex?.recall_items_ready ?? recallIndex.items.length,
|
|
18822
|
+
recall_index_preview: projectIndex?.recall_index_preview ?? recallIndex.items.slice(0, 3).map((item) => ({
|
|
18823
|
+
key: item.key,
|
|
18824
|
+
kind: item.kind,
|
|
18825
|
+
freshness: item.freshness,
|
|
18826
|
+
title: item.title
|
|
18827
|
+
})),
|
|
18590
18828
|
resume_freshness: projectIndex?.resume_freshness ?? "stale",
|
|
18591
18829
|
resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
|
|
18592
18830
|
resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
|
|
@@ -18620,7 +18858,9 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
|
|
|
18620
18858
|
if (requestCount > 0 || toolCount > 0)
|
|
18621
18859
|
suggested.push("activity_feed");
|
|
18622
18860
|
if (requestCount > 0 || chatCount > 0 || observationCount > 0)
|
|
18623
|
-
suggested.push("resume_thread", "search_recall");
|
|
18861
|
+
suggested.push("load_recall_item", "resume_thread", "search_recall");
|
|
18862
|
+
if (requestCount > 0 || chatCount > 0 || observationCount > 0)
|
|
18863
|
+
suggested.unshift("list_recall_items");
|
|
18624
18864
|
if ((sessionCount > 0 || chatCount > 0) && chatCoverageState !== "transcript-backed")
|
|
18625
18865
|
suggested.push("repair_recall");
|
|
18626
18866
|
if (observationCount > 0)
|
|
@@ -19467,6 +19707,13 @@ function getSessionContext(db, input) {
|
|
|
19467
19707
|
limit: 8
|
|
19468
19708
|
});
|
|
19469
19709
|
const recentChatMessages = recentChat.messages.length;
|
|
19710
|
+
const recallIndex = listRecallItems(db, {
|
|
19711
|
+
cwd,
|
|
19712
|
+
project_scoped: true,
|
|
19713
|
+
user_id: input.user_id,
|
|
19714
|
+
current_device_id: input.current_device_id,
|
|
19715
|
+
limit: 10
|
|
19716
|
+
});
|
|
19470
19717
|
const latestSession = context.recentSessions?.[0] ?? null;
|
|
19471
19718
|
const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
|
|
19472
19719
|
const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
|
|
@@ -19479,6 +19726,14 @@ function getSessionContext(db, input) {
|
|
|
19479
19726
|
canonical_id: context.canonical_id,
|
|
19480
19727
|
continuity_state: continuityState,
|
|
19481
19728
|
continuity_summary: describeContinuityState(continuityState),
|
|
19729
|
+
recall_mode: recallIndex.continuity_mode,
|
|
19730
|
+
recall_items_ready: recallIndex.items.length,
|
|
19731
|
+
recall_index_preview: recallIndex.items.slice(0, 3).map((item) => ({
|
|
19732
|
+
key: item.key,
|
|
19733
|
+
kind: item.kind,
|
|
19734
|
+
freshness: item.freshness,
|
|
19735
|
+
title: item.title
|
|
19736
|
+
})),
|
|
19482
19737
|
resume_freshness: classifyResumeFreshness(resumeTimestamp),
|
|
19483
19738
|
resume_source_session_id: latestSession?.session_id ?? null,
|
|
19484
19739
|
resume_source_device_id: latestSession?.device_id ?? null,
|
|
@@ -19541,6 +19796,8 @@ function buildSuggestedTools2(context, chatCoverageState) {
|
|
|
19541
19796
|
tools.push("activity_feed");
|
|
19542
19797
|
}
|
|
19543
19798
|
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
|
|
19799
|
+
tools.push("list_recall_items");
|
|
19800
|
+
tools.push("load_recall_item");
|
|
19544
19801
|
tools.push("resume_thread");
|
|
19545
19802
|
tools.push("search_recall");
|
|
19546
19803
|
}
|
|
@@ -19565,6 +19822,139 @@ function buildSuggestedTools2(context, chatCoverageState) {
|
|
|
19565
19822
|
return Array.from(new Set(tools)).slice(0, 5);
|
|
19566
19823
|
}
|
|
19567
19824
|
|
|
19825
|
+
// src/tools/load-recall-item.ts
|
|
19826
|
+
function loadRecallItem(db, input) {
|
|
19827
|
+
const [kind, rawId] = input.key.split(":", 2);
|
|
19828
|
+
if (!kind || !rawId) {
|
|
19829
|
+
return {
|
|
19830
|
+
kind: "unknown",
|
|
19831
|
+
key: input.key,
|
|
19832
|
+
title: null,
|
|
19833
|
+
detail: "Malformed recall key",
|
|
19834
|
+
session_id: null,
|
|
19835
|
+
source_device_id: null,
|
|
19836
|
+
payload: null
|
|
19837
|
+
};
|
|
19838
|
+
}
|
|
19839
|
+
if (kind === "handoff") {
|
|
19840
|
+
const id = Number.parseInt(rawId, 10);
|
|
19841
|
+
const result = loadHandoff(db, {
|
|
19842
|
+
id,
|
|
19843
|
+
cwd: input.cwd,
|
|
19844
|
+
user_id: input.user_id,
|
|
19845
|
+
current_device_id: input.current_device_id
|
|
19846
|
+
});
|
|
19847
|
+
if (!result.handoff) {
|
|
19848
|
+
return missing(input.key, "handoff");
|
|
19849
|
+
}
|
|
19850
|
+
return {
|
|
19851
|
+
kind: "handoff",
|
|
19852
|
+
key: input.key,
|
|
19853
|
+
title: result.handoff.title,
|
|
19854
|
+
detail: summarizeNarrative(result.handoff.narrative),
|
|
19855
|
+
session_id: result.handoff.session_id ?? null,
|
|
19856
|
+
source_device_id: result.handoff.device_id ?? null,
|
|
19857
|
+
payload: {
|
|
19858
|
+
type: "handoff",
|
|
19859
|
+
handoff_id: result.handoff.id,
|
|
19860
|
+
narrative: result.handoff.narrative ?? null
|
|
19861
|
+
}
|
|
19862
|
+
};
|
|
19863
|
+
}
|
|
19864
|
+
if (kind === "session") {
|
|
19865
|
+
const story = getSessionStory(db, { session_id: rawId });
|
|
19866
|
+
if (!story.session) {
|
|
19867
|
+
return missing(input.key, "thread");
|
|
19868
|
+
}
|
|
19869
|
+
return {
|
|
19870
|
+
kind: "thread",
|
|
19871
|
+
key: input.key,
|
|
19872
|
+
title: story.summary?.current_thread ?? story.latest_request ?? story.summary?.completed ?? story.session.session_id,
|
|
19873
|
+
detail: story.summary?.next_steps ?? story.summary?.completed ?? null,
|
|
19874
|
+
session_id: story.session.session_id,
|
|
19875
|
+
source_device_id: story.session.device_id ?? null,
|
|
19876
|
+
payload: {
|
|
19877
|
+
type: "thread",
|
|
19878
|
+
latest_request: story.latest_request,
|
|
19879
|
+
current_thread: story.summary?.current_thread ?? null,
|
|
19880
|
+
recent_outcomes: story.recent_outcomes,
|
|
19881
|
+
hot_files: story.hot_files
|
|
19882
|
+
}
|
|
19883
|
+
};
|
|
19884
|
+
}
|
|
19885
|
+
if (kind === "chat") {
|
|
19886
|
+
const id = Number.parseInt(rawId, 10);
|
|
19887
|
+
const messages = getRecentChat(db, {
|
|
19888
|
+
cwd: input.cwd,
|
|
19889
|
+
project_scoped: false,
|
|
19890
|
+
user_id: input.user_id,
|
|
19891
|
+
limit: 200
|
|
19892
|
+
}).messages;
|
|
19893
|
+
const message = messages.find((item) => item.id === id);
|
|
19894
|
+
if (!message) {
|
|
19895
|
+
return missing(input.key, "chat");
|
|
19896
|
+
}
|
|
19897
|
+
const source = message.source_kind === "transcript" ? "transcript" : message.remote_source_id?.startsWith("history:") ? "history" : "hook";
|
|
19898
|
+
return {
|
|
19899
|
+
kind: "chat",
|
|
19900
|
+
key: input.key,
|
|
19901
|
+
title: `${message.role} [${source}]`,
|
|
19902
|
+
detail: message.content,
|
|
19903
|
+
session_id: message.session_id,
|
|
19904
|
+
source_device_id: message.device_id ?? null,
|
|
19905
|
+
payload: {
|
|
19906
|
+
type: "chat",
|
|
19907
|
+
role: message.role,
|
|
19908
|
+
content: message.content,
|
|
19909
|
+
source
|
|
19910
|
+
}
|
|
19911
|
+
};
|
|
19912
|
+
}
|
|
19913
|
+
if (kind === "obs") {
|
|
19914
|
+
const id = Number.parseInt(rawId, 10);
|
|
19915
|
+
const result = getObservations(db, {
|
|
19916
|
+
ids: [id],
|
|
19917
|
+
user_id: input.user_id
|
|
19918
|
+
});
|
|
19919
|
+
const obs = result.observations[0];
|
|
19920
|
+
if (!obs) {
|
|
19921
|
+
return missing(input.key, "memory");
|
|
19922
|
+
}
|
|
19923
|
+
return {
|
|
19924
|
+
kind: "memory",
|
|
19925
|
+
key: input.key,
|
|
19926
|
+
title: obs.title,
|
|
19927
|
+
detail: obs.narrative ?? obs.facts ?? null,
|
|
19928
|
+
session_id: obs.session_id ?? null,
|
|
19929
|
+
source_device_id: obs.device_id ?? null,
|
|
19930
|
+
payload: {
|
|
19931
|
+
type: "memory",
|
|
19932
|
+
observation_id: obs.id,
|
|
19933
|
+
observation_type: obs.type,
|
|
19934
|
+
narrative: obs.narrative ?? null,
|
|
19935
|
+
facts: obs.facts ?? null
|
|
19936
|
+
}
|
|
19937
|
+
};
|
|
19938
|
+
}
|
|
19939
|
+
return missing(input.key, "unknown");
|
|
19940
|
+
}
|
|
19941
|
+
function summarizeNarrative(value) {
|
|
19942
|
+
if (!value)
|
|
19943
|
+
return null;
|
|
19944
|
+
return value.split(/\n+/).map((line) => line.trim()).find(Boolean) ?? null;
|
|
19945
|
+
}
|
|
19946
|
+
function missing(key, kind) {
|
|
19947
|
+
return {
|
|
19948
|
+
kind,
|
|
19949
|
+
key,
|
|
19950
|
+
title: null,
|
|
19951
|
+
detail: "Recall item not found",
|
|
19952
|
+
session_id: null,
|
|
19953
|
+
source_device_id: null,
|
|
19954
|
+
payload: null
|
|
19955
|
+
};
|
|
19956
|
+
}
|
|
19957
|
+
|
|
19568
19958
|
// src/tools/capture-git-worktree.ts
|
|
19569
19959
|
import { execSync as execSync2 } from "node:child_process";
|
|
19570
19960
|
import { existsSync as existsSync4 } from "node:fs";
|
|
@@ -21613,7 +22003,7 @@ var REFACTOR_HINTS = [
|
|
|
21613
22003
|
"extract",
|
|
21614
22004
|
"move"
|
|
21615
22005
|
];
|
|
21616
|
-
function
|
|
22006
|
+
function normalize2(text) {
|
|
21617
22007
|
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
21618
22008
|
}
|
|
21619
22009
|
function uniq(items) {
|
|
@@ -21642,7 +22032,7 @@ function countLinesByPrefix(diff, prefix) {
|
|
|
21642
22032
|
`).filter((line) => line.startsWith(prefix) && !line.startsWith(`${prefix}${prefix}${prefix}`)).length;
|
|
21643
22033
|
}
|
|
21644
22034
|
function detectType(summary, diff, files) {
|
|
21645
|
-
const corpus =
|
|
22035
|
+
const corpus = normalize2([summary ?? "", diff, files.join(" ")].join(" "));
|
|
21646
22036
|
if (BUGFIX_HINTS.some((hint) => corpus.includes(hint)))
|
|
21647
22037
|
return "bugfix";
|
|
21648
22038
|
if (DECISION_HINTS.some((hint) => corpus.includes(hint)))
|
|
@@ -21702,7 +22092,7 @@ function buildFacts(diff, files) {
|
|
|
21702
22092
|
if (added > 0 || removed > 0) {
|
|
21703
22093
|
facts.push(`Diff footprint: +${added} / -${removed}`);
|
|
21704
22094
|
}
|
|
21705
|
-
const lower =
|
|
22095
|
+
const lower = normalize2(diff);
|
|
21706
22096
|
if (lower.includes("auth") || lower.includes("token") || lower.includes("oauth")) {
|
|
21707
22097
|
facts.push("Touches authentication or credential flow");
|
|
21708
22098
|
}
|
|
@@ -22039,7 +22429,7 @@ process.on("SIGTERM", () => {
|
|
|
22039
22429
|
});
|
|
22040
22430
|
var server = new McpServer({
|
|
22041
22431
|
name: "engrm",
|
|
22042
|
-
version: "0.4.
|
|
22432
|
+
version: "0.4.32"
|
|
22043
22433
|
});
|
|
22044
22434
|
server.tool("save_observation", "Save an observation to memory", {
|
|
22045
22435
|
type: exports_external.enum([
|
|
@@ -22484,7 +22874,138 @@ ${rows}`
|
|
|
22484
22874
|
]
|
|
22485
22875
|
};
|
|
22486
22876
|
});
|
|
22487
|
-
server.tool("
|
|
22877
|
+
server.tool("list_recall_items", "USE FIRST when continuity feels fuzzy. List the best current handoffs, session threads, chat snippets, and memory entries before opening one exact item.", {
|
|
22878
|
+
cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
|
|
22879
|
+
project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
|
|
22880
|
+
user_id: exports_external.string().optional().describe("Optional user override"),
|
|
22881
|
+
limit: exports_external.number().optional().describe("Max recall items to list")
|
|
22882
|
+
}, async (params) => {
|
|
22883
|
+
const result = listRecallItems(db, {
|
|
22884
|
+
cwd: params.cwd ?? process.cwd(),
|
|
22885
|
+
project_scoped: params.project_scoped,
|
|
22886
|
+
user_id: params.user_id ?? config2.user_id,
|
|
22887
|
+
current_device_id: config2.device_id,
|
|
22888
|
+
limit: params.limit
|
|
22889
|
+
});
|
|
22890
|
+
if (result.items.length === 0) {
|
|
22891
|
+
return {
|
|
22892
|
+
content: [
|
|
22893
|
+
{
|
|
22894
|
+
type: "text",
|
|
22895
|
+
text: result.project ? `No recall items found yet for project ${result.project}` : "No recall items found yet."
|
|
22896
|
+
}
|
|
22897
|
+
]
|
|
22898
|
+
};
|
|
22899
|
+
}
|
|
22900
|
+
const projectLine = result.project ? `Project: ${result.project}
|
|
22901
|
+
` : "";
|
|
22902
|
+
const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
|
|
22903
|
+
${item.detail}`).join(`
|
|
22904
|
+
`);
|
|
22905
|
+
return {
|
|
22906
|
+
content: [
|
|
22907
|
+
{
|
|
22908
|
+
type: "text",
|
|
22909
|
+
text: `${projectLine}Recall index (${result.continuity_mode} mode):
|
|
22910
|
+
` + `${rows}
|
|
22911
|
+
|
|
22912
|
+
` + `Suggested next step: use load_handoff for handoff:* items, get_observations([...]) for obs:* items, or resume_thread when you want one merged resume point.`
|
|
22913
|
+
}
|
|
22914
|
+
]
|
|
22915
|
+
};
|
|
22916
|
+
});
|
|
22917
|
+
server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact recall item key so you can inspect a specific handoff, thread, chat message, or memory entry without fuzzy recall guessing.", {
|
|
22918
|
+
key: exports_external.string().describe("Exact recall key from list_recall_items, such as handoff:12, session:sess-1, chat:55, or obs:402"),
|
|
22919
|
+
cwd: exports_external.string().optional().describe("Optional cwd override"),
|
|
22920
|
+
user_id: exports_external.string().optional().describe("Optional user override")
|
|
22921
|
+
}, async (params) => {
|
|
22922
|
+
const result = loadRecallItem(db, {
|
|
22923
|
+
key: params.key,
|
|
22924
|
+
cwd: params.cwd ?? process.cwd(),
|
|
22925
|
+
user_id: params.user_id ?? config2.user_id,
|
|
22926
|
+
current_device_id: config2.device_id
|
|
22927
|
+
});
|
|
22928
|
+
if (!result.payload) {
|
|
22929
|
+
return {
|
|
22930
|
+
content: [
|
|
22931
|
+
{
|
|
22932
|
+
type: "text",
|
|
22933
|
+
text: `No recall item found for ${params.key}`
|
|
22934
|
+
}
|
|
22935
|
+
]
|
|
22936
|
+
};
|
|
22937
|
+
}
|
|
22938
|
+
if (result.payload.type === "handoff") {
|
|
22939
|
+
return {
|
|
22940
|
+
content: [
|
|
22941
|
+
{
|
|
22942
|
+
type: "text",
|
|
22943
|
+
text: `Recall item ${result.key} [handoff]
|
|
22944
|
+
` + `Title: ${result.title}
|
|
22945
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
22946
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
22947
|
+
|
|
22948
|
+
` + `${result.payload.narrative ?? "(no narrative)"}`
|
|
22949
|
+
}
|
|
22950
|
+
]
|
|
22951
|
+
};
|
|
22952
|
+
}
|
|
22953
|
+
if (result.payload.type === "thread") {
|
|
22954
|
+
const outcomes = result.payload.recent_outcomes.length > 0 ? result.payload.recent_outcomes.map((item) => `- ${item}`).join(`
|
|
22955
|
+
`) : "- (none)";
|
|
22956
|
+
const hotFiles = result.payload.hot_files.length > 0 ? result.payload.hot_files.map((item) => `- ${item.path}${item.count > 1 ? ` (${item.count})` : ""}`).join(`
|
|
22957
|
+
`) : "- (none)";
|
|
22958
|
+
return {
|
|
22959
|
+
content: [
|
|
22960
|
+
{
|
|
22961
|
+
type: "text",
|
|
22962
|
+
text: `Recall item ${result.key} [thread]
|
|
22963
|
+
` + `Title: ${result.title}
|
|
22964
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
22965
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
22966
|
+
` + `Latest request: ${result.payload.latest_request ?? "(none)"}
|
|
22967
|
+
` + `Current thread: ${result.payload.current_thread ?? "(none)"}
|
|
22968
|
+
|
|
22969
|
+
` + `Recent outcomes:
|
|
22970
|
+
${outcomes}
|
|
22971
|
+
|
|
22972
|
+
` + `Hot files:
|
|
22973
|
+
${hotFiles}`
|
|
22974
|
+
}
|
|
22975
|
+
]
|
|
22976
|
+
};
|
|
22977
|
+
}
|
|
22978
|
+
if (result.payload.type === "chat") {
|
|
22979
|
+
return {
|
|
22980
|
+
content: [
|
|
22981
|
+
{
|
|
22982
|
+
type: "text",
|
|
22983
|
+
text: `Recall item ${result.key} [chat]
|
|
22984
|
+
` + `Title: ${result.title}
|
|
22985
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
22986
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
22987
|
+
|
|
22988
|
+
` + `${result.payload.content}`
|
|
22989
|
+
}
|
|
22990
|
+
]
|
|
22991
|
+
};
|
|
22992
|
+
}
|
|
22993
|
+
return {
|
|
22994
|
+
content: [
|
|
22995
|
+
{
|
|
22996
|
+
type: "text",
|
|
22997
|
+
text: `Recall item ${result.key} [memory]
|
|
22998
|
+
` + `Title: ${result.title}
|
|
22999
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
23000
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
23001
|
+
` + `Type: ${result.payload.observation_type}
|
|
23002
|
+
|
|
23003
|
+
` + `${result.payload.narrative ?? result.payload.facts ?? "(no detail)"}`
|
|
23004
|
+
}
|
|
23005
|
+
]
|
|
23006
|
+
};
|
|
23007
|
+
});
|
|
23008
|
+
server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?' answer. Build a clear resume point for the current project by combining handoff, live recall, current thread, and recent chat continuity.", {
|
|
22488
23009
|
cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
|
|
22489
23010
|
limit: exports_external.number().optional().describe("Max recall hits/chat snippets to include"),
|
|
22490
23011
|
user_id: exports_external.string().optional().describe("Optional user override"),
|
|
@@ -22871,6 +23392,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
22871
23392
|
const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
22872
23393
|
`) : "- (none)";
|
|
22873
23394
|
const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
23395
|
+
`) : "- (none)";
|
|
23396
|
+
const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
|
|
22874
23397
|
`) : "- (none)";
|
|
22875
23398
|
const projectLine = result.project ? `Project: ${result.project}
|
|
22876
23399
|
|
|
@@ -22885,6 +23408,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
22885
23408
|
{
|
|
22886
23409
|
type: "text",
|
|
22887
23410
|
text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
|
|
23411
|
+
` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
|
|
22888
23412
|
` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
|
|
22889
23413
|
` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat.length} messages across ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, history ${result.chat_source_summary.history}, hook ${result.chat_source_summary.hook})
|
|
22890
23414
|
` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
|
|
@@ -22892,6 +23416,9 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
22892
23416
|
` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
|
|
22893
23417
|
` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
|
|
22894
23418
|
|
|
23419
|
+
` + `Recall preview:
|
|
23420
|
+
${recallPreviewLines}
|
|
23421
|
+
|
|
22895
23422
|
` + `Next actions:
|
|
22896
23423
|
${result.resume_next_actions.length > 0 ? result.resume_next_actions.map((item) => `- ${item}`).join(`
|
|
22897
23424
|
`) : "- (none)"}
|
|
@@ -23089,6 +23616,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
|
|
|
23089
23616
|
text: `Project: ${result.project_name}
|
|
23090
23617
|
` + `Canonical ID: ${result.canonical_id}
|
|
23091
23618
|
` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
|
|
23619
|
+
` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
|
|
23092
23620
|
` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
|
|
23093
23621
|
` + `Loaded observations: ${result.session_count}
|
|
23094
23622
|
` + `Searchable total: ${result.total_active}
|
|
@@ -23101,6 +23629,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
|
|
|
23101
23629
|
` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, history ${result.chat_source_summary.history}, hook ${result.chat_source_summary.hook})
|
|
23102
23630
|
` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
|
|
23103
23631
|
` + `Next actions: ${result.resume_next_actions.length > 0 ? result.resume_next_actions.join(" | ") : "(none)"}
|
|
23632
|
+
` + `Recall preview: ${result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => item.key).join(", ") : "(none)"}
|
|
23104
23633
|
` + `Raw chronology active: ${result.raw_capture_active ? "yes" : "no"}
|
|
23105
23634
|
|
|
23106
23635
|
` + result.preview
|
|
@@ -23165,6 +23694,8 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
23165
23694
|
const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
23166
23695
|
`) : "- (none)";
|
|
23167
23696
|
const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
|
|
23697
|
+
`) : "- (none)";
|
|
23698
|
+
const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
|
|
23168
23699
|
`) : "- (none)";
|
|
23169
23700
|
return {
|
|
23170
23701
|
content: [
|
|
@@ -23173,6 +23704,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
23173
23704
|
text: `Project: ${result.project}
|
|
23174
23705
|
` + `Canonical ID: ${result.canonical_id}
|
|
23175
23706
|
` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
|
|
23707
|
+
` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
|
|
23176
23708
|
` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
|
|
23177
23709
|
` + `Recent requests captured: ${result.recent_requests_count}
|
|
23178
23710
|
` + `Recent tools captured: ${result.recent_tools_count}
|
|
@@ -23188,6 +23720,9 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
23188
23720
|
` + `Estimated read cost: ~${result.estimated_read_tokens}t
|
|
23189
23721
|
` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
|
|
23190
23722
|
|
|
23723
|
+
` + `Recall preview:
|
|
23724
|
+
${recallPreviewLines}
|
|
23725
|
+
|
|
23191
23726
|
` + `Next actions:
|
|
23192
23727
|
${result.resume_next_actions.length > 0 ? result.resume_next_actions.map((item) => `- ${item}`).join(`
|
|
23193
23728
|
`) : "- (none)"}
|
|
@@ -23368,7 +23903,7 @@ Transcript messages seen: ${result.total}`
|
|
|
23368
23903
|
]
|
|
23369
23904
|
};
|
|
23370
23905
|
});
|
|
23371
|
-
server.tool("repair_recall", "Rehydrate recent session recall for the current project from Claude transcripts or history fallback
|
|
23906
|
+
server.tool("repair_recall", "USE WHEN recall feels thin or under-captured. Rehydrate recent session recall for the current project from Claude transcripts or history fallback before resuming.", {
|
|
23372
23907
|
session_id: exports_external.string().optional().describe("Optional single session ID to repair instead of scanning recent project sessions"),
|
|
23373
23908
|
cwd: exports_external.string().optional().describe("Project directory used to resolve project sessions and Claude history/transcript files"),
|
|
23374
23909
|
limit: exports_external.number().optional().describe("How many recent sessions to inspect when repairing a project"),
|
package/package.json
CHANGED