engrm 0.4.32 → 0.4.34

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 CHANGED
@@ -226,6 +226,7 @@ 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
+ | `agent_memory_index` | Compare continuity and capture health across Claude Code, Codex, OpenClaw, and other agents |
229
230
  | `repair_recall` | Use when continuity feels thin; rehydrate recent recall from transcript or Claude history fallback |
230
231
  | `list_recall_items` | Use first when continuity feels fuzzy; list the best current handoffs, threads, chat snippets, and memory entries |
231
232
  | `load_recall_item` | Use after `list_recall_items`; load one exact recall item key |
@@ -402,10 +403,13 @@ What each tool is good for:
402
403
 
403
404
  - `capture_status` tells you whether prompt/tool hooks are live on this machine
404
405
  - `capture_quality` shows whether chat recall is transcript-backed, history-backed, or still hook-only across the workspace
406
+ - `agent_memory_index` lets you compare Claude Code, Codex, and other agent sessions on the same repo, so cross-agent validation stops being guesswork
407
+ - when multiple agents are active on the same repo, startup plus the MCP workbench now surface the active agent set and suggest `agent_memory_index` automatically
405
408
  - `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
406
- - `resume_thread` is the fastest “get me back into the live thread” path when you want freshness, source, next actions, tool trail, and chat in one place
409
+ - `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
410
  - `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
411
  - `load_recall_item` completes that protocol by letting agents open one exact recall key directly after listing
412
+ - `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
409
413
  - `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed, history-backed, or only hook-captured
410
414
  - `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
411
415
  - 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
@@ -3144,7 +3144,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3144
3144
  import { join as join3 } from "node:path";
3145
3145
  import { homedir } from "node:os";
3146
3146
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3147
- var CLIENT_VERSION = "0.4.32";
3147
+ var CLIENT_VERSION = "0.4.34";
3148
3148
  function hashFile(filePath) {
3149
3149
  try {
3150
3150
  if (!existsSync3(filePath))
@@ -5617,7 +5617,15 @@ function formatSplashScreen(data) {
5617
5617
  lines.push(` ${line}`);
5618
5618
  }
5619
5619
  }
5620
- const inspectHints = formatInspectHints(data.context, contextIndex.observationIds);
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);
5621
5629
  if (inspectHints.length > 0) {
5622
5630
  lines.push("");
5623
5631
  for (const line of inspectHints) {
@@ -5836,14 +5844,18 @@ function formatContextIndex(context, shownItems) {
5836
5844
  observationIds: selected.map((obs) => obs.id)
5837
5845
  };
5838
5846
  }
5839
- function formatInspectHints(context, visibleObservationIds = []) {
5847
+ function formatInspectHints(context, visibleObservationIds = [], recallItems = []) {
5840
5848
  const hints = [];
5841
5849
  const continuityState = getStartupContinuityState(context);
5850
+ const activeAgents = collectStartupAgents(context);
5842
5851
  if ((context.recentSessions?.length ?? 0) > 0) {
5843
5852
  hints.push("recent_sessions");
5844
5853
  hints.push("session_story");
5845
5854
  hints.push("create_handoff");
5846
5855
  }
5856
+ if (activeAgents.length > 1) {
5857
+ hints.push("agent_memory_index");
5858
+ }
5847
5859
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0) {
5848
5860
  hints.push("activity_feed");
5849
5861
  }
@@ -5875,12 +5887,93 @@ function formatInspectHints(context, visibleObservationIds = []) {
5875
5887
  if (unique.length === 0)
5876
5888
  return [];
5877
5889
  const ids = visibleObservationIds.slice(0, 5);
5890
+ const openNowItem = recallItems.find((item) => item.kind !== "memory") ?? null;
5878
5891
  const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
5879
5892
  return [
5880
5893
  `${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
5894
+ ...openNowItem ? [`${c2.dim}Open now:${c2.reset} load_recall_item("${openNowItem.key}")`] : [],
5881
5895
  ...fetchHint ? [`${c2.dim}Pull detail:${c2.reset} ${fetchHint}`] : []
5882
5896
  ];
5883
5897
  }
5898
+ function collectStartupAgents(context) {
5899
+ return Array.from(new Set((context.recentSessions ?? []).map((session) => session.agent?.trim()).filter((agent) => Boolean(agent) && !agent.startsWith("engrm-")))).sort();
5900
+ }
5901
+ function formatStartupRecallPreview(recallItems) {
5902
+ const items = recallItems.slice(0, 3);
5903
+ if (items.length === 0)
5904
+ return [];
5905
+ return [
5906
+ `${c2.dim}Recall preview:${c2.reset} exact keys you can open now`,
5907
+ ...items.map((item) => `${item.key} [${item.kind} \xB7 ${item.freshness}] ${truncateInline(item.title, 110)}`)
5908
+ ];
5909
+ }
5910
+ function buildStartupRecallItems(context) {
5911
+ const items = [];
5912
+ for (const handoff of context.recentHandoffs?.slice(0, 2) ?? []) {
5913
+ 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();
5914
+ if (!title)
5915
+ continue;
5916
+ const freshness = classifyResumeFreshness(handoff.created_at_epoch);
5917
+ items.push({
5918
+ key: `handoff:${handoff.id}`,
5919
+ kind: "handoff",
5920
+ freshness,
5921
+ title,
5922
+ score: freshnessScore(freshness) + 40
5923
+ });
5924
+ }
5925
+ for (const session of context.recentSessions?.slice(0, 2) ?? []) {
5926
+ const title = chooseMeaningfulSessionSummary(session.request, session.completed);
5927
+ if (!title)
5928
+ continue;
5929
+ const sourceEpoch = session.completed_at_epoch ?? session.started_at_epoch ?? null;
5930
+ const freshness = classifyResumeFreshness(sourceEpoch);
5931
+ items.push({
5932
+ key: `session:${session.session_id}`,
5933
+ kind: "thread",
5934
+ freshness,
5935
+ title,
5936
+ score: freshnessScore(freshness) + 30
5937
+ });
5938
+ }
5939
+ for (const message of context.recentChatMessages?.slice(-2) ?? []) {
5940
+ if (!message.content.trim())
5941
+ continue;
5942
+ const freshness = classifyResumeFreshness(message.created_at_epoch);
5943
+ items.push({
5944
+ key: `chat:${message.id}`,
5945
+ kind: "chat",
5946
+ freshness,
5947
+ title: `[${message.role}] ${message.content.replace(/\s+/g, " ").trim()}`,
5948
+ score: freshnessScore(freshness) + 20
5949
+ });
5950
+ }
5951
+ for (const obs of pickContextIndexObservations(context).slice(0, 2)) {
5952
+ const createdAtEpoch = Math.floor(new Date(obs.created_at).getTime() / 1000);
5953
+ const freshness = classifyResumeFreshness(Number.isFinite(createdAtEpoch) ? createdAtEpoch : null);
5954
+ items.push({
5955
+ key: `obs:${obs.id}`,
5956
+ kind: "memory",
5957
+ freshness,
5958
+ title: obs.title,
5959
+ score: freshnessScore(freshness) + 10
5960
+ });
5961
+ }
5962
+ const seen = new Set;
5963
+ return items.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)).filter((item) => {
5964
+ if (seen.has(item.key))
5965
+ return false;
5966
+ seen.add(item.key);
5967
+ return true;
5968
+ }).map(({ score: _score, ...item }) => item);
5969
+ }
5970
+ function freshnessScore(freshness) {
5971
+ if (freshness === "live")
5972
+ return 3;
5973
+ if (freshness === "recent")
5974
+ return 2;
5975
+ return 1;
5976
+ }
5884
5977
  function rememberShownItem(shown, value) {
5885
5978
  if (!value)
5886
5979
  return;
@@ -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.32",
3085
+ client_version: "0.4.34",
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
@@ -18589,8 +18589,9 @@ function getProjectMemoryIndex(db, input) {
18589
18589
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
18590
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);
18591
18591
  const captureSummary = summarizeCaptureState(recentSessions);
18592
+ const activeAgents = collectActiveAgents(recentSessions);
18592
18593
  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);
18593
- const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.coverage_state);
18594
+ const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.coverage_state, activeAgents);
18594
18595
  const estimatedReadTokens = estimateTokens([
18595
18596
  recentOutcomes.join(`
18596
18597
  `),
@@ -18602,9 +18603,12 @@ function getProjectMemoryIndex(db, input) {
18602
18603
  `));
18603
18604
  const continuityState = classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount.length, recentChatCount, recentSessions, recentOutcomes.length);
18604
18605
  const sourceTimestamp = pickResumeSourceTimestamp(latestSession, recentChat.messages);
18606
+ const bestRecallItem = pickBestRecallItem(recallIndex.items);
18605
18607
  return {
18606
18608
  project: project.name,
18607
18609
  canonical_id: project.canonical_id,
18610
+ active_agents: activeAgents,
18611
+ cross_agent_active: activeAgents.length > 1,
18608
18612
  continuity_state: continuityState,
18609
18613
  continuity_summary: describeContinuityState(continuityState),
18610
18614
  recall_mode: recallIndex.continuity_mode,
@@ -18615,6 +18619,9 @@ function getProjectMemoryIndex(db, input) {
18615
18619
  freshness: item.freshness,
18616
18620
  title: item.title
18617
18621
  })),
18622
+ best_recall_key: bestRecallItem?.key ?? null,
18623
+ best_recall_title: bestRecallItem?.title ?? null,
18624
+ best_recall_kind: bestRecallItem?.kind ?? null,
18618
18625
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18619
18626
  resume_source_session_id: latestSession?.session_id ?? null,
18620
18627
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18643,6 +18650,9 @@ function getProjectMemoryIndex(db, input) {
18643
18650
  suggested_tools: suggestedTools
18644
18651
  };
18645
18652
  }
18653
+ function pickBestRecallItem(items) {
18654
+ return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
18655
+ }
18646
18656
  function pickResumeSourceTimestamp(latestSession, messages) {
18647
18657
  const latestChatEpoch = messages.length > 0 ? messages[messages.length - 1]?.created_at_epoch ?? null : null;
18648
18658
  return latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
@@ -18725,11 +18735,17 @@ function summarizeCaptureState(sessions) {
18725
18735
  }
18726
18736
  return summary;
18727
18737
  }
18728
- function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, chatCoverageState) {
18738
+ function collectActiveAgents(sessions) {
18739
+ return Array.from(new Set(sessions.map((session) => session.agent?.trim()).filter((agent) => Boolean(agent) && !agent.startsWith("engrm-")))).sort();
18740
+ }
18741
+ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, chatCoverageState, activeAgents) {
18729
18742
  const suggested = [];
18730
18743
  if (sessions.length > 0) {
18731
18744
  suggested.push("recent_sessions");
18732
18745
  }
18746
+ if (activeAgents.length > 1) {
18747
+ suggested.push("agent_memory_index");
18748
+ }
18733
18749
  if (requestCount > 0 || toolCount > 0) {
18734
18750
  suggested.push("activity_feed");
18735
18751
  }
@@ -18754,7 +18770,7 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
18754
18770
  if (recentChatCount > 0) {
18755
18771
  suggested.push("recent_chat", "search_chat");
18756
18772
  }
18757
- return Array.from(new Set(suggested)).slice(0, 5);
18773
+ return Array.from(new Set(suggested)).slice(0, 6);
18758
18774
  }
18759
18775
 
18760
18776
  // src/tools/memory-console.ts
@@ -18812,8 +18828,11 @@ function getMemoryConsole(db, input) {
18812
18828
  user_id: input.user_id
18813
18829
  }) : null;
18814
18830
  const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.messages.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
18831
+ const activeAgents = projectIndex?.active_agents ?? collectActiveAgents(sessions);
18815
18832
  return {
18816
18833
  project: project?.name,
18834
+ active_agents: activeAgents,
18835
+ cross_agent_active: projectIndex?.cross_agent_active ?? activeAgents.length > 1,
18817
18836
  capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
18818
18837
  continuity_state: continuityState,
18819
18838
  continuity_summary: projectIndex?.continuity_summary ?? describeContinuityState(continuityState),
@@ -18825,6 +18844,9 @@ function getMemoryConsole(db, input) {
18825
18844
  freshness: item.freshness,
18826
18845
  title: item.title
18827
18846
  })),
18847
+ best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
18848
+ best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
18849
+ best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.kind ?? null,
18828
18850
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18829
18851
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18830
18852
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -18848,13 +18870,15 @@ function getMemoryConsole(db, input) {
18848
18870
  assistant_checkpoint_types: projectIndex?.assistant_checkpoint_types ?? [],
18849
18871
  top_types: projectIndex?.top_types ?? [],
18850
18872
  estimated_read_tokens: projectIndex?.estimated_read_tokens,
18851
- suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.coverage_state)
18873
+ suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.coverage_state, activeAgents.length)
18852
18874
  };
18853
18875
  }
18854
- function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, chatCoverageState) {
18876
+ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, chatCoverageState, activeAgentCount) {
18855
18877
  const suggested = [];
18856
18878
  if (sessionCount > 0)
18857
18879
  suggested.push("recent_sessions");
18880
+ if (activeAgentCount > 1)
18881
+ suggested.push("agent_memory_index");
18858
18882
  if (requestCount > 0 || toolCount > 0)
18859
18883
  suggested.push("activity_feed");
18860
18884
  if (requestCount > 0 || chatCount > 0 || observationCount > 0)
@@ -18873,7 +18897,7 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
18873
18897
  suggested.push("refresh_chat_recall");
18874
18898
  if (chatCount > 0)
18875
18899
  suggested.push("recent_chat", "search_chat");
18876
- return Array.from(new Set(suggested)).slice(0, 5);
18900
+ return Array.from(new Set(suggested)).slice(0, 6);
18877
18901
  }
18878
18902
 
18879
18903
  // src/tools/workspace-memory-index.ts
@@ -19519,6 +19543,192 @@ function getCaptureQuality(db, input = {}) {
19519
19543
  };
19520
19544
  }
19521
19545
 
19546
+ // src/tools/agent-memory-index.ts
19547
+ function getAgentMemoryIndex(db, input) {
19548
+ const cwd = input.cwd ?? process.cwd();
19549
+ const projectScoped = input.project_scoped !== false;
19550
+ let projectId = null;
19551
+ let projectName;
19552
+ if (projectScoped) {
19553
+ const detected = detectProject(cwd);
19554
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
19555
+ if (project) {
19556
+ projectId = project.id;
19557
+ projectName = project.name;
19558
+ }
19559
+ }
19560
+ const userFilter = input.user_id ? " AND s.user_id = ?" : "";
19561
+ const userArgs = input.user_id ? [input.user_id] : [];
19562
+ const projectFilter = projectId !== null ? " AND s.project_id = ?" : "";
19563
+ const projectArgs = projectId !== null ? [projectId] : [];
19564
+ const sessionRows = db.db.query(`SELECT
19565
+ s.agent as agent,
19566
+ COUNT(*) as session_count,
19567
+ SUM(CASE WHEN ss.request IS NOT NULL OR ss.completed IS NOT NULL THEN 1 ELSE 0 END) as summary_session_count,
19568
+ SUM(COALESCE(pc.prompt_count, 0)) as prompt_count,
19569
+ SUM(COALESCE(tc.tool_event_count, 0)) as tool_event_count,
19570
+ MAX(COALESCE(s.completed_at_epoch, s.started_at_epoch)) as last_seen_epoch
19571
+ FROM sessions s
19572
+ LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
19573
+ LEFT JOIN (
19574
+ SELECT session_id, COUNT(*) as prompt_count
19575
+ FROM user_prompts
19576
+ GROUP BY session_id
19577
+ ) pc ON pc.session_id = s.session_id
19578
+ LEFT JOIN (
19579
+ SELECT session_id, COUNT(*) as tool_event_count
19580
+ FROM tool_events
19581
+ GROUP BY session_id
19582
+ ) tc ON tc.session_id = s.session_id
19583
+ WHERE 1 = 1${projectFilter}${userFilter}
19584
+ GROUP BY s.agent
19585
+ ORDER BY last_seen_epoch DESC, s.agent ASC`).all(...projectArgs, ...userArgs).filter((row) => !isInternalAgent(row.agent));
19586
+ const observationCounts = new Map;
19587
+ const observationRows = db.db.query(`SELECT
19588
+ COALESCE(s.agent, o.agent) as agent,
19589
+ COUNT(*) as observation_count,
19590
+ SUM(CASE WHEN o.type = 'message' AND o.concepts LIKE '%session-handoff%' THEN 1 ELSE 0 END) as handoff_count
19591
+ FROM observations o
19592
+ LEFT JOIN sessions s ON s.session_id = o.session_id
19593
+ WHERE o.lifecycle IN ('active', 'aging', 'pinned')
19594
+ AND o.superseded_by IS NULL
19595
+ ${projectId !== null ? "AND o.project_id = ?" : ""}
19596
+ ${input.user_id ? "AND (o.sensitivity != 'personal' OR o.user_id = ?)" : ""}
19597
+ GROUP BY COALESCE(s.agent, o.agent)`).all(...projectId !== null ? [projectId] : [], ...input.user_id ? [input.user_id] : []).filter((row) => !isInternalAgent(row.agent));
19598
+ for (const row of observationRows) {
19599
+ observationCounts.set(row.agent, {
19600
+ observation_count: row.observation_count,
19601
+ handoff_count: row.handoff_count
19602
+ });
19603
+ }
19604
+ const chatCoverage = new Map;
19605
+ const chatRows = db.db.query(`SELECT
19606
+ COALESCE(s.agent, cm.agent) as agent,
19607
+ CASE
19608
+ WHEN cm.source_kind = 'transcript' THEN 'transcript'
19609
+ WHEN cm.remote_source_id LIKE 'history:%' THEN 'history'
19610
+ ELSE 'hook'
19611
+ END as origin_kind,
19612
+ COUNT(*) as count
19613
+ FROM chat_messages cm
19614
+ LEFT JOIN sessions s ON s.session_id = cm.session_id
19615
+ WHERE 1 = 1
19616
+ ${projectId !== null ? "AND cm.project_id = ?" : ""}
19617
+ ${input.user_id ? "AND cm.user_id = ?" : ""}
19618
+ GROUP BY COALESCE(s.agent, cm.agent), origin_kind`).all(...projectId !== null ? [projectId] : [], ...input.user_id ? [input.user_id] : []).filter((row) => !isInternalAgent(row.agent));
19619
+ for (const row of chatRows) {
19620
+ const current = chatCoverage.get(row.agent) ?? {
19621
+ chat_message_count: 0,
19622
+ transcript_count: 0,
19623
+ history_count: 0,
19624
+ hook_count: 0
19625
+ };
19626
+ current.chat_message_count += row.count;
19627
+ if (row.origin_kind === "transcript")
19628
+ current.transcript_count += row.count;
19629
+ else if (row.origin_kind === "history")
19630
+ current.history_count += row.count;
19631
+ else
19632
+ current.hook_count += row.count;
19633
+ chatCoverage.set(row.agent, current);
19634
+ }
19635
+ const recentSessions = db.getRecentSessions(projectId, 200, input.user_id).filter((session) => !isInternalAgent(session.agent));
19636
+ const latestByAgent = new Map;
19637
+ const devicesByAgent = new Map;
19638
+ for (const session of recentSessions) {
19639
+ if (!latestByAgent.has(session.agent))
19640
+ latestByAgent.set(session.agent, session);
19641
+ const devices = devicesByAgent.get(session.agent) ?? new Set;
19642
+ if (session.device_id)
19643
+ devices.add(session.device_id);
19644
+ devicesByAgent.set(session.agent, devices);
19645
+ }
19646
+ const knownAgents = new Set([
19647
+ ...sessionRows.map((row) => row.agent),
19648
+ ...Array.from(observationCounts.keys()),
19649
+ ...Array.from(chatCoverage.keys())
19650
+ ]);
19651
+ const agents = Array.from(knownAgents).map((agent) => {
19652
+ const session = sessionRows.find((row) => row.agent === agent) ?? {
19653
+ agent,
19654
+ session_count: 0,
19655
+ summary_session_count: 0,
19656
+ prompt_count: 0,
19657
+ tool_event_count: 0,
19658
+ last_seen_epoch: null
19659
+ };
19660
+ const obs = observationCounts.get(agent) ?? { observation_count: 0, handoff_count: 0 };
19661
+ const chat = chatCoverage.get(agent) ?? {
19662
+ chat_message_count: 0,
19663
+ transcript_count: 0,
19664
+ history_count: 0,
19665
+ hook_count: 0
19666
+ };
19667
+ const latestSession = latestByAgent.get(agent) ?? null;
19668
+ return {
19669
+ agent,
19670
+ session_count: session.session_count,
19671
+ summary_session_count: session.summary_session_count,
19672
+ prompt_count: session.prompt_count,
19673
+ tool_event_count: session.tool_event_count,
19674
+ observation_count: obs.observation_count,
19675
+ handoff_count: obs.handoff_count,
19676
+ chat_message_count: chat.chat_message_count,
19677
+ chat_coverage_state: chat.transcript_count > 0 ? "transcript-backed" : chat.history_count > 0 ? "history-backed" : chat.hook_count > 0 ? "hook-only" : "none",
19678
+ continuity_state: classifyAgentContinuity(session.last_seen_epoch, session.prompt_count, session.tool_event_count, chat.chat_message_count, obs.handoff_count, obs.observation_count),
19679
+ capture_state: classifyAgentCaptureState(session.prompt_count, session.tool_event_count, session.summary_session_count, obs.observation_count, chat.chat_message_count),
19680
+ last_seen_epoch: session.last_seen_epoch,
19681
+ latest_session_id: latestSession?.session_id ?? null,
19682
+ latest_summary: latestSession?.current_thread ?? latestSession?.request ?? latestSession?.completed ?? null,
19683
+ devices: Array.from(devicesByAgent.get(agent) ?? []).sort()
19684
+ };
19685
+ }).sort((a, b) => {
19686
+ const epochA = a.last_seen_epoch ?? 0;
19687
+ const epochB = b.last_seen_epoch ?? 0;
19688
+ return epochB - epochA || a.agent.localeCompare(b.agent);
19689
+ });
19690
+ return {
19691
+ project: projectName,
19692
+ agents,
19693
+ suggested_tools: buildSuggestedTools2(agents)
19694
+ };
19695
+ }
19696
+ function isInternalAgent(agent) {
19697
+ return !agent || agent.startsWith("engrm-");
19698
+ }
19699
+ function classifyAgentContinuity(lastSeenEpoch, promptCount, toolCount, chatCount, handoffCount, observationCount) {
19700
+ if (!lastSeenEpoch)
19701
+ return "cold";
19702
+ const ageMs = Date.now() - lastSeenEpoch * 1000;
19703
+ const hasStrongContinuity = promptCount > 0 || toolCount > 0 || chatCount > 0 || handoffCount > 0;
19704
+ if (ageMs <= 3 * 24 * 60 * 60 * 1000 && hasStrongContinuity)
19705
+ return "fresh";
19706
+ if (observationCount > 0 || promptCount > 0 || toolCount > 0 || chatCount > 0)
19707
+ return "thin";
19708
+ return "cold";
19709
+ }
19710
+ function classifyAgentCaptureState(promptCount, toolCount, summarySessionCount, observationCount, chatCount) {
19711
+ if (promptCount > 0 && toolCount > 0)
19712
+ return "rich";
19713
+ if (promptCount > 0 || toolCount > 0)
19714
+ return "partial";
19715
+ if (summarySessionCount > 0 || observationCount > 0 || chatCount > 0)
19716
+ return "summary-only";
19717
+ return "legacy";
19718
+ }
19719
+ function buildSuggestedTools2(agents) {
19720
+ if (agents.length === 0)
19721
+ return [];
19722
+ const suggestions = ["recent_sessions", "capture_quality"];
19723
+ if (agents.some((agent) => agent.continuity_state !== "fresh")) {
19724
+ suggestions.push("resume_thread");
19725
+ }
19726
+ if (agents.some((agent) => agent.chat_coverage_state === "hook-only")) {
19727
+ suggestions.push("repair_recall");
19728
+ }
19729
+ return suggestions;
19730
+ }
19731
+
19522
19732
  // src/tools/tool-memory-index.ts
19523
19733
  function parseConcepts(value) {
19524
19734
  if (!value)
@@ -19721,9 +19931,13 @@ function getSessionContext(db, input) {
19721
19931
  const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
19722
19932
  const latestChatEpoch = recentChat.messages.length > 0 ? recentChat.messages[recentChat.messages.length - 1]?.created_at_epoch ?? null : null;
19723
19933
  const resumeTimestamp = latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
19934
+ const bestRecallItem = recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null;
19935
+ const activeAgents = collectActiveAgents(context.recentSessions ?? []);
19724
19936
  return {
19725
19937
  project_name: context.project_name,
19726
19938
  canonical_id: context.canonical_id,
19939
+ active_agents: activeAgents,
19940
+ cross_agent_active: activeAgents.length > 1,
19727
19941
  continuity_state: continuityState,
19728
19942
  continuity_summary: describeContinuityState(continuityState),
19729
19943
  recall_mode: recallIndex.continuity_mode,
@@ -19734,6 +19948,9 @@ function getSessionContext(db, input) {
19734
19948
  freshness: item.freshness,
19735
19949
  title: item.title
19736
19950
  })),
19951
+ best_recall_key: bestRecallItem?.key ?? null,
19952
+ best_recall_title: bestRecallItem?.title ?? null,
19953
+ best_recall_kind: bestRecallItem?.kind ?? null,
19737
19954
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
19738
19955
  resume_source_session_id: latestSession?.session_id ?? null,
19739
19956
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -19756,7 +19973,7 @@ function getSessionContext(db, input) {
19756
19973
  capture_state: captureState,
19757
19974
  raw_capture_active: recentRequests > 0 || recentTools > 0,
19758
19975
  estimated_read_tokens: estimateTokens(preview),
19759
- suggested_tools: buildSuggestedTools2(context, recentChat.coverage_state),
19976
+ suggested_tools: buildSuggestedTools3(context, recentChat.coverage_state, activeAgents.length),
19760
19977
  preview
19761
19978
  };
19762
19979
  }
@@ -19787,11 +20004,14 @@ function parseJsonArray3(value) {
19787
20004
  return [];
19788
20005
  }
19789
20006
  }
19790
- function buildSuggestedTools2(context, chatCoverageState) {
20007
+ function buildSuggestedTools3(context, chatCoverageState, activeAgentCount) {
19791
20008
  const tools = [];
19792
20009
  if ((context.recentSessions?.length ?? 0) > 0) {
19793
20010
  tools.push("recent_sessions");
19794
20011
  }
20012
+ if (activeAgentCount > 1) {
20013
+ tools.push("agent_memory_index");
20014
+ }
19795
20015
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
19796
20016
  tools.push("activity_feed");
19797
20017
  }
@@ -19819,7 +20039,7 @@ function buildSuggestedTools2(context, chatCoverageState) {
19819
20039
  if ((context.recentChatMessages?.length ?? 0) > 0) {
19820
20040
  tools.push("recent_chat", "search_chat");
19821
20041
  }
19822
- return Array.from(new Set(tools)).slice(0, 5);
20042
+ return Array.from(new Set(tools)).slice(0, 6);
19823
20043
  }
19824
20044
 
19825
20045
  // src/tools/load-recall-item.ts
@@ -20390,6 +20610,7 @@ async function resumeThread(db, config2, input = {}) {
20390
20610
  }
20391
20611
  }
20392
20612
  const { context, handoff, recentChat, recentSessions, recall } = snapshot;
20613
+ const bestRecallItem = pickBestRecallItem2(snapshot.recallIndex.items);
20393
20614
  const latestSession = recentSessions[0] ?? null;
20394
20615
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
20395
20616
  const inferredRequest = latestSession?.request?.trim() || null;
@@ -20418,6 +20639,7 @@ async function resumeThread(db, config2, input = {}) {
20418
20639
  currentThread
20419
20640
  });
20420
20641
  const suggestedTools = Array.from(new Set([
20642
+ ...bestRecallItem ? ["load_recall_item"] : [],
20421
20643
  "search_recall",
20422
20644
  ...recentChat.coverage_state !== "transcript-backed" && recentChat.messages.length > 0 ? ["repair_recall", "refresh_chat_recall"] : [],
20423
20645
  ...handoff ? ["load_handoff"] : [],
@@ -20432,6 +20654,9 @@ async function resumeThread(db, config2, input = {}) {
20432
20654
  resume_source_device_id: handoff?.device_id ?? latestSession?.device_id ?? null,
20433
20655
  resume_confidence: resumeConfidence,
20434
20656
  resume_basis: resumeBasis,
20657
+ best_recall_key: bestRecallItem?.key ?? null,
20658
+ best_recall_title: bestRecallItem?.title ?? null,
20659
+ best_recall_kind: bestRecallItem?.kind ?? null,
20435
20660
  repair_attempted: shouldRepair,
20436
20661
  repair_result: repairResult ? {
20437
20662
  imported_chat_messages: repairResult.imported_chat_messages,
@@ -20490,14 +20715,25 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20490
20715
  user_id: userId,
20491
20716
  limit
20492
20717
  });
20718
+ const recallIndex = listRecallItems(db, {
20719
+ cwd,
20720
+ project_scoped: true,
20721
+ user_id: userId,
20722
+ current_device_id: currentDeviceId,
20723
+ limit
20724
+ });
20493
20725
  return {
20494
20726
  context,
20495
20727
  handoff: handoffResult.handoff,
20496
20728
  recentChat,
20497
20729
  recentSessions,
20498
- recall
20730
+ recall,
20731
+ recallIndex
20499
20732
  };
20500
20733
  }
20734
+ function pickBestRecallItem2(items) {
20735
+ return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
20736
+ }
20501
20737
  function extractCurrentThread(handoff) {
20502
20738
  const narrative = handoff?.narrative ?? "";
20503
20739
  const match = narrative.match(/Current thread:\s*(.+)/i);
@@ -22429,7 +22665,7 @@ process.on("SIGTERM", () => {
22429
22665
  });
22430
22666
  var server = new McpServer({
22431
22667
  name: "engrm",
22432
- version: "0.4.32"
22668
+ version: "0.4.34"
22433
22669
  });
22434
22670
  server.tool("save_observation", "Save an observation to memory", {
22435
22671
  type: exports_external.enum([
@@ -23023,6 +23259,8 @@ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?
23023
23259
  const handoffLine = result.handoff ? `Handoff: #${result.handoff.id} ${result.handoff.title}${result.handoff.source ? ` (${result.handoff.source})` : ""}
23024
23260
  ` : `Handoff: (none)
23025
23261
  `;
23262
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23263
+ ` : "";
23026
23264
  const basisLines = result.resume_basis.length > 0 ? result.resume_basis.map((item) => `- ${item}`).join(`
23027
23265
  `) : "- (none)";
23028
23266
  const toolTrailLines = result.tool_trail.length > 0 ? result.tool_trail.map((item) => `- ${item}`).join(`
@@ -23057,7 +23295,7 @@ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?
23057
23295
  ` + `Freshness: ${result.resume_freshness}
23058
23296
  ` + `Source: ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23059
23297
  ` + `Resume confidence: ${result.resume_confidence}
23060
- ` + repairLine + `Current thread: ${result.current_thread ?? "(unknown)"}
23298
+ ` + repairLine + openExactLine + `Current thread: ${result.current_thread ?? "(unknown)"}
23061
23299
  ` + `Latest request: ${result.latest_request ?? "(none)"}
23062
23300
  ` + `${handoffLine}` + `Chat recall: ${result.chat_coverage_state}
23063
23301
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
@@ -23395,6 +23633,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23395
23633
  `) : "- (none)";
23396
23634
  const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
23397
23635
  `) : "- (none)";
23636
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23637
+ ` : "";
23398
23638
  const projectLine = result.project ? `Project: ${result.project}
23399
23639
 
23400
23640
  ` : "";
@@ -23407,7 +23647,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23407
23647
  content: [
23408
23648
  {
23409
23649
  type: "text",
23410
- text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} ${result.continuity_summary}
23650
+ text: `${projectLine}` + `${captureLine}` + `${result.active_agents.length > 0 ? `Agents active: ${result.active_agents.join(", ")}
23651
+ ` : ""}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23411
23652
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23412
23653
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23413
23654
  ` + `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})
@@ -23416,7 +23657,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23416
23657
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
23417
23658
  ` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23418
23659
 
23419
- ` + `Recall preview:
23660
+ ` + openExactLine + `Recall preview:
23420
23661
  ${recallPreviewLines}
23421
23662
 
23422
23663
  ` + `Next actions:
@@ -23530,6 +23771,37 @@ ${projectLines}`
23530
23771
  ]
23531
23772
  };
23532
23773
  });
23774
+ server.tool("agent_memory_index", "Compare continuity and capture health across Claude Code, Codex, OpenClaw, and other agents for the current project or workspace.", {
23775
+ cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
23776
+ project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
23777
+ user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
23778
+ }, async (params) => {
23779
+ const result = getAgentMemoryIndex(db, {
23780
+ cwd: params.cwd ?? process.cwd(),
23781
+ project_scoped: params.project_scoped,
23782
+ user_id: params.user_id ?? config2.user_id
23783
+ });
23784
+ const rows = result.agents.length > 0 ? result.agents.map((agent) => {
23785
+ const lastSeen = agent.last_seen_epoch ? new Date(agent.last_seen_epoch * 1000).toISOString().replace("T", " ").slice(0, 16) : "unknown";
23786
+ const latest = agent.latest_summary ? ` latest="${agent.latest_summary.replace(/\s+/g, " ").trim().slice(0, 120)}"` : "";
23787
+ const devices = agent.devices.length > 0 ? ` devices=[${agent.devices.join(", ")}]` : "";
23788
+ return `- ${agent.agent}: continuity=${agent.continuity_state} capture=${agent.capture_state} chat=${agent.chat_coverage_state} sessions=${agent.session_count} prompts=${agent.prompt_count} tools=${agent.tool_event_count} obs=${agent.observation_count} handoffs=${agent.handoff_count} chat_msgs=${agent.chat_message_count} last_seen=${lastSeen}${devices}${latest}`;
23789
+ }).join(`
23790
+ `) : "- (none)";
23791
+ return {
23792
+ content: [
23793
+ {
23794
+ type: "text",
23795
+ text: `${result.project ? `Project: ${result.project}
23796
+
23797
+ ` : ""}` + `Agent memory index:
23798
+ ${rows}
23799
+
23800
+ ` + `Suggested next step: ${result.suggested_tools.join(", ") || "(none)"}`
23801
+ }
23802
+ ]
23803
+ };
23804
+ });
23533
23805
  server.tool("tool_memory_index", "Show which tools are actually producing durable memory, which plugins they exercise, and what memory types they create.", {
23534
23806
  cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
23535
23807
  project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
@@ -23615,8 +23887,10 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23615
23887
  type: "text",
23616
23888
  text: `Project: ${result.project_name}
23617
23889
  ` + `Canonical ID: ${result.canonical_id}
23890
+ ` + `Agents active: ${result.active_agents.join(", ") || "(none)"}
23618
23891
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23619
23892
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23893
+ ` + `Open exact: ${result.best_recall_key ? `load_recall_item("${result.best_recall_key}")` : "(none)"}
23620
23894
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23621
23895
  ` + `Loaded observations: ${result.session_count}
23622
23896
  ` + `Searchable total: ${result.total_active}
@@ -23697,12 +23971,15 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23697
23971
  `) : "- (none)";
23698
23972
  const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
23699
23973
  `) : "- (none)";
23974
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23975
+ ` : "";
23700
23976
  return {
23701
23977
  content: [
23702
23978
  {
23703
23979
  type: "text",
23704
23980
  text: `Project: ${result.project}
23705
23981
  ` + `Canonical ID: ${result.canonical_id}
23982
+ ` + `Agents active: ${result.active_agents.join(", ") || "(none)"}
23706
23983
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23707
23984
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23708
23985
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
@@ -23720,7 +23997,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23720
23997
  ` + `Estimated read cost: ~${result.estimated_read_tokens}t
23721
23998
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23722
23999
 
23723
- ` + `Recall preview:
24000
+ ` + openExactLine + `Recall preview:
23724
24001
  ${recallPreviewLines}
23725
24002
 
23726
24003
  ` + `Next actions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.32",
3
+ "version": "0.4.34",
4
4
  "description": "Shared memory across devices, sessions, and agents, with thin MCP tools for durable capture and live continuity",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",