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/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 lexical = db.searchChatMessages(input.query, projectId, limit * 2, input.user_id);
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 * 2, input.user_id);
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: countDistinctSessions(messages),
16424
- source_summary: summarizeChatSources(messages),
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
- return Array.from(scores.entries()).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([id]) => id);
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 summarizeChatSources(messages) {
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.source_kind] += 1;
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 countDistinctSessions(messages) {
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, input),
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 score = base + Math.max(0, item.rank) * 0.08;
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: Math.floor(new Date(item.created_at).getTime() / 1000) || undefined,
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.12 : ageHours < 72 ? 0.05 : 0.02;
16514
- const sourceBoost = item.source_kind === "transcript" ? 0.06 : 0.03;
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.source_kind] += 1;
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} [${message.source_kind}]`,
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 messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
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 createdAtEpoch = Math.max(0, now - (messages.length - transcriptIndex));
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
- source_kind: "transcript",
21312
- transcript_index: transcriptIndex
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.28"
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.source_kind}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
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.source_kind}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "description": "Shared memory across devices, sessions, and coding agents",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",