engrm 0.4.28 → 0.4.29
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/dist/hooks/post-tool-use.js +137 -10
- package/dist/hooks/pre-compact.js +137 -10
- package/dist/hooks/session-start.js +13 -3
- package/dist/hooks/stop.js +138 -11
- package/dist/hooks/user-prompt-submit.js +137 -10
- package/dist/server.js +323 -81
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -16394,8 +16394,69 @@ function sanitizeFtsQuery(query) {
|
|
|
16394
16394
|
return safe;
|
|
16395
16395
|
}
|
|
16396
16396
|
|
|
16397
|
+
// src/tools/recent-chat.ts
|
|
16398
|
+
function getRecentChat(db, input) {
|
|
16399
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16400
|
+
if (input.session_id) {
|
|
16401
|
+
const messages2 = db.getSessionChatMessages(input.session_id, limit).slice(-limit).reverse();
|
|
16402
|
+
return {
|
|
16403
|
+
messages: messages2,
|
|
16404
|
+
session_count: countDistinctSessions(messages2),
|
|
16405
|
+
source_summary: summarizeChatSources(messages2),
|
|
16406
|
+
transcript_backed: messages2.some((message) => message.source_kind === "transcript")
|
|
16407
|
+
};
|
|
16408
|
+
}
|
|
16409
|
+
const projectScoped = input.project_scoped !== false;
|
|
16410
|
+
let projectId = null;
|
|
16411
|
+
let projectName;
|
|
16412
|
+
if (projectScoped) {
|
|
16413
|
+
const cwd = input.cwd ?? process.cwd();
|
|
16414
|
+
const detected = detectProject(cwd);
|
|
16415
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16416
|
+
if (project) {
|
|
16417
|
+
projectId = project.id;
|
|
16418
|
+
projectName = project.name;
|
|
16419
|
+
}
|
|
16420
|
+
}
|
|
16421
|
+
const messages = db.getRecentChatMessages(projectId, limit, input.user_id);
|
|
16422
|
+
return {
|
|
16423
|
+
messages,
|
|
16424
|
+
project: projectName,
|
|
16425
|
+
session_count: countDistinctSessions(messages),
|
|
16426
|
+
source_summary: summarizeChatSources(messages),
|
|
16427
|
+
transcript_backed: messages.some((message) => message.source_kind === "transcript")
|
|
16428
|
+
};
|
|
16429
|
+
}
|
|
16430
|
+
function summarizeChatSources(messages) {
|
|
16431
|
+
return messages.reduce((summary, message) => {
|
|
16432
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
16433
|
+
return summary;
|
|
16434
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
16435
|
+
}
|
|
16436
|
+
function countDistinctSessions(messages) {
|
|
16437
|
+
return new Set(messages.map((message) => message.session_id)).size;
|
|
16438
|
+
}
|
|
16439
|
+
function getChatCaptureOrigin(message) {
|
|
16440
|
+
if (message.source_kind === "transcript")
|
|
16441
|
+
return "transcript";
|
|
16442
|
+
if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
|
|
16443
|
+
return "history";
|
|
16444
|
+
}
|
|
16445
|
+
return "hook";
|
|
16446
|
+
}
|
|
16447
|
+
|
|
16397
16448
|
// src/tools/search-chat.ts
|
|
16398
16449
|
async function searchChat(db, input) {
|
|
16450
|
+
const normalizedQuery = normalizeQuery(input.query);
|
|
16451
|
+
if (!normalizedQuery) {
|
|
16452
|
+
return {
|
|
16453
|
+
messages: [],
|
|
16454
|
+
session_count: 0,
|
|
16455
|
+
source_summary: { transcript: 0, hook: 0 },
|
|
16456
|
+
transcript_backed: false,
|
|
16457
|
+
semantic_backed: false
|
|
16458
|
+
};
|
|
16459
|
+
}
|
|
16399
16460
|
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16400
16461
|
const projectScoped = input.project_scoped !== false;
|
|
16401
16462
|
let projectId = null;
|
|
@@ -16409,26 +16470,33 @@ async function searchChat(db, input) {
|
|
|
16409
16470
|
projectName = project.name;
|
|
16410
16471
|
}
|
|
16411
16472
|
}
|
|
16412
|
-
const
|
|
16473
|
+
const recallIntent = isRecentThreadRecallQuery(normalizedQuery);
|
|
16474
|
+
const lexical = recallIntent ? db.getRecentChatMessages(projectId, limit * 4, input.user_id) : db.searchChatMessages(input.query, projectId, limit * 3, input.user_id);
|
|
16413
16475
|
let semantic = [];
|
|
16414
|
-
const queryEmbedding = db.vecAvailable ? await embedText(composeChatEmbeddingText(queryForEmbedding(input.query))) : null;
|
|
16476
|
+
const queryEmbedding = db.vecAvailable && !recallIntent ? await embedText(composeChatEmbeddingText(queryForEmbedding(input.query))) : null;
|
|
16415
16477
|
if (queryEmbedding && db.vecAvailable) {
|
|
16416
|
-
semantic = db.searchChatVec(queryEmbedding, projectId, limit *
|
|
16478
|
+
semantic = db.searchChatVec(queryEmbedding, projectId, limit * 3, input.user_id);
|
|
16417
16479
|
}
|
|
16418
|
-
const messageIds = mergeChatResults(lexical, semantic, limit);
|
|
16480
|
+
const messageIds = mergeChatResults(db, lexical, semantic, normalizedQuery, limit);
|
|
16419
16481
|
const messages = messageIds.length > 0 ? db.getChatMessagesByIds(messageIds) : [];
|
|
16420
16482
|
return {
|
|
16421
16483
|
messages,
|
|
16422
16484
|
project: projectName,
|
|
16423
|
-
session_count:
|
|
16424
|
-
source_summary:
|
|
16485
|
+
session_count: countDistinctSessions2(messages),
|
|
16486
|
+
source_summary: summarizeChatSources2(messages),
|
|
16425
16487
|
transcript_backed: messages.some((message) => message.source_kind === "transcript"),
|
|
16426
16488
|
semantic_backed: semantic.length > 0
|
|
16427
16489
|
};
|
|
16428
16490
|
}
|
|
16429
16491
|
var RRF_K2 = 40;
|
|
16430
|
-
function mergeChatResults(lexical, semantic, limit) {
|
|
16492
|
+
function mergeChatResults(db, lexical, semantic, query, limit) {
|
|
16431
16493
|
const scores = new Map;
|
|
16494
|
+
const rows = new Map(lexical.map((message) => [message.id, message]));
|
|
16495
|
+
const semanticOnlyIds = semantic.map((match) => match.chat_message_id).filter((id) => !rows.has(id));
|
|
16496
|
+
for (const row of db.getChatMessagesByIds(semanticOnlyIds)) {
|
|
16497
|
+
rows.set(row.id, row);
|
|
16498
|
+
}
|
|
16499
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
16432
16500
|
for (let rank = 0;rank < lexical.length; rank++) {
|
|
16433
16501
|
const message = lexical[rank];
|
|
16434
16502
|
scores.set(message.id, (scores.get(message.id) ?? 0) + 1 / (RRF_K2 + rank + 1));
|
|
@@ -16437,18 +16505,61 @@ function mergeChatResults(lexical, semantic, limit) {
|
|
|
16437
16505
|
const match = semantic[rank];
|
|
16438
16506
|
scores.set(match.chat_message_id, (scores.get(match.chat_message_id) ?? 0) + 1 / (RRF_K2 + rank + 1));
|
|
16439
16507
|
}
|
|
16440
|
-
|
|
16508
|
+
for (const [id, row] of rows) {
|
|
16509
|
+
scores.set(id, (scores.get(id) ?? 0) + computeChatQualityBoost(row, query, nowEpoch));
|
|
16510
|
+
}
|
|
16511
|
+
return Array.from(scores.entries()).sort((a, b) => {
|
|
16512
|
+
if (b[1] !== a[1])
|
|
16513
|
+
return b[1] - a[1];
|
|
16514
|
+
return (rows.get(b[0])?.created_at_epoch ?? 0) - (rows.get(a[0])?.created_at_epoch ?? 0);
|
|
16515
|
+
}).slice(0, limit).map(([id]) => id);
|
|
16441
16516
|
}
|
|
16442
16517
|
function queryForEmbedding(query) {
|
|
16443
16518
|
return query.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
16444
16519
|
}
|
|
16445
|
-
function
|
|
16520
|
+
function normalizeQuery(query) {
|
|
16521
|
+
return query.replace(/\s+/g, " ").trim().toLowerCase();
|
|
16522
|
+
}
|
|
16523
|
+
function isRecentThreadRecallQuery(query) {
|
|
16524
|
+
const normalized = query.replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
16525
|
+
if (!normalized)
|
|
16526
|
+
return false;
|
|
16527
|
+
return [
|
|
16528
|
+
"what were we just talking about",
|
|
16529
|
+
"what were we talking about",
|
|
16530
|
+
"what did we just talk about",
|
|
16531
|
+
"what did we talk about",
|
|
16532
|
+
"what were we discussing",
|
|
16533
|
+
"what were we just discussing",
|
|
16534
|
+
"catch me up",
|
|
16535
|
+
"resume the thread",
|
|
16536
|
+
"where were we",
|
|
16537
|
+
"where did we leave off",
|
|
16538
|
+
"what is the current thread",
|
|
16539
|
+
"what's the current thread"
|
|
16540
|
+
].some((pattern) => normalized.includes(pattern));
|
|
16541
|
+
}
|
|
16542
|
+
function computeChatQualityBoost(message, query, nowEpoch) {
|
|
16543
|
+
const normalizedContent = message.content.replace(/\s+/g, " ").trim().toLowerCase();
|
|
16544
|
+
const ageHours = Math.max(0, (nowEpoch - message.created_at_epoch) / 3600);
|
|
16545
|
+
const directPhraseBoost = normalizedContent.includes(query) ? 0.3 : 0;
|
|
16546
|
+
const termBoost = allQueryTermsPresent(normalizedContent, query) ? 0.12 : 0;
|
|
16547
|
+
const origin = getChatCaptureOrigin(message);
|
|
16548
|
+
const sourceBoost = origin === "transcript" ? 0.12 : origin === "history" ? 0.08 : 0.04;
|
|
16549
|
+
const recencyBoost = ageHours < 1 ? 0.35 : ageHours < 6 ? 0.22 : ageHours < 24 ? 0.12 : ageHours < 72 ? 0.04 : 0;
|
|
16550
|
+
return directPhraseBoost + termBoost + sourceBoost + recencyBoost;
|
|
16551
|
+
}
|
|
16552
|
+
function allQueryTermsPresent(content, query) {
|
|
16553
|
+
const terms = query.split(/\s+/).filter((term) => term.length >= 3);
|
|
16554
|
+
return terms.length > 0 && terms.every((term) => content.includes(term));
|
|
16555
|
+
}
|
|
16556
|
+
function summarizeChatSources2(messages) {
|
|
16446
16557
|
return messages.reduce((summary, message) => {
|
|
16447
|
-
summary[message
|
|
16558
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
16448
16559
|
return summary;
|
|
16449
|
-
}, { transcript: 0, hook: 0 });
|
|
16560
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
16450
16561
|
}
|
|
16451
|
-
function
|
|
16562
|
+
function countDistinctSessions2(messages) {
|
|
16452
16563
|
return new Set(messages.map((message) => message.session_id)).size;
|
|
16453
16564
|
}
|
|
16454
16565
|
|
|
@@ -16463,8 +16574,25 @@ async function searchRecall(db, input) {
|
|
|
16463
16574
|
};
|
|
16464
16575
|
}
|
|
16465
16576
|
const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
|
|
16577
|
+
const recentThreadQuery = isRecentThreadRecallQuery2(query);
|
|
16578
|
+
const projectScoped = input.project_scoped !== false;
|
|
16579
|
+
let projectId = null;
|
|
16580
|
+
let projectName;
|
|
16581
|
+
if (projectScoped) {
|
|
16582
|
+
const cwd = input.cwd ?? process.cwd();
|
|
16583
|
+
const detected = detectProject(cwd);
|
|
16584
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16585
|
+
if (project) {
|
|
16586
|
+
projectId = project.id;
|
|
16587
|
+
projectName = project.name;
|
|
16588
|
+
}
|
|
16589
|
+
}
|
|
16590
|
+
const recentSessions = db.getRecentSessions(projectId, 3, input.user_id);
|
|
16466
16591
|
const [memory, chat] = await Promise.all([
|
|
16467
|
-
searchObservations(db,
|
|
16592
|
+
searchObservations(db, {
|
|
16593
|
+
...input,
|
|
16594
|
+
limit: recentThreadQuery ? Math.max(1, Math.ceil(limit / 2)) : input.limit
|
|
16595
|
+
}),
|
|
16468
16596
|
searchChat(db, {
|
|
16469
16597
|
query,
|
|
16470
16598
|
limit: limit * 2,
|
|
@@ -16473,10 +16601,10 @@ async function searchRecall(db, input) {
|
|
|
16473
16601
|
user_id: input.user_id
|
|
16474
16602
|
})
|
|
16475
16603
|
]);
|
|
16476
|
-
const merged = mergeRecallResults(memory.observations, chat.messages, limit);
|
|
16604
|
+
const merged = mergeRecallResults(memory.observations, chat.messages, limit, recentThreadQuery, buildSessionPriorityMap(recentSessions));
|
|
16477
16605
|
return {
|
|
16478
16606
|
query,
|
|
16479
|
-
project: memory.project ?? chat.project,
|
|
16607
|
+
project: memory.project ?? chat.project ?? projectName,
|
|
16480
16608
|
results: merged,
|
|
16481
16609
|
totals: {
|
|
16482
16610
|
memory: memory.total,
|
|
@@ -16484,18 +16612,23 @@ async function searchRecall(db, input) {
|
|
|
16484
16612
|
}
|
|
16485
16613
|
};
|
|
16486
16614
|
}
|
|
16487
|
-
function mergeRecallResults(memory, chat, limit) {
|
|
16615
|
+
function mergeRecallResults(memory, chat, limit, recentThreadQuery, sessionPriority) {
|
|
16488
16616
|
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
16489
16617
|
const scored = [];
|
|
16490
16618
|
for (let index = 0;index < memory.length; index++) {
|
|
16491
16619
|
const item = memory[index];
|
|
16492
16620
|
const base = 1 / (60 + index + 1);
|
|
16493
|
-
const
|
|
16621
|
+
const createdAtEpoch = Math.floor(new Date(item.created_at).getTime() / 1000) || undefined;
|
|
16622
|
+
const ageHours = createdAtEpoch ? Math.max(0, (nowEpoch - createdAtEpoch) / 3600) : 9999;
|
|
16623
|
+
const freshnessPenalty = ageHours > 24 * 7 ? 0.12 : ageHours > 72 ? 0.05 : 0;
|
|
16624
|
+
const continuityPenalty = recentThreadQuery ? 0.45 : 0;
|
|
16625
|
+
const sessionBoost = item.session_id ? sessionPriority.get(item.session_id) ?? 0 : 0;
|
|
16626
|
+
const score = base + Math.max(0, item.rank) * 0.08 + sessionBoost - freshnessPenalty - continuityPenalty;
|
|
16494
16627
|
scored.push({
|
|
16495
16628
|
kind: "memory",
|
|
16496
16629
|
rank: score,
|
|
16497
16630
|
created_at: item.created_at,
|
|
16498
|
-
created_at_epoch:
|
|
16631
|
+
created_at_epoch: createdAtEpoch,
|
|
16499
16632
|
project_name: item.project_name,
|
|
16500
16633
|
observation_id: item.id,
|
|
16501
16634
|
id: item.id,
|
|
@@ -16509,12 +16642,14 @@ function mergeRecallResults(memory, chat, limit) {
|
|
|
16509
16642
|
const item = chat[index];
|
|
16510
16643
|
const base = 1 / (60 + index + 1);
|
|
16511
16644
|
const ageHours = Math.max(0, (nowEpoch - item.created_at_epoch) / 3600);
|
|
16512
|
-
const immediacyBoost = ageHours < 1 ? 1 : 0;
|
|
16513
|
-
const recencyBoost = ageHours < 24 ? 0.
|
|
16514
|
-
const sourceBoost = item.source_kind === "transcript" ? 0.
|
|
16645
|
+
const immediacyBoost = ageHours < 1 ? 1.2 : ageHours < 6 ? 0.55 : 0;
|
|
16646
|
+
const recencyBoost = ageHours < 24 ? 0.18 : ageHours < 72 ? 0.08 : 0.02;
|
|
16647
|
+
const sourceBoost = item.source_kind === "transcript" ? 0.1 : 0.04;
|
|
16648
|
+
const continuityBoost = recentThreadQuery ? 0.35 : 0;
|
|
16649
|
+
const sessionBoost = sessionPriority.get(item.session_id) ?? 0;
|
|
16515
16650
|
scored.push({
|
|
16516
16651
|
kind: "chat",
|
|
16517
|
-
rank: base + immediacyBoost + recencyBoost + sourceBoost,
|
|
16652
|
+
rank: base + immediacyBoost + recencyBoost + sourceBoost + continuityBoost + sessionBoost,
|
|
16518
16653
|
created_at_epoch: item.created_at_epoch,
|
|
16519
16654
|
session_id: item.session_id,
|
|
16520
16655
|
id: item.id,
|
|
@@ -16530,6 +16665,38 @@ function mergeRecallResults(memory, chat, limit) {
|
|
|
16530
16665
|
return (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0);
|
|
16531
16666
|
}).slice(0, limit);
|
|
16532
16667
|
}
|
|
16668
|
+
function buildSessionPriorityMap(sessions) {
|
|
16669
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
16670
|
+
const priorities = new Map;
|
|
16671
|
+
for (let index = 0;index < sessions.length; index++) {
|
|
16672
|
+
const session = sessions[index];
|
|
16673
|
+
const activityEpoch = session.completed_at_epoch ?? session.started_at_epoch ?? 0;
|
|
16674
|
+
const ageHours = Math.max(0, (nowEpoch - activityEpoch) / 3600);
|
|
16675
|
+
const freshnessBoost = ageHours < 1 ? 0.28 : ageHours < 6 ? 0.18 : ageHours < 24 ? 0.08 : 0;
|
|
16676
|
+
const rankBoost = index === 0 ? 0.12 : index === 1 ? 0.07 : 0.03;
|
|
16677
|
+
priorities.set(session.session_id, freshnessBoost + rankBoost);
|
|
16678
|
+
}
|
|
16679
|
+
return priorities;
|
|
16680
|
+
}
|
|
16681
|
+
function isRecentThreadRecallQuery2(query) {
|
|
16682
|
+
const normalized = query.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
16683
|
+
if (!normalized)
|
|
16684
|
+
return false;
|
|
16685
|
+
return [
|
|
16686
|
+
"what were we just talking about",
|
|
16687
|
+
"what were we talking about",
|
|
16688
|
+
"what did we just talk about",
|
|
16689
|
+
"what did we talk about",
|
|
16690
|
+
"what were we discussing",
|
|
16691
|
+
"what were we just discussing",
|
|
16692
|
+
"catch me up",
|
|
16693
|
+
"resume the thread",
|
|
16694
|
+
"where were we",
|
|
16695
|
+
"where did we leave off",
|
|
16696
|
+
"what is the current thread",
|
|
16697
|
+
"what s the current thread"
|
|
16698
|
+
].some((pattern) => normalized.includes(pattern));
|
|
16699
|
+
}
|
|
16533
16700
|
function parseFactsPreview(facts) {
|
|
16534
16701
|
if (!facts)
|
|
16535
16702
|
return null;
|
|
@@ -16684,49 +16851,6 @@ function getRecentRequests(db, input) {
|
|
|
16684
16851
|
};
|
|
16685
16852
|
}
|
|
16686
16853
|
|
|
16687
|
-
// src/tools/recent-chat.ts
|
|
16688
|
-
function getRecentChat(db, input) {
|
|
16689
|
-
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16690
|
-
if (input.session_id) {
|
|
16691
|
-
const messages2 = db.getSessionChatMessages(input.session_id, limit).slice(-limit).reverse();
|
|
16692
|
-
return {
|
|
16693
|
-
messages: messages2,
|
|
16694
|
-
session_count: countDistinctSessions2(messages2),
|
|
16695
|
-
source_summary: summarizeChatSources2(messages2),
|
|
16696
|
-
transcript_backed: messages2.some((message) => message.source_kind === "transcript")
|
|
16697
|
-
};
|
|
16698
|
-
}
|
|
16699
|
-
const projectScoped = input.project_scoped !== false;
|
|
16700
|
-
let projectId = null;
|
|
16701
|
-
let projectName;
|
|
16702
|
-
if (projectScoped) {
|
|
16703
|
-
const cwd = input.cwd ?? process.cwd();
|
|
16704
|
-
const detected = detectProject(cwd);
|
|
16705
|
-
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16706
|
-
if (project) {
|
|
16707
|
-
projectId = project.id;
|
|
16708
|
-
projectName = project.name;
|
|
16709
|
-
}
|
|
16710
|
-
}
|
|
16711
|
-
const messages = db.getRecentChatMessages(projectId, limit, input.user_id);
|
|
16712
|
-
return {
|
|
16713
|
-
messages,
|
|
16714
|
-
project: projectName,
|
|
16715
|
-
session_count: countDistinctSessions2(messages),
|
|
16716
|
-
source_summary: summarizeChatSources2(messages),
|
|
16717
|
-
transcript_backed: messages.some((message) => message.source_kind === "transcript")
|
|
16718
|
-
};
|
|
16719
|
-
}
|
|
16720
|
-
function summarizeChatSources2(messages) {
|
|
16721
|
-
return messages.reduce((summary, message) => {
|
|
16722
|
-
summary[message.source_kind] += 1;
|
|
16723
|
-
return summary;
|
|
16724
|
-
}, { transcript: 0, hook: 0 });
|
|
16725
|
-
}
|
|
16726
|
-
function countDistinctSessions2(messages) {
|
|
16727
|
-
return new Set(messages.map((message) => message.session_id)).size;
|
|
16728
|
-
}
|
|
16729
|
-
|
|
16730
16854
|
// src/tools/session-story.ts
|
|
16731
16855
|
function getSessionStory(db, input) {
|
|
16732
16856
|
const session = db.getSessionById(input.session_id);
|
|
@@ -16849,9 +16973,9 @@ function collectProvenanceSummary(observations) {
|
|
|
16849
16973
|
}
|
|
16850
16974
|
function summarizeChatSources3(messages) {
|
|
16851
16975
|
return messages.reduce((summary, message) => {
|
|
16852
|
-
summary[message
|
|
16976
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
16853
16977
|
return summary;
|
|
16854
|
-
}, { transcript: 0, hook: 0 });
|
|
16978
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
16855
16979
|
}
|
|
16856
16980
|
|
|
16857
16981
|
// src/tools/handoffs.ts
|
|
@@ -18646,12 +18770,13 @@ function toToolEvent(tool) {
|
|
|
18646
18770
|
}
|
|
18647
18771
|
function toChatEvent(message) {
|
|
18648
18772
|
const content = message.content.replace(/\s+/g, " ").trim();
|
|
18773
|
+
const origin = getChatCaptureOrigin(message);
|
|
18649
18774
|
return {
|
|
18650
18775
|
kind: "chat",
|
|
18651
18776
|
created_at_epoch: message.created_at_epoch,
|
|
18652
18777
|
session_id: message.session_id,
|
|
18653
18778
|
id: message.id,
|
|
18654
|
-
title: `${message.role} [${
|
|
18779
|
+
title: `${message.role} [${origin}]`,
|
|
18655
18780
|
detail: content.slice(0, 220)
|
|
18656
18781
|
};
|
|
18657
18782
|
}
|
|
@@ -21229,6 +21354,7 @@ function reduceOpenClawContentToMemory(input) {
|
|
|
21229
21354
|
}
|
|
21230
21355
|
|
|
21231
21356
|
// src/capture/transcript.ts
|
|
21357
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
21232
21358
|
import { readFileSync as readFileSync5, existsSync as existsSync7 } from "node:fs";
|
|
21233
21359
|
import { join as join5 } from "node:path";
|
|
21234
21360
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -21282,23 +21408,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
21282
21408
|
}
|
|
21283
21409
|
return messages;
|
|
21284
21410
|
}
|
|
21411
|
+
function resolveHistoryPath(historyPath) {
|
|
21412
|
+
if (historyPath)
|
|
21413
|
+
return historyPath;
|
|
21414
|
+
const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
|
|
21415
|
+
if (override)
|
|
21416
|
+
return override;
|
|
21417
|
+
return join5(homedir3(), ".claude", "history.jsonl");
|
|
21418
|
+
}
|
|
21419
|
+
function readHistoryFallback(sessionId, cwd, opts) {
|
|
21420
|
+
const path = resolveHistoryPath(opts?.historyPath);
|
|
21421
|
+
if (!existsSync7(path))
|
|
21422
|
+
return [];
|
|
21423
|
+
let raw;
|
|
21424
|
+
try {
|
|
21425
|
+
raw = readFileSync5(path, "utf-8");
|
|
21426
|
+
} catch {
|
|
21427
|
+
return [];
|
|
21428
|
+
}
|
|
21429
|
+
const targetCanonical = detectProject(cwd).canonical_id;
|
|
21430
|
+
const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
|
|
21431
|
+
const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
|
|
21432
|
+
const entries = [];
|
|
21433
|
+
for (const line of raw.split(`
|
|
21434
|
+
`)) {
|
|
21435
|
+
if (!line.trim())
|
|
21436
|
+
continue;
|
|
21437
|
+
let entry;
|
|
21438
|
+
try {
|
|
21439
|
+
entry = JSON.parse(line);
|
|
21440
|
+
} catch {
|
|
21441
|
+
continue;
|
|
21442
|
+
}
|
|
21443
|
+
if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
|
|
21444
|
+
continue;
|
|
21445
|
+
const createdAtEpoch = Math.floor(entry.timestamp / 1000);
|
|
21446
|
+
entries.push({
|
|
21447
|
+
display: entry.display.trim(),
|
|
21448
|
+
project: typeof entry.project === "string" ? entry.project : "",
|
|
21449
|
+
sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
|
|
21450
|
+
timestamp: createdAtEpoch
|
|
21451
|
+
});
|
|
21452
|
+
}
|
|
21453
|
+
const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
|
|
21454
|
+
if (bySession.length > 0) {
|
|
21455
|
+
return dedupeHistoryMessages(bySession.map((entry) => ({
|
|
21456
|
+
role: "user",
|
|
21457
|
+
text: entry.display,
|
|
21458
|
+
createdAtEpoch: entry.timestamp
|
|
21459
|
+
})));
|
|
21460
|
+
}
|
|
21461
|
+
const byProjectAndWindow = entries.filter((entry) => {
|
|
21462
|
+
if (entry.display.length === 0)
|
|
21463
|
+
return false;
|
|
21464
|
+
if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
|
|
21465
|
+
return false;
|
|
21466
|
+
if (!entry.project)
|
|
21467
|
+
return false;
|
|
21468
|
+
return detectProject(entry.project).canonical_id === targetCanonical;
|
|
21469
|
+
}).sort((a, b) => a.timestamp - b.timestamp);
|
|
21470
|
+
return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
|
|
21471
|
+
role: "user",
|
|
21472
|
+
text: entry.display,
|
|
21473
|
+
createdAtEpoch: entry.timestamp
|
|
21474
|
+
})));
|
|
21475
|
+
}
|
|
21285
21476
|
async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
21286
|
-
const
|
|
21477
|
+
const session = db.getSessionById(sessionId);
|
|
21478
|
+
const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
21287
21479
|
...message,
|
|
21288
21480
|
text: message.text.trim()
|
|
21289
21481
|
})).filter((message) => message.text.length > 0);
|
|
21482
|
+
const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
|
|
21483
|
+
...message,
|
|
21484
|
+
sourceKind: "transcript",
|
|
21485
|
+
transcriptIndex: index + 1,
|
|
21486
|
+
createdAtEpoch: null,
|
|
21487
|
+
remoteSourceId: null
|
|
21488
|
+
})) : readHistoryFallback(sessionId, cwd, {
|
|
21489
|
+
startedAtEpoch: session?.started_at_epoch ?? null,
|
|
21490
|
+
completedAtEpoch: session?.completed_at_epoch ?? null
|
|
21491
|
+
}).map((message) => ({
|
|
21492
|
+
role: message.role,
|
|
21493
|
+
text: message.text,
|
|
21494
|
+
sourceKind: "hook",
|
|
21495
|
+
transcriptIndex: null,
|
|
21496
|
+
createdAtEpoch: message.createdAtEpoch,
|
|
21497
|
+
remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
|
|
21498
|
+
}));
|
|
21290
21499
|
if (messages.length === 0)
|
|
21291
21500
|
return { imported: 0, total: 0 };
|
|
21292
|
-
const session = db.getSessionById(sessionId);
|
|
21293
21501
|
const projectId = session?.project_id ?? null;
|
|
21294
21502
|
const now = Math.floor(Date.now() / 1000);
|
|
21295
21503
|
let imported = 0;
|
|
21296
21504
|
for (let index = 0;index < messages.length; index++) {
|
|
21297
|
-
const transcriptIndex = index + 1;
|
|
21298
|
-
if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
|
|
21299
|
-
continue;
|
|
21300
21505
|
const message = messages[index];
|
|
21301
|
-
const
|
|
21506
|
+
const transcriptIndex = message.transcriptIndex ?? index + 1;
|
|
21507
|
+
if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
|
|
21508
|
+
continue;
|
|
21509
|
+
}
|
|
21510
|
+
if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
|
|
21511
|
+
continue;
|
|
21512
|
+
}
|
|
21513
|
+
const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
|
|
21302
21514
|
const row = db.insertChatMessage({
|
|
21303
21515
|
session_id: sessionId,
|
|
21304
21516
|
project_id: projectId,
|
|
@@ -21308,10 +21520,23 @@ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
|
21308
21520
|
device_id: config2.device_id,
|
|
21309
21521
|
agent: "claude-code",
|
|
21310
21522
|
created_at_epoch: createdAtEpoch,
|
|
21311
|
-
|
|
21312
|
-
|
|
21523
|
+
remote_source_id: message.remoteSourceId,
|
|
21524
|
+
source_kind: message.sourceKind,
|
|
21525
|
+
transcript_index: message.transcriptIndex
|
|
21313
21526
|
});
|
|
21314
21527
|
db.addToOutbox("chat_message", row.id);
|
|
21528
|
+
if (message.role === "user") {
|
|
21529
|
+
db.insertUserPrompt({
|
|
21530
|
+
session_id: sessionId,
|
|
21531
|
+
project_id: projectId,
|
|
21532
|
+
prompt: message.text,
|
|
21533
|
+
cwd,
|
|
21534
|
+
user_id: config2.user_id,
|
|
21535
|
+
device_id: config2.device_id,
|
|
21536
|
+
agent: "claude-code",
|
|
21537
|
+
created_at_epoch: createdAtEpoch
|
|
21538
|
+
});
|
|
21539
|
+
}
|
|
21315
21540
|
if (db.vecAvailable) {
|
|
21316
21541
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
21317
21542
|
if (embedding) {
|
|
@@ -21322,6 +21547,23 @@ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
|
|
|
21322
21547
|
}
|
|
21323
21548
|
return { imported, total: messages.length };
|
|
21324
21549
|
}
|
|
21550
|
+
function dedupeHistoryMessages(messages) {
|
|
21551
|
+
const deduped = [];
|
|
21552
|
+
for (const message of messages) {
|
|
21553
|
+
const compact = message.text.replace(/\s+/g, " ").trim();
|
|
21554
|
+
if (!compact)
|
|
21555
|
+
continue;
|
|
21556
|
+
const previous = deduped[deduped.length - 1];
|
|
21557
|
+
if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
|
|
21558
|
+
continue;
|
|
21559
|
+
deduped.push({ ...message, text: compact });
|
|
21560
|
+
}
|
|
21561
|
+
return deduped;
|
|
21562
|
+
}
|
|
21563
|
+
function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
21564
|
+
const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
|
|
21565
|
+
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
21566
|
+
}
|
|
21325
21567
|
|
|
21326
21568
|
// src/server.ts
|
|
21327
21569
|
if (!configExists()) {
|
|
@@ -21393,7 +21635,7 @@ process.on("SIGTERM", () => {
|
|
|
21393
21635
|
});
|
|
21394
21636
|
var server = new McpServer({
|
|
21395
21637
|
name: "engrm",
|
|
21396
|
-
version: "0.4.
|
|
21638
|
+
version: "0.4.29"
|
|
21397
21639
|
});
|
|
21398
21640
|
server.tool("save_observation", "Save an observation to memory", {
|
|
21399
21641
|
type: exports_external.enum([
|
|
@@ -22708,12 +22950,12 @@ server.tool("recent_chat", "Inspect recently captured chat messages in the separ
|
|
|
22708
22950
|
const result = getRecentChat(db, params);
|
|
22709
22951
|
const projectLine = result.project ? `Project: ${result.project}
|
|
22710
22952
|
` : "";
|
|
22711
|
-
const coverageLine = `Coverage: ${result.messages.length} messages across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · hook ${result.source_summary.hook}
|
|
22953
|
+
const coverageLine = `Coverage: ${result.messages.length} messages across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · history ${result.source_summary.history} · hook ${result.source_summary.hook}
|
|
22712
22954
|
` + `${result.transcript_backed ? "" : `Hint: run refresh_chat_recall if this looks under-captured.
|
|
22713
22955
|
`}`;
|
|
22714
22956
|
const rows = result.messages.length > 0 ? result.messages.map((msg) => {
|
|
22715
22957
|
const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
22716
|
-
return `- ${stamp} [${msg.role}] [${msg
|
|
22958
|
+
return `- ${stamp} [${msg.role}] [${getChatCaptureOrigin(msg)}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
|
|
22717
22959
|
}).join(`
|
|
22718
22960
|
`) : "- (none)";
|
|
22719
22961
|
return {
|
|
@@ -22736,12 +22978,12 @@ server.tool("search_chat", "Search the separate chat lane without mixing it into
|
|
|
22736
22978
|
const result = await searchChat(db, params);
|
|
22737
22979
|
const projectLine = result.project ? `Project: ${result.project}
|
|
22738
22980
|
` : "";
|
|
22739
|
-
const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · hook ${result.source_summary.hook}` + `${result.semantic_backed ? " · semantic yes" : ""}
|
|
22981
|
+
const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · history ${result.source_summary.history} · hook ${result.source_summary.hook}` + `${result.semantic_backed ? " · semantic yes" : ""}
|
|
22740
22982
|
` + `${result.transcript_backed ? "" : `Hint: run refresh_chat_recall if this looks under-captured.
|
|
22741
22983
|
`}`;
|
|
22742
22984
|
const rows = result.messages.length > 0 ? result.messages.map((msg) => {
|
|
22743
22985
|
const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
22744
|
-
return `- ${stamp} [${msg.role}] [${msg
|
|
22986
|
+
return `- ${stamp} [${msg.role}] [${getChatCaptureOrigin(msg)}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
|
|
22745
22987
|
}).join(`
|
|
22746
22988
|
`) : "- (none)";
|
|
22747
22989
|
return {
|