engrm 0.4.31 → 0.4.33
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 +28 -3
- package/dist/hooks/pre-compact.js +27 -0
- package/dist/hooks/session-start.js +145 -3
- package/dist/hooks/stop.js +1 -1
- package/dist/server.js +583 -11
- 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.
|
|
@@ -381,7 +403,10 @@ What each tool is good for:
|
|
|
381
403
|
- `capture_status` tells you whether prompt/tool hooks are live on this machine
|
|
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
|
-
- `resume_thread` is the fastest “get me back into the live thread” path when you want freshness, source, next actions, tool trail, and
|
|
406
|
+
- `resume_thread` is the fastest “get me back into the live thread” path when you want freshness, source, next actions, tool trail, chat, and one exact `load_recall_item(...)` suggestion 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
|
|
409
|
+
- `memory_console`, `project_memory_index`, and `session_context` now also surface one best exact `load_recall_item(...)` jump, so the workbench can hand you the right deterministic next step instead of only showing recall counts
|
|
385
410
|
- `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
411
|
- `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
412
|
- 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.33";
|
|
3094
3148
|
function hashFile(filePath) {
|
|
3095
3149
|
try {
|
|
3096
3150
|
if (!existsSync3(filePath))
|
|
@@ -5563,7 +5617,15 @@ function formatSplashScreen(data) {
|
|
|
5563
5617
|
lines.push(` ${line}`);
|
|
5564
5618
|
}
|
|
5565
5619
|
}
|
|
5566
|
-
const
|
|
5620
|
+
const recallItems = buildStartupRecallItems(data.context);
|
|
5621
|
+
const recallPreview = formatStartupRecallPreview(recallItems);
|
|
5622
|
+
if (recallPreview.length > 0) {
|
|
5623
|
+
lines.push("");
|
|
5624
|
+
for (const line of recallPreview) {
|
|
5625
|
+
lines.push(` ${line}`);
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
const inspectHints = formatInspectHints(data.context, contextIndex.observationIds, recallItems);
|
|
5567
5629
|
if (inspectHints.length > 0) {
|
|
5568
5630
|
lines.push("");
|
|
5569
5631
|
for (const line of inspectHints) {
|
|
@@ -5782,7 +5844,7 @@ function formatContextIndex(context, shownItems) {
|
|
|
5782
5844
|
observationIds: selected.map((obs) => obs.id)
|
|
5783
5845
|
};
|
|
5784
5846
|
}
|
|
5785
|
-
function formatInspectHints(context, visibleObservationIds = []) {
|
|
5847
|
+
function formatInspectHints(context, visibleObservationIds = [], recallItems = []) {
|
|
5786
5848
|
const hints = [];
|
|
5787
5849
|
const continuityState = getStartupContinuityState(context);
|
|
5788
5850
|
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
@@ -5794,6 +5856,8 @@ function formatInspectHints(context, visibleObservationIds = []) {
|
|
|
5794
5856
|
hints.push("activity_feed");
|
|
5795
5857
|
}
|
|
5796
5858
|
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
|
|
5859
|
+
hints.push("list_recall_items");
|
|
5860
|
+
hints.push("load_recall_item");
|
|
5797
5861
|
hints.push("resume_thread");
|
|
5798
5862
|
hints.push("search_recall");
|
|
5799
5863
|
}
|
|
@@ -5819,12 +5883,90 @@ function formatInspectHints(context, visibleObservationIds = []) {
|
|
|
5819
5883
|
if (unique.length === 0)
|
|
5820
5884
|
return [];
|
|
5821
5885
|
const ids = visibleObservationIds.slice(0, 5);
|
|
5886
|
+
const openNowItem = recallItems.find((item) => item.kind !== "memory") ?? null;
|
|
5822
5887
|
const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
|
|
5823
5888
|
return [
|
|
5824
5889
|
`${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
|
|
5890
|
+
...openNowItem ? [`${c2.dim}Open now:${c2.reset} load_recall_item("${openNowItem.key}")`] : [],
|
|
5825
5891
|
...fetchHint ? [`${c2.dim}Pull detail:${c2.reset} ${fetchHint}`] : []
|
|
5826
5892
|
];
|
|
5827
5893
|
}
|
|
5894
|
+
function formatStartupRecallPreview(recallItems) {
|
|
5895
|
+
const items = recallItems.slice(0, 3);
|
|
5896
|
+
if (items.length === 0)
|
|
5897
|
+
return [];
|
|
5898
|
+
return [
|
|
5899
|
+
`${c2.dim}Recall preview:${c2.reset} exact keys you can open now`,
|
|
5900
|
+
...items.map((item) => `${item.key} [${item.kind} \xB7 ${item.freshness}] ${truncateInline(item.title, 110)}`)
|
|
5901
|
+
];
|
|
5902
|
+
}
|
|
5903
|
+
function buildStartupRecallItems(context) {
|
|
5904
|
+
const items = [];
|
|
5905
|
+
for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
|
|
5906
|
+
const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+\u00B7\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
5907
|
+
if (!title)
|
|
5908
|
+
continue;
|
|
5909
|
+
const freshness = classifyResumeFreshness(handoff.created_at_epoch);
|
|
5910
|
+
items.push({
|
|
5911
|
+
key: `handoff:${handoff.id}`,
|
|
5912
|
+
kind: "handoff",
|
|
5913
|
+
freshness,
|
|
5914
|
+
title,
|
|
5915
|
+
score: freshnessScore(freshness) + 40
|
|
5916
|
+
});
|
|
5917
|
+
}
|
|
5918
|
+
for (const session of context.recentSessions?.slice(0, 2) ?? []) {
|
|
5919
|
+
const title = chooseMeaningfulSessionSummary(session.request, session.completed);
|
|
5920
|
+
if (!title)
|
|
5921
|
+
continue;
|
|
5922
|
+
const sourceEpoch = session.completed_at_epoch ?? session.started_at_epoch ?? null;
|
|
5923
|
+
const freshness = classifyResumeFreshness(sourceEpoch);
|
|
5924
|
+
items.push({
|
|
5925
|
+
key: `session:${session.session_id}`,
|
|
5926
|
+
kind: "thread",
|
|
5927
|
+
freshness,
|
|
5928
|
+
title,
|
|
5929
|
+
score: freshnessScore(freshness) + 30
|
|
5930
|
+
});
|
|
5931
|
+
}
|
|
5932
|
+
for (const message of context.recentChatMessages?.slice(-2) ?? []) {
|
|
5933
|
+
if (!message.content.trim())
|
|
5934
|
+
continue;
|
|
5935
|
+
const freshness = classifyResumeFreshness(message.created_at_epoch);
|
|
5936
|
+
items.push({
|
|
5937
|
+
key: `chat:${message.id}`,
|
|
5938
|
+
kind: "chat",
|
|
5939
|
+
freshness,
|
|
5940
|
+
title: `[${message.role}] ${message.content.replace(/\s+/g, " ").trim()}`,
|
|
5941
|
+
score: freshnessScore(freshness) + 20
|
|
5942
|
+
});
|
|
5943
|
+
}
|
|
5944
|
+
for (const obs of pickContextIndexObservations(context).slice(0, 2)) {
|
|
5945
|
+
const createdAtEpoch = Math.floor(new Date(obs.created_at).getTime() / 1000);
|
|
5946
|
+
const freshness = classifyResumeFreshness(Number.isFinite(createdAtEpoch) ? createdAtEpoch : null);
|
|
5947
|
+
items.push({
|
|
5948
|
+
key: `obs:${obs.id}`,
|
|
5949
|
+
kind: "memory",
|
|
5950
|
+
freshness,
|
|
5951
|
+
title: obs.title,
|
|
5952
|
+
score: freshnessScore(freshness) + 10
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
const seen = new Set;
|
|
5956
|
+
return items.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)).filter((item) => {
|
|
5957
|
+
if (seen.has(item.key))
|
|
5958
|
+
return false;
|
|
5959
|
+
seen.add(item.key);
|
|
5960
|
+
return true;
|
|
5961
|
+
}).map(({ score: _score, ...item }) => item);
|
|
5962
|
+
}
|
|
5963
|
+
function freshnessScore(freshness) {
|
|
5964
|
+
if (freshness === "live")
|
|
5965
|
+
return 3;
|
|
5966
|
+
if (freshness === "recent")
|
|
5967
|
+
return 2;
|
|
5968
|
+
return 1;
|
|
5969
|
+
}
|
|
5828
5970
|
function rememberShownItem(shown, value) {
|
|
5829
5971
|
if (!value)
|
|
5830
5972
|
return;
|
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.33",
|
|
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);
|
|
@@ -18388,11 +18602,23 @@ function getProjectMemoryIndex(db, input) {
|
|
|
18388
18602
|
`));
|
|
18389
18603
|
const continuityState = classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount.length, recentChatCount, recentSessions, recentOutcomes.length);
|
|
18390
18604
|
const sourceTimestamp = pickResumeSourceTimestamp(latestSession, recentChat.messages);
|
|
18605
|
+
const bestRecallItem = pickBestRecallItem(recallIndex.items);
|
|
18391
18606
|
return {
|
|
18392
18607
|
project: project.name,
|
|
18393
18608
|
canonical_id: project.canonical_id,
|
|
18394
18609
|
continuity_state: continuityState,
|
|
18395
18610
|
continuity_summary: describeContinuityState(continuityState),
|
|
18611
|
+
recall_mode: recallIndex.continuity_mode,
|
|
18612
|
+
recall_items_ready: recallIndex.items.length,
|
|
18613
|
+
recall_index_preview: recallIndex.items.slice(0, 3).map((item) => ({
|
|
18614
|
+
key: item.key,
|
|
18615
|
+
kind: item.kind,
|
|
18616
|
+
freshness: item.freshness,
|
|
18617
|
+
title: item.title
|
|
18618
|
+
})),
|
|
18619
|
+
best_recall_key: bestRecallItem?.key ?? null,
|
|
18620
|
+
best_recall_title: bestRecallItem?.title ?? null,
|
|
18621
|
+
best_recall_kind: bestRecallItem?.kind ?? null,
|
|
18396
18622
|
resume_freshness: classifyResumeFreshness(sourceTimestamp),
|
|
18397
18623
|
resume_source_session_id: latestSession?.session_id ?? null,
|
|
18398
18624
|
resume_source_device_id: latestSession?.device_id ?? null,
|
|
@@ -18421,6 +18647,9 @@ function getProjectMemoryIndex(db, input) {
|
|
|
18421
18647
|
suggested_tools: suggestedTools
|
|
18422
18648
|
};
|
|
18423
18649
|
}
|
|
18650
|
+
function pickBestRecallItem(items) {
|
|
18651
|
+
return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
|
|
18652
|
+
}
|
|
18424
18653
|
function pickResumeSourceTimestamp(latestSession, messages) {
|
|
18425
18654
|
const latestChatEpoch = messages.length > 0 ? messages[messages.length - 1]?.created_at_epoch ?? null : null;
|
|
18426
18655
|
return latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
|
|
@@ -18475,7 +18704,7 @@ function extractPaths(value) {
|
|
|
18475
18704
|
return [];
|
|
18476
18705
|
}
|
|
18477
18706
|
}
|
|
18478
|
-
function
|
|
18707
|
+
function looksLikeFileOperationTitle4(value) {
|
|
18479
18708
|
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
18480
18709
|
}
|
|
18481
18710
|
function summarizeCaptureState(sessions) {
|
|
@@ -18512,6 +18741,8 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
|
|
|
18512
18741
|
suggested.push("activity_feed");
|
|
18513
18742
|
}
|
|
18514
18743
|
if (requestCount > 0 || recentChatCount > 0 || observationCount > 0) {
|
|
18744
|
+
suggested.push("list_recall_items");
|
|
18745
|
+
suggested.push("load_recall_item");
|
|
18515
18746
|
suggested.push("resume_thread");
|
|
18516
18747
|
suggested.push("search_recall");
|
|
18517
18748
|
}
|
|
@@ -18577,6 +18808,12 @@ function getMemoryConsole(db, input) {
|
|
|
18577
18808
|
user_id: input.user_id,
|
|
18578
18809
|
limit: 6
|
|
18579
18810
|
});
|
|
18811
|
+
const recallIndex = listRecallItems(db, {
|
|
18812
|
+
cwd,
|
|
18813
|
+
project_scoped: projectScoped,
|
|
18814
|
+
user_id: input.user_id,
|
|
18815
|
+
limit: 10
|
|
18816
|
+
});
|
|
18580
18817
|
const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
|
|
18581
18818
|
cwd,
|
|
18582
18819
|
user_id: input.user_id
|
|
@@ -18587,6 +18824,17 @@ function getMemoryConsole(db, input) {
|
|
|
18587
18824
|
capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
|
|
18588
18825
|
continuity_state: continuityState,
|
|
18589
18826
|
continuity_summary: projectIndex?.continuity_summary ?? describeContinuityState(continuityState),
|
|
18827
|
+
recall_mode: projectIndex?.recall_mode ?? recallIndex.continuity_mode,
|
|
18828
|
+
recall_items_ready: projectIndex?.recall_items_ready ?? recallIndex.items.length,
|
|
18829
|
+
recall_index_preview: projectIndex?.recall_index_preview ?? recallIndex.items.slice(0, 3).map((item) => ({
|
|
18830
|
+
key: item.key,
|
|
18831
|
+
kind: item.kind,
|
|
18832
|
+
freshness: item.freshness,
|
|
18833
|
+
title: item.title
|
|
18834
|
+
})),
|
|
18835
|
+
best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
|
|
18836
|
+
best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
|
|
18837
|
+
best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.kind ?? null,
|
|
18590
18838
|
resume_freshness: projectIndex?.resume_freshness ?? "stale",
|
|
18591
18839
|
resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
|
|
18592
18840
|
resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
|
|
@@ -18620,7 +18868,9 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
|
|
|
18620
18868
|
if (requestCount > 0 || toolCount > 0)
|
|
18621
18869
|
suggested.push("activity_feed");
|
|
18622
18870
|
if (requestCount > 0 || chatCount > 0 || observationCount > 0)
|
|
18623
|
-
suggested.push("resume_thread", "search_recall");
|
|
18871
|
+
suggested.push("load_recall_item", "resume_thread", "search_recall");
|
|
18872
|
+
if (requestCount > 0 || chatCount > 0 || observationCount > 0)
|
|
18873
|
+
suggested.unshift("list_recall_items");
|
|
18624
18874
|
if ((sessionCount > 0 || chatCount > 0) && chatCoverageState !== "transcript-backed")
|
|
18625
18875
|
suggested.push("repair_recall");
|
|
18626
18876
|
if (observationCount > 0)
|
|
@@ -19467,6 +19717,13 @@ function getSessionContext(db, input) {
|
|
|
19467
19717
|
limit: 8
|
|
19468
19718
|
});
|
|
19469
19719
|
const recentChatMessages = recentChat.messages.length;
|
|
19720
|
+
const recallIndex = listRecallItems(db, {
|
|
19721
|
+
cwd,
|
|
19722
|
+
project_scoped: true,
|
|
19723
|
+
user_id: input.user_id,
|
|
19724
|
+
current_device_id: input.current_device_id,
|
|
19725
|
+
limit: 10
|
|
19726
|
+
});
|
|
19470
19727
|
const latestSession = context.recentSessions?.[0] ?? null;
|
|
19471
19728
|
const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
|
|
19472
19729
|
const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
|
|
@@ -19474,11 +19731,23 @@ function getSessionContext(db, input) {
|
|
|
19474
19731
|
const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
|
|
19475
19732
|
const latestChatEpoch = recentChat.messages.length > 0 ? recentChat.messages[recentChat.messages.length - 1]?.created_at_epoch ?? null : null;
|
|
19476
19733
|
const resumeTimestamp = latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
|
|
19734
|
+
const bestRecallItem = recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null;
|
|
19477
19735
|
return {
|
|
19478
19736
|
project_name: context.project_name,
|
|
19479
19737
|
canonical_id: context.canonical_id,
|
|
19480
19738
|
continuity_state: continuityState,
|
|
19481
19739
|
continuity_summary: describeContinuityState(continuityState),
|
|
19740
|
+
recall_mode: recallIndex.continuity_mode,
|
|
19741
|
+
recall_items_ready: recallIndex.items.length,
|
|
19742
|
+
recall_index_preview: recallIndex.items.slice(0, 3).map((item) => ({
|
|
19743
|
+
key: item.key,
|
|
19744
|
+
kind: item.kind,
|
|
19745
|
+
freshness: item.freshness,
|
|
19746
|
+
title: item.title
|
|
19747
|
+
})),
|
|
19748
|
+
best_recall_key: bestRecallItem?.key ?? null,
|
|
19749
|
+
best_recall_title: bestRecallItem?.title ?? null,
|
|
19750
|
+
best_recall_kind: bestRecallItem?.kind ?? null,
|
|
19482
19751
|
resume_freshness: classifyResumeFreshness(resumeTimestamp),
|
|
19483
19752
|
resume_source_session_id: latestSession?.session_id ?? null,
|
|
19484
19753
|
resume_source_device_id: latestSession?.device_id ?? null,
|
|
@@ -19541,6 +19810,8 @@ function buildSuggestedTools2(context, chatCoverageState) {
|
|
|
19541
19810
|
tools.push("activity_feed");
|
|
19542
19811
|
}
|
|
19543
19812
|
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
|
|
19813
|
+
tools.push("list_recall_items");
|
|
19814
|
+
tools.push("load_recall_item");
|
|
19544
19815
|
tools.push("resume_thread");
|
|
19545
19816
|
tools.push("search_recall");
|
|
19546
19817
|
}
|
|
@@ -19565,6 +19836,139 @@ function buildSuggestedTools2(context, chatCoverageState) {
|
|
|
19565
19836
|
return Array.from(new Set(tools)).slice(0, 5);
|
|
19566
19837
|
}
|
|
19567
19838
|
|
|
19839
|
+
// src/tools/load-recall-item.ts
|
|
19840
|
+
function loadRecallItem(db, input) {
|
|
19841
|
+
const [kind, rawId] = input.key.split(":", 2);
|
|
19842
|
+
if (!kind || !rawId) {
|
|
19843
|
+
return {
|
|
19844
|
+
kind: "unknown",
|
|
19845
|
+
key: input.key,
|
|
19846
|
+
title: null,
|
|
19847
|
+
detail: "Malformed recall key",
|
|
19848
|
+
session_id: null,
|
|
19849
|
+
source_device_id: null,
|
|
19850
|
+
payload: null
|
|
19851
|
+
};
|
|
19852
|
+
}
|
|
19853
|
+
if (kind === "handoff") {
|
|
19854
|
+
const id = Number.parseInt(rawId, 10);
|
|
19855
|
+
const result = loadHandoff(db, {
|
|
19856
|
+
id,
|
|
19857
|
+
cwd: input.cwd,
|
|
19858
|
+
user_id: input.user_id,
|
|
19859
|
+
current_device_id: input.current_device_id
|
|
19860
|
+
});
|
|
19861
|
+
if (!result.handoff) {
|
|
19862
|
+
return missing(input.key, "handoff");
|
|
19863
|
+
}
|
|
19864
|
+
return {
|
|
19865
|
+
kind: "handoff",
|
|
19866
|
+
key: input.key,
|
|
19867
|
+
title: result.handoff.title,
|
|
19868
|
+
detail: summarizeNarrative(result.handoff.narrative),
|
|
19869
|
+
session_id: result.handoff.session_id ?? null,
|
|
19870
|
+
source_device_id: result.handoff.device_id ?? null,
|
|
19871
|
+
payload: {
|
|
19872
|
+
type: "handoff",
|
|
19873
|
+
handoff_id: result.handoff.id,
|
|
19874
|
+
narrative: result.handoff.narrative ?? null
|
|
19875
|
+
}
|
|
19876
|
+
};
|
|
19877
|
+
}
|
|
19878
|
+
if (kind === "session") {
|
|
19879
|
+
const story = getSessionStory(db, { session_id: rawId });
|
|
19880
|
+
if (!story.session) {
|
|
19881
|
+
return missing(input.key, "thread");
|
|
19882
|
+
}
|
|
19883
|
+
return {
|
|
19884
|
+
kind: "thread",
|
|
19885
|
+
key: input.key,
|
|
19886
|
+
title: story.summary?.current_thread ?? story.latest_request ?? story.summary?.completed ?? story.session.session_id,
|
|
19887
|
+
detail: story.summary?.next_steps ?? story.summary?.completed ?? null,
|
|
19888
|
+
session_id: story.session.session_id,
|
|
19889
|
+
source_device_id: story.session.device_id ?? null,
|
|
19890
|
+
payload: {
|
|
19891
|
+
type: "thread",
|
|
19892
|
+
latest_request: story.latest_request,
|
|
19893
|
+
current_thread: story.summary?.current_thread ?? null,
|
|
19894
|
+
recent_outcomes: story.recent_outcomes,
|
|
19895
|
+
hot_files: story.hot_files
|
|
19896
|
+
}
|
|
19897
|
+
};
|
|
19898
|
+
}
|
|
19899
|
+
if (kind === "chat") {
|
|
19900
|
+
const id = Number.parseInt(rawId, 10);
|
|
19901
|
+
const messages = getRecentChat(db, {
|
|
19902
|
+
cwd: input.cwd,
|
|
19903
|
+
project_scoped: false,
|
|
19904
|
+
user_id: input.user_id,
|
|
19905
|
+
limit: 200
|
|
19906
|
+
}).messages;
|
|
19907
|
+
const message = messages.find((item) => item.id === id);
|
|
19908
|
+
if (!message) {
|
|
19909
|
+
return missing(input.key, "chat");
|
|
19910
|
+
}
|
|
19911
|
+
const source = message.source_kind === "transcript" ? "transcript" : message.remote_source_id?.startsWith("history:") ? "history" : "hook";
|
|
19912
|
+
return {
|
|
19913
|
+
kind: "chat",
|
|
19914
|
+
key: input.key,
|
|
19915
|
+
title: `${message.role} [${source}]`,
|
|
19916
|
+
detail: message.content,
|
|
19917
|
+
session_id: message.session_id,
|
|
19918
|
+
source_device_id: message.device_id ?? null,
|
|
19919
|
+
payload: {
|
|
19920
|
+
type: "chat",
|
|
19921
|
+
role: message.role,
|
|
19922
|
+
content: message.content,
|
|
19923
|
+
source
|
|
19924
|
+
}
|
|
19925
|
+
};
|
|
19926
|
+
}
|
|
19927
|
+
if (kind === "obs") {
|
|
19928
|
+
const id = Number.parseInt(rawId, 10);
|
|
19929
|
+
const result = getObservations(db, {
|
|
19930
|
+
ids: [id],
|
|
19931
|
+
user_id: input.user_id
|
|
19932
|
+
});
|
|
19933
|
+
const obs = result.observations[0];
|
|
19934
|
+
if (!obs) {
|
|
19935
|
+
return missing(input.key, "memory");
|
|
19936
|
+
}
|
|
19937
|
+
return {
|
|
19938
|
+
kind: "memory",
|
|
19939
|
+
key: input.key,
|
|
19940
|
+
title: obs.title,
|
|
19941
|
+
detail: obs.narrative ?? obs.facts ?? null,
|
|
19942
|
+
session_id: obs.session_id ?? null,
|
|
19943
|
+
source_device_id: obs.device_id ?? null,
|
|
19944
|
+
payload: {
|
|
19945
|
+
type: "memory",
|
|
19946
|
+
observation_id: obs.id,
|
|
19947
|
+
observation_type: obs.type,
|
|
19948
|
+
narrative: obs.narrative ?? null,
|
|
19949
|
+
facts: obs.facts ?? null
|
|
19950
|
+
}
|
|
19951
|
+
};
|
|
19952
|
+
}
|
|
19953
|
+
return missing(input.key, "unknown");
|
|
19954
|
+
}
|
|
19955
|
+
function summarizeNarrative(value) {
|
|
19956
|
+
if (!value)
|
|
19957
|
+
return null;
|
|
19958
|
+
return value.split(/\n+/).map((line) => line.trim()).find(Boolean) ?? null;
|
|
19959
|
+
}
|
|
19960
|
+
function missing(key, kind) {
|
|
19961
|
+
return {
|
|
19962
|
+
kind,
|
|
19963
|
+
key,
|
|
19964
|
+
title: null,
|
|
19965
|
+
detail: "Recall item not found",
|
|
19966
|
+
session_id: null,
|
|
19967
|
+
source_device_id: null,
|
|
19968
|
+
payload: null
|
|
19969
|
+
};
|
|
19970
|
+
}
|
|
19971
|
+
|
|
19568
19972
|
// src/tools/capture-git-worktree.ts
|
|
19569
19973
|
import { execSync as execSync2 } from "node:child_process";
|
|
19570
19974
|
import { existsSync as existsSync4 } from "node:fs";
|
|
@@ -20000,6 +20404,7 @@ async function resumeThread(db, config2, input = {}) {
|
|
|
20000
20404
|
}
|
|
20001
20405
|
}
|
|
20002
20406
|
const { context, handoff, recentChat, recentSessions, recall } = snapshot;
|
|
20407
|
+
const bestRecallItem = pickBestRecallItem2(snapshot.recallIndex.items);
|
|
20003
20408
|
const latestSession = recentSessions[0] ?? null;
|
|
20004
20409
|
const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
|
|
20005
20410
|
const inferredRequest = latestSession?.request?.trim() || null;
|
|
@@ -20028,6 +20433,7 @@ async function resumeThread(db, config2, input = {}) {
|
|
|
20028
20433
|
currentThread
|
|
20029
20434
|
});
|
|
20030
20435
|
const suggestedTools = Array.from(new Set([
|
|
20436
|
+
...bestRecallItem ? ["load_recall_item"] : [],
|
|
20031
20437
|
"search_recall",
|
|
20032
20438
|
...recentChat.coverage_state !== "transcript-backed" && recentChat.messages.length > 0 ? ["repair_recall", "refresh_chat_recall"] : [],
|
|
20033
20439
|
...handoff ? ["load_handoff"] : [],
|
|
@@ -20042,6 +20448,9 @@ async function resumeThread(db, config2, input = {}) {
|
|
|
20042
20448
|
resume_source_device_id: handoff?.device_id ?? latestSession?.device_id ?? null,
|
|
20043
20449
|
resume_confidence: resumeConfidence,
|
|
20044
20450
|
resume_basis: resumeBasis,
|
|
20451
|
+
best_recall_key: bestRecallItem?.key ?? null,
|
|
20452
|
+
best_recall_title: bestRecallItem?.title ?? null,
|
|
20453
|
+
best_recall_kind: bestRecallItem?.kind ?? null,
|
|
20045
20454
|
repair_attempted: shouldRepair,
|
|
20046
20455
|
repair_result: repairResult ? {
|
|
20047
20456
|
imported_chat_messages: repairResult.imported_chat_messages,
|
|
@@ -20100,14 +20509,25 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
|
|
|
20100
20509
|
user_id: userId,
|
|
20101
20510
|
limit
|
|
20102
20511
|
});
|
|
20512
|
+
const recallIndex = listRecallItems(db, {
|
|
20513
|
+
cwd,
|
|
20514
|
+
project_scoped: true,
|
|
20515
|
+
user_id: userId,
|
|
20516
|
+
current_device_id: currentDeviceId,
|
|
20517
|
+
limit
|
|
20518
|
+
});
|
|
20103
20519
|
return {
|
|
20104
20520
|
context,
|
|
20105
20521
|
handoff: handoffResult.handoff,
|
|
20106
20522
|
recentChat,
|
|
20107
20523
|
recentSessions,
|
|
20108
|
-
recall
|
|
20524
|
+
recall,
|
|
20525
|
+
recallIndex
|
|
20109
20526
|
};
|
|
20110
20527
|
}
|
|
20528
|
+
function pickBestRecallItem2(items) {
|
|
20529
|
+
return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
|
|
20530
|
+
}
|
|
20111
20531
|
function extractCurrentThread(handoff) {
|
|
20112
20532
|
const narrative = handoff?.narrative ?? "";
|
|
20113
20533
|
const match = narrative.match(/Current thread:\s*(.+)/i);
|
|
@@ -21613,7 +22033,7 @@ var REFACTOR_HINTS = [
|
|
|
21613
22033
|
"extract",
|
|
21614
22034
|
"move"
|
|
21615
22035
|
];
|
|
21616
|
-
function
|
|
22036
|
+
function normalize2(text) {
|
|
21617
22037
|
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
21618
22038
|
}
|
|
21619
22039
|
function uniq(items) {
|
|
@@ -21642,7 +22062,7 @@ function countLinesByPrefix(diff, prefix) {
|
|
|
21642
22062
|
`).filter((line) => line.startsWith(prefix) && !line.startsWith(`${prefix}${prefix}${prefix}`)).length;
|
|
21643
22063
|
}
|
|
21644
22064
|
function detectType(summary, diff, files) {
|
|
21645
|
-
const corpus =
|
|
22065
|
+
const corpus = normalize2([summary ?? "", diff, files.join(" ")].join(" "));
|
|
21646
22066
|
if (BUGFIX_HINTS.some((hint) => corpus.includes(hint)))
|
|
21647
22067
|
return "bugfix";
|
|
21648
22068
|
if (DECISION_HINTS.some((hint) => corpus.includes(hint)))
|
|
@@ -21702,7 +22122,7 @@ function buildFacts(diff, files) {
|
|
|
21702
22122
|
if (added > 0 || removed > 0) {
|
|
21703
22123
|
facts.push(`Diff footprint: +${added} / -${removed}`);
|
|
21704
22124
|
}
|
|
21705
|
-
const lower =
|
|
22125
|
+
const lower = normalize2(diff);
|
|
21706
22126
|
if (lower.includes("auth") || lower.includes("token") || lower.includes("oauth")) {
|
|
21707
22127
|
facts.push("Touches authentication or credential flow");
|
|
21708
22128
|
}
|
|
@@ -22039,7 +22459,7 @@ process.on("SIGTERM", () => {
|
|
|
22039
22459
|
});
|
|
22040
22460
|
var server = new McpServer({
|
|
22041
22461
|
name: "engrm",
|
|
22042
|
-
version: "0.4.
|
|
22462
|
+
version: "0.4.33"
|
|
22043
22463
|
});
|
|
22044
22464
|
server.tool("save_observation", "Save an observation to memory", {
|
|
22045
22465
|
type: exports_external.enum([
|
|
@@ -22484,7 +22904,138 @@ ${rows}`
|
|
|
22484
22904
|
]
|
|
22485
22905
|
};
|
|
22486
22906
|
});
|
|
22487
|
-
server.tool("
|
|
22907
|
+
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.", {
|
|
22908
|
+
cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
|
|
22909
|
+
project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
|
|
22910
|
+
user_id: exports_external.string().optional().describe("Optional user override"),
|
|
22911
|
+
limit: exports_external.number().optional().describe("Max recall items to list")
|
|
22912
|
+
}, async (params) => {
|
|
22913
|
+
const result = listRecallItems(db, {
|
|
22914
|
+
cwd: params.cwd ?? process.cwd(),
|
|
22915
|
+
project_scoped: params.project_scoped,
|
|
22916
|
+
user_id: params.user_id ?? config2.user_id,
|
|
22917
|
+
current_device_id: config2.device_id,
|
|
22918
|
+
limit: params.limit
|
|
22919
|
+
});
|
|
22920
|
+
if (result.items.length === 0) {
|
|
22921
|
+
return {
|
|
22922
|
+
content: [
|
|
22923
|
+
{
|
|
22924
|
+
type: "text",
|
|
22925
|
+
text: result.project ? `No recall items found yet for project ${result.project}` : "No recall items found yet."
|
|
22926
|
+
}
|
|
22927
|
+
]
|
|
22928
|
+
};
|
|
22929
|
+
}
|
|
22930
|
+
const projectLine = result.project ? `Project: ${result.project}
|
|
22931
|
+
` : "";
|
|
22932
|
+
const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
|
|
22933
|
+
${item.detail}`).join(`
|
|
22934
|
+
`);
|
|
22935
|
+
return {
|
|
22936
|
+
content: [
|
|
22937
|
+
{
|
|
22938
|
+
type: "text",
|
|
22939
|
+
text: `${projectLine}Recall index (${result.continuity_mode} mode):
|
|
22940
|
+
` + `${rows}
|
|
22941
|
+
|
|
22942
|
+
` + `Suggested next step: use load_handoff for handoff:* items, get_observations([...]) for obs:* items, or resume_thread when you want one merged resume point.`
|
|
22943
|
+
}
|
|
22944
|
+
]
|
|
22945
|
+
};
|
|
22946
|
+
});
|
|
22947
|
+
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.", {
|
|
22948
|
+
key: exports_external.string().describe("Exact recall key from list_recall_items, such as handoff:12, session:sess-1, chat:55, or obs:402"),
|
|
22949
|
+
cwd: exports_external.string().optional().describe("Optional cwd override"),
|
|
22950
|
+
user_id: exports_external.string().optional().describe("Optional user override")
|
|
22951
|
+
}, async (params) => {
|
|
22952
|
+
const result = loadRecallItem(db, {
|
|
22953
|
+
key: params.key,
|
|
22954
|
+
cwd: params.cwd ?? process.cwd(),
|
|
22955
|
+
user_id: params.user_id ?? config2.user_id,
|
|
22956
|
+
current_device_id: config2.device_id
|
|
22957
|
+
});
|
|
22958
|
+
if (!result.payload) {
|
|
22959
|
+
return {
|
|
22960
|
+
content: [
|
|
22961
|
+
{
|
|
22962
|
+
type: "text",
|
|
22963
|
+
text: `No recall item found for ${params.key}`
|
|
22964
|
+
}
|
|
22965
|
+
]
|
|
22966
|
+
};
|
|
22967
|
+
}
|
|
22968
|
+
if (result.payload.type === "handoff") {
|
|
22969
|
+
return {
|
|
22970
|
+
content: [
|
|
22971
|
+
{
|
|
22972
|
+
type: "text",
|
|
22973
|
+
text: `Recall item ${result.key} [handoff]
|
|
22974
|
+
` + `Title: ${result.title}
|
|
22975
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
22976
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
22977
|
+
|
|
22978
|
+
` + `${result.payload.narrative ?? "(no narrative)"}`
|
|
22979
|
+
}
|
|
22980
|
+
]
|
|
22981
|
+
};
|
|
22982
|
+
}
|
|
22983
|
+
if (result.payload.type === "thread") {
|
|
22984
|
+
const outcomes = result.payload.recent_outcomes.length > 0 ? result.payload.recent_outcomes.map((item) => `- ${item}`).join(`
|
|
22985
|
+
`) : "- (none)";
|
|
22986
|
+
const hotFiles = result.payload.hot_files.length > 0 ? result.payload.hot_files.map((item) => `- ${item.path}${item.count > 1 ? ` (${item.count})` : ""}`).join(`
|
|
22987
|
+
`) : "- (none)";
|
|
22988
|
+
return {
|
|
22989
|
+
content: [
|
|
22990
|
+
{
|
|
22991
|
+
type: "text",
|
|
22992
|
+
text: `Recall item ${result.key} [thread]
|
|
22993
|
+
` + `Title: ${result.title}
|
|
22994
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
22995
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
22996
|
+
` + `Latest request: ${result.payload.latest_request ?? "(none)"}
|
|
22997
|
+
` + `Current thread: ${result.payload.current_thread ?? "(none)"}
|
|
22998
|
+
|
|
22999
|
+
` + `Recent outcomes:
|
|
23000
|
+
${outcomes}
|
|
23001
|
+
|
|
23002
|
+
` + `Hot files:
|
|
23003
|
+
${hotFiles}`
|
|
23004
|
+
}
|
|
23005
|
+
]
|
|
23006
|
+
};
|
|
23007
|
+
}
|
|
23008
|
+
if (result.payload.type === "chat") {
|
|
23009
|
+
return {
|
|
23010
|
+
content: [
|
|
23011
|
+
{
|
|
23012
|
+
type: "text",
|
|
23013
|
+
text: `Recall item ${result.key} [chat]
|
|
23014
|
+
` + `Title: ${result.title}
|
|
23015
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
23016
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
23017
|
+
|
|
23018
|
+
` + `${result.payload.content}`
|
|
23019
|
+
}
|
|
23020
|
+
]
|
|
23021
|
+
};
|
|
23022
|
+
}
|
|
23023
|
+
return {
|
|
23024
|
+
content: [
|
|
23025
|
+
{
|
|
23026
|
+
type: "text",
|
|
23027
|
+
text: `Recall item ${result.key} [memory]
|
|
23028
|
+
` + `Title: ${result.title}
|
|
23029
|
+
` + `Session: ${result.session_id ?? "(unknown)"}
|
|
23030
|
+
` + `Source: ${result.source_device_id ?? "(unknown)"}
|
|
23031
|
+
` + `Type: ${result.payload.observation_type}
|
|
23032
|
+
|
|
23033
|
+
` + `${result.payload.narrative ?? result.payload.facts ?? "(no detail)"}`
|
|
23034
|
+
}
|
|
23035
|
+
]
|
|
23036
|
+
};
|
|
23037
|
+
});
|
|
23038
|
+
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
23039
|
cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
|
|
22489
23040
|
limit: exports_external.number().optional().describe("Max recall hits/chat snippets to include"),
|
|
22490
23041
|
user_id: exports_external.string().optional().describe("Optional user override"),
|
|
@@ -22502,6 +23053,8 @@ server.tool("resume_thread", "Build a clear resume point for the current project
|
|
|
22502
23053
|
const handoffLine = result.handoff ? `Handoff: #${result.handoff.id} ${result.handoff.title}${result.handoff.source ? ` (${result.handoff.source})` : ""}
|
|
22503
23054
|
` : `Handoff: (none)
|
|
22504
23055
|
`;
|
|
23056
|
+
const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
|
|
23057
|
+
` : "";
|
|
22505
23058
|
const basisLines = result.resume_basis.length > 0 ? result.resume_basis.map((item) => `- ${item}`).join(`
|
|
22506
23059
|
`) : "- (none)";
|
|
22507
23060
|
const toolTrailLines = result.tool_trail.length > 0 ? result.tool_trail.map((item) => `- ${item}`).join(`
|
|
@@ -22536,7 +23089,7 @@ server.tool("resume_thread", "Build a clear resume point for the current project
|
|
|
22536
23089
|
` + `Freshness: ${result.resume_freshness}
|
|
22537
23090
|
` + `Source: ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
|
|
22538
23091
|
` + `Resume confidence: ${result.resume_confidence}
|
|
22539
|
-
` + repairLine + `Current thread: ${result.current_thread ?? "(unknown)"}
|
|
23092
|
+
` + repairLine + openExactLine + `Current thread: ${result.current_thread ?? "(unknown)"}
|
|
22540
23093
|
` + `Latest request: ${result.latest_request ?? "(none)"}
|
|
22541
23094
|
` + `${handoffLine}` + `Chat recall: ${result.chat_coverage_state}
|
|
22542
23095
|
` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
|
|
@@ -22872,6 +23425,10 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
22872
23425
|
`) : "- (none)";
|
|
22873
23426
|
const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
22874
23427
|
`) : "- (none)";
|
|
23428
|
+
const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
|
|
23429
|
+
`) : "- (none)";
|
|
23430
|
+
const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
|
|
23431
|
+
` : "";
|
|
22875
23432
|
const projectLine = result.project ? `Project: ${result.project}
|
|
22876
23433
|
|
|
22877
23434
|
` : "";
|
|
@@ -22885,6 +23442,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
22885
23442
|
{
|
|
22886
23443
|
type: "text",
|
|
22887
23444
|
text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
|
|
23445
|
+
` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
|
|
22888
23446
|
` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
|
|
22889
23447
|
` + `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
23448
|
` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
|
|
@@ -22892,6 +23450,9 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
22892
23450
|
` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
|
|
22893
23451
|
` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
|
|
22894
23452
|
|
|
23453
|
+
` + openExactLine + `Recall preview:
|
|
23454
|
+
${recallPreviewLines}
|
|
23455
|
+
|
|
22895
23456
|
` + `Next actions:
|
|
22896
23457
|
${result.resume_next_actions.length > 0 ? result.resume_next_actions.map((item) => `- ${item}`).join(`
|
|
22897
23458
|
`) : "- (none)"}
|
|
@@ -23089,6 +23650,8 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
|
|
|
23089
23650
|
text: `Project: ${result.project_name}
|
|
23090
23651
|
` + `Canonical ID: ${result.canonical_id}
|
|
23091
23652
|
` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
|
|
23653
|
+
` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
|
|
23654
|
+
` + `Open exact: ${result.best_recall_key ? `load_recall_item("${result.best_recall_key}")` : "(none)"}
|
|
23092
23655
|
` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
|
|
23093
23656
|
` + `Loaded observations: ${result.session_count}
|
|
23094
23657
|
` + `Searchable total: ${result.total_active}
|
|
@@ -23101,6 +23664,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
|
|
|
23101
23664
|
` + `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
23665
|
` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
|
|
23103
23666
|
` + `Next actions: ${result.resume_next_actions.length > 0 ? result.resume_next_actions.join(" | ") : "(none)"}
|
|
23667
|
+
` + `Recall preview: ${result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => item.key).join(", ") : "(none)"}
|
|
23104
23668
|
` + `Raw chronology active: ${result.raw_capture_active ? "yes" : "no"}
|
|
23105
23669
|
|
|
23106
23670
|
` + result.preview
|
|
@@ -23166,6 +23730,10 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
23166
23730
|
`) : "- (none)";
|
|
23167
23731
|
const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
|
|
23168
23732
|
`) : "- (none)";
|
|
23733
|
+
const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
|
|
23734
|
+
`) : "- (none)";
|
|
23735
|
+
const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
|
|
23736
|
+
` : "";
|
|
23169
23737
|
return {
|
|
23170
23738
|
content: [
|
|
23171
23739
|
{
|
|
@@ -23173,6 +23741,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
23173
23741
|
text: `Project: ${result.project}
|
|
23174
23742
|
` + `Canonical ID: ${result.canonical_id}
|
|
23175
23743
|
` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
|
|
23744
|
+
` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
|
|
23176
23745
|
` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
|
|
23177
23746
|
` + `Recent requests captured: ${result.recent_requests_count}
|
|
23178
23747
|
` + `Recent tools captured: ${result.recent_tools_count}
|
|
@@ -23188,6 +23757,9 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
23188
23757
|
` + `Estimated read cost: ~${result.estimated_read_tokens}t
|
|
23189
23758
|
` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
|
|
23190
23759
|
|
|
23760
|
+
` + openExactLine + `Recall preview:
|
|
23761
|
+
${recallPreviewLines}
|
|
23762
|
+
|
|
23191
23763
|
` + `Next actions:
|
|
23192
23764
|
${result.resume_next_actions.length > 0 ? result.resume_next_actions.map((item) => `- ${item}`).join(`
|
|
23193
23765
|
`) : "- (none)"}
|
|
@@ -23368,7 +23940,7 @@ Transcript messages seen: ${result.total}`
|
|
|
23368
23940
|
]
|
|
23369
23941
|
};
|
|
23370
23942
|
});
|
|
23371
|
-
server.tool("repair_recall", "Rehydrate recent session recall for the current project from Claude transcripts or history fallback
|
|
23943
|
+
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
23944
|
session_id: exports_external.string().optional().describe("Optional single session ID to repair instead of scanning recent project sessions"),
|
|
23373
23945
|
cwd: exports_external.string().optional().describe("Project directory used to resolve project sessions and Claude history/transcript files"),
|
|
23374
23946
|
limit: exports_external.number().optional().describe("How many recent sessions to inspect when repairing a project"),
|
package/package.json
CHANGED