engrm 0.4.33 → 0.4.35

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,8 +403,14 @@ 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
408
+ - `agent_memory_index` now also gives the best exact recall jump per agent, so you can compare agents and open the right handoff/thread immediately
409
+ - recall previews and `load_recall_item` now show source-agent provenance too, so exact recall stays readable when Claude, Codex, and OpenClaw all touch the same project
405
410
  - `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
406
411
  - `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
412
+ - `resume_thread(agent="claude-code")` lets you deliberately recover one agent's thread on a shared repo instead of only taking the blended repo-level default
413
+ - startup and the MCP workbench now surface a direct `resume_thread(agent="...")` hint when multiple agents are active on one repo
407
414
  - `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
415
  - `load_recall_item` completes that protocol by letting agents open one exact recall key directly after listing
409
416
  - `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
@@ -2297,6 +2297,61 @@ function formatHandoffSource2(handoff) {
2297
2297
  return `from ${handoff.device_id} · ${ageLabel}`;
2298
2298
  }
2299
2299
 
2300
+ // src/tools/inbox-messages.ts
2301
+ function buildHandoffMessageFilterSql(lifecycle) {
2302
+ return `
2303
+ type = 'message'
2304
+ AND lifecycle IN (${lifecycle})
2305
+ AND (
2306
+ COALESCE(source_tool, '') IN ('create_handoff', 'rolling_handoff')
2307
+ OR title LIKE 'Handoff:%'
2308
+ OR title LIKE 'Handoff Draft:%'
2309
+ OR (
2310
+ concepts IS NOT NULL AND (
2311
+ concepts LIKE '%"handoff"%'
2312
+ OR concepts LIKE '%"session-handoff"%'
2313
+ OR concepts LIKE '%"draft-handoff"%'
2314
+ )
2315
+ )
2316
+ )
2317
+ `;
2318
+ }
2319
+ var HANDOFF_MESSAGE_FILTER_SQL = buildHandoffMessageFilterSql("'active', 'pinned'");
2320
+ var INBOX_MESSAGE_FILTER_SQL = `
2321
+ type = 'message'
2322
+ AND lifecycle IN ('active', 'pinned')
2323
+ AND NOT (${HANDOFF_MESSAGE_FILTER_SQL})
2324
+ `;
2325
+ function classifyMessageObservation(observation) {
2326
+ if (observation.type !== "message")
2327
+ return null;
2328
+ const concepts = parseConcepts(observation.concepts);
2329
+ const isDraft = observation.title.startsWith("Handoff Draft:") || observation.source_tool === "rolling_handoff" || concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
2330
+ if (isDraft)
2331
+ return "draft-handoff";
2332
+ const isHandoff = observation.title.startsWith("Handoff:") || observation.source_tool === "create_handoff" || concepts.includes("handoff") || concepts.includes("session-handoff");
2333
+ if (isHandoff)
2334
+ return "handoff";
2335
+ return "inbox-note";
2336
+ }
2337
+ function parseConcepts(value) {
2338
+ if (!value)
2339
+ return [];
2340
+ try {
2341
+ const parsed = JSON.parse(value);
2342
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
2343
+ } catch {
2344
+ return [];
2345
+ }
2346
+ }
2347
+ function getUnreadInboxMessageCount(db, currentDeviceId, userId, lastReadId) {
2348
+ return db.db.query(`SELECT COUNT(*) as c FROM observations
2349
+ WHERE ${INBOX_MESSAGE_FILTER_SQL}
2350
+ AND id > ?
2351
+ AND device_id != ?
2352
+ AND (sensitivity != 'personal' OR user_id = ?)`).get(lastReadId, currentDeviceId, userId)?.c ?? 0;
2353
+ }
2354
+
2300
2355
  // src/context/inject.ts
2301
2356
  var FRESH_CONTINUITY_WINDOW_DAYS2 = 3;
2302
2357
  function tokenizeProjectHint2(text) {
@@ -2992,6 +3047,32 @@ function getRecentOutcomes2(db, projectId, userId, recentSessions) {
2992
3047
  return picked;
2993
3048
  }
2994
3049
 
3050
+ // src/tools/inbox-messages.ts
3051
+ function buildHandoffMessageFilterSql2(lifecycle) {
3052
+ return `
3053
+ type = 'message'
3054
+ AND lifecycle IN (${lifecycle})
3055
+ AND (
3056
+ COALESCE(source_tool, '') IN ('create_handoff', 'rolling_handoff')
3057
+ OR title LIKE 'Handoff:%'
3058
+ OR title LIKE 'Handoff Draft:%'
3059
+ OR (
3060
+ concepts IS NOT NULL AND (
3061
+ concepts LIKE '%"handoff"%'
3062
+ OR concepts LIKE '%"session-handoff"%'
3063
+ OR concepts LIKE '%"draft-handoff"%'
3064
+ )
3065
+ )
3066
+ )
3067
+ `;
3068
+ }
3069
+ var HANDOFF_MESSAGE_FILTER_SQL2 = buildHandoffMessageFilterSql2("'active', 'pinned'");
3070
+ var INBOX_MESSAGE_FILTER_SQL2 = `
3071
+ type = 'message'
3072
+ AND lifecycle IN ('active', 'pinned')
3073
+ AND NOT (${HANDOFF_MESSAGE_FILTER_SQL2})
3074
+ `;
3075
+
2995
3076
  // src/tools/project-memory-index.ts
2996
3077
  function classifyResumeFreshness(sourceTimestamp) {
2997
3078
  if (!sourceTimestamp)
@@ -3144,7 +3225,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3144
3225
  import { join as join3 } from "node:path";
3145
3226
  import { homedir } from "node:os";
3146
3227
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3147
- var CLIENT_VERSION = "0.4.33";
3228
+ var CLIENT_VERSION = "0.4.35";
3148
3229
  function hashFile(filePath) {
3149
3230
  try {
3150
3231
  if (!existsSync3(filePath))
@@ -5494,12 +5575,7 @@ async function main() {
5494
5575
  try {
5495
5576
  const readKey = `messages_read_${config.device_id}`;
5496
5577
  const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
5497
- msgCount = db.db.query(`SELECT COUNT(*) as c FROM observations
5498
- WHERE type = 'message'
5499
- AND id > ?
5500
- AND lifecycle IN ('active', 'pinned')
5501
- AND device_id != ?
5502
- AND (sensitivity != 'personal' OR user_id = ?)`).get(lastReadId, config.device_id, config.user_id)?.c ?? 0;
5578
+ msgCount = getUnreadInboxMessageCount(db, config.device_id, config.user_id, lastReadId);
5503
5579
  } catch {}
5504
5580
  const splash = formatSplashScreen({
5505
5581
  projectName: context.project_name,
@@ -5653,6 +5729,7 @@ function formatVisibleStartupBrief(context) {
5653
5729
  const projectSignals = buildProjectSignalLine(context);
5654
5730
  const shownItems = new Set;
5655
5731
  const latestHandoffLines = buildLatestHandoffLines(context);
5732
+ const inboxNoteLines = buildRecentInboxNoteLines(context);
5656
5733
  const freshContinuity = hasFreshContinuitySignal(context);
5657
5734
  if (latestHandoffLines.length > 0) {
5658
5735
  lines.push(`${c2.cyan}Latest handoff:${c2.reset}`);
@@ -5661,6 +5738,13 @@ function formatVisibleStartupBrief(context) {
5661
5738
  rememberShownItem(shownItems, item);
5662
5739
  }
5663
5740
  }
5741
+ if (inboxNoteLines.length > 0) {
5742
+ lines.push(`${c2.cyan}Inbox notes:${c2.reset}`);
5743
+ for (const item of inboxNoteLines) {
5744
+ lines.push(` - ${truncateInline(item, 160)}`);
5745
+ rememberShownItem(shownItems, item);
5746
+ }
5747
+ }
5664
5748
  lines.push(`${c2.cyan}Continuity:${c2.reset} ${continuityState} \u2014 ${truncateInline(describeContinuityState(continuityState), 160)}`);
5665
5749
  const resumeReadiness = buildResumeReadinessLine(context);
5666
5750
  if (resumeReadiness) {
@@ -5786,6 +5870,10 @@ function buildLatestHandoffLines(context) {
5786
5870
  }
5787
5871
  return Array.from(new Set(lines.filter(Boolean))).slice(0, 2);
5788
5872
  }
5873
+ function buildRecentInboxNoteLines(context) {
5874
+ const notes = context.observations.filter((obs) => classifyMessageObservation(obs) === "inbox-note").slice(0, 2);
5875
+ return Array.from(new Set(notes.map((obs) => obs.title.trim()).filter((title) => title.length > 0)));
5876
+ }
5789
5877
  function buildResumeReadinessLine(context) {
5790
5878
  const latestSession = context.recentSessions?.[0] ?? null;
5791
5879
  const latestHandoff = context.recentHandoffs?.[0] ?? null;
@@ -5847,11 +5935,15 @@ function formatContextIndex(context, shownItems) {
5847
5935
  function formatInspectHints(context, visibleObservationIds = [], recallItems = []) {
5848
5936
  const hints = [];
5849
5937
  const continuityState = getStartupContinuityState(context);
5938
+ const activeAgents = collectStartupAgents(context);
5850
5939
  if ((context.recentSessions?.length ?? 0) > 0) {
5851
5940
  hints.push("recent_sessions");
5852
5941
  hints.push("session_story");
5853
5942
  hints.push("create_handoff");
5854
5943
  }
5944
+ if (activeAgents.length > 1) {
5945
+ hints.push("agent_memory_index");
5946
+ }
5855
5947
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0) {
5856
5948
  hints.push("activity_feed");
5857
5949
  }
@@ -5884,20 +5976,25 @@ function formatInspectHints(context, visibleObservationIds = [], recallItems = [
5884
5976
  return [];
5885
5977
  const ids = visibleObservationIds.slice(0, 5);
5886
5978
  const openNowItem = recallItems.find((item) => item.kind !== "memory") ?? null;
5979
+ const resumeAgent = activeAgents.length > 1 ? context.recentSessions?.[0]?.agent ?? null : null;
5887
5980
  const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
5888
5981
  return [
5889
5982
  `${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
5890
5983
  ...openNowItem ? [`${c2.dim}Open now:${c2.reset} load_recall_item("${openNowItem.key}")`] : [],
5984
+ ...resumeAgent ? [`${c2.dim}Resume agent:${c2.reset} resume_thread(agent="${resumeAgent}")`] : [],
5891
5985
  ...fetchHint ? [`${c2.dim}Pull detail:${c2.reset} ${fetchHint}`] : []
5892
5986
  ];
5893
5987
  }
5988
+ function collectStartupAgents(context) {
5989
+ return Array.from(new Set((context.recentSessions ?? []).map((session) => session.agent?.trim()).filter((agent) => Boolean(agent) && !agent.startsWith("engrm-")))).sort();
5990
+ }
5894
5991
  function formatStartupRecallPreview(recallItems) {
5895
5992
  const items = recallItems.slice(0, 3);
5896
5993
  if (items.length === 0)
5897
5994
  return [];
5898
5995
  return [
5899
5996
  `${c2.dim}Recall preview:${c2.reset} exact keys you can open now`,
5900
- ...items.map((item) => `${item.key} [${item.kind} \xB7 ${item.freshness}] ${truncateInline(item.title, 110)}`)
5997
+ ...items.map((item) => `${item.key} [${item.kind} \xB7 ${item.freshness}${item.source_agent ? ` \xB7 ${item.source_agent}` : ""}] ${truncateInline(item.title, 110)}`)
5901
5998
  ];
5902
5999
  }
5903
6000
  function buildStartupRecallItems(context) {
@@ -5912,6 +6009,7 @@ function buildStartupRecallItems(context) {
5912
6009
  kind: "handoff",
5913
6010
  freshness,
5914
6011
  title,
6012
+ source_agent: (context.recentSessions ?? []).find((session) => session.session_id === handoff.session_id)?.agent ?? null,
5915
6013
  score: freshnessScore(freshness) + 40
5916
6014
  });
5917
6015
  }
@@ -5926,6 +6024,7 @@ function buildStartupRecallItems(context) {
5926
6024
  kind: "thread",
5927
6025
  freshness,
5928
6026
  title,
6027
+ source_agent: session.agent ?? null,
5929
6028
  score: freshnessScore(freshness) + 30
5930
6029
  });
5931
6030
  }
@@ -5938,6 +6037,7 @@ function buildStartupRecallItems(context) {
5938
6037
  kind: "chat",
5939
6038
  freshness,
5940
6039
  title: `[${message.role}] ${message.content.replace(/\s+/g, " ").trim()}`,
6040
+ source_agent: message.agent ?? null,
5941
6041
  score: freshnessScore(freshness) + 20
5942
6042
  });
5943
6043
  }
@@ -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.33",
3085
+ client_version: "0.4.35",
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
@@ -16827,6 +16827,69 @@ function pinObservation(db, input) {
16827
16827
  return { success: true };
16828
16828
  }
16829
16829
 
16830
+ // src/tools/inbox-messages.ts
16831
+ function buildHandoffMessageFilterSql(lifecycle) {
16832
+ return `
16833
+ type = 'message'
16834
+ AND lifecycle IN (${lifecycle})
16835
+ AND (
16836
+ COALESCE(source_tool, '') IN ('create_handoff', 'rolling_handoff')
16837
+ OR title LIKE 'Handoff:%'
16838
+ OR title LIKE 'Handoff Draft:%'
16839
+ OR (
16840
+ concepts IS NOT NULL AND (
16841
+ concepts LIKE '%"handoff"%'
16842
+ OR concepts LIKE '%"session-handoff"%'
16843
+ OR concepts LIKE '%"draft-handoff"%'
16844
+ )
16845
+ )
16846
+ )
16847
+ `;
16848
+ }
16849
+ var HANDOFF_MESSAGE_FILTER_SQL = buildHandoffMessageFilterSql("'active', 'pinned'");
16850
+ var INBOX_MESSAGE_FILTER_SQL = `
16851
+ type = 'message'
16852
+ AND lifecycle IN ('active', 'pinned')
16853
+ AND NOT (${HANDOFF_MESSAGE_FILTER_SQL})
16854
+ `;
16855
+ function getHandoffMessageFilterSql(options) {
16856
+ return buildHandoffMessageFilterSql(options?.include_aging ? "'active', 'aging', 'pinned'" : "'active', 'pinned'");
16857
+ }
16858
+ function classifyMessageObservation(observation) {
16859
+ if (observation.type !== "message")
16860
+ return null;
16861
+ const concepts = parseConcepts(observation.concepts);
16862
+ const isDraft = observation.title.startsWith("Handoff Draft:") || observation.source_tool === "rolling_handoff" || concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
16863
+ if (isDraft)
16864
+ return "draft-handoff";
16865
+ const isHandoff = observation.title.startsWith("Handoff:") || observation.source_tool === "create_handoff" || concepts.includes("handoff") || concepts.includes("session-handoff");
16866
+ if (isHandoff)
16867
+ return "handoff";
16868
+ return "inbox-note";
16869
+ }
16870
+ function parseConcepts(value) {
16871
+ if (!value)
16872
+ return [];
16873
+ try {
16874
+ const parsed = JSON.parse(value);
16875
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
16876
+ } catch {
16877
+ return [];
16878
+ }
16879
+ }
16880
+ function getInboxMessageCount(db) {
16881
+ return db.db.query(`SELECT COUNT(*) as c FROM observations
16882
+ WHERE ${INBOX_MESSAGE_FILTER_SQL}`).get()?.c ?? 0;
16883
+ }
16884
+ function getUnreadInboxMessages(db, currentDeviceId, userId, lastReadId, limit = 20) {
16885
+ return db.db.query(`SELECT id, title, narrative, user_id, device_id, created_at FROM observations
16886
+ WHERE ${INBOX_MESSAGE_FILTER_SQL}
16887
+ AND id > ?
16888
+ AND device_id != ?
16889
+ AND (sensitivity != 'personal' OR user_id = ?)
16890
+ ORDER BY created_at_epoch DESC LIMIT ?`).all(lastReadId, currentDeviceId, userId, limit);
16891
+ }
16892
+
16830
16893
  // src/tools/recent.ts
16831
16894
  function getRecentActivity(db, input) {
16832
16895
  const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
@@ -16865,7 +16928,10 @@ function getRecentActivity(db, input) {
16865
16928
  LEFT JOIN projects ON projects.id = observations.project_id
16866
16929
  WHERE ${conditions.join(" AND ")}
16867
16930
  ORDER BY observations.created_at_epoch DESC
16868
- LIMIT ?`).all(...params);
16931
+ LIMIT ?`).all(...params).map((observation) => ({
16932
+ ...observation,
16933
+ message_kind: classifyMessageObservation(observation)
16934
+ }));
16869
16935
  return {
16870
16936
  observations,
16871
16937
  project: projectName
@@ -18359,6 +18425,7 @@ function listRecallItems(db, input) {
18359
18425
  user_id: input.user_id,
18360
18426
  limit: Math.min(limit * 2, 24)
18361
18427
  }).observations;
18428
+ const sessionAgentById = new Map(sessions.filter((session) => Boolean(session.session_id)).map((session) => [session.session_id, session.agent ?? null]));
18362
18429
  const items = [
18363
18430
  ...handoffs.map((handoff) => ({
18364
18431
  key: `handoff:${handoff.id}`,
@@ -18368,7 +18435,8 @@ function listRecallItems(db, input) {
18368
18435
  created_at_epoch: handoff.created_at_epoch,
18369
18436
  freshness: classifyFreshness(handoff.created_at_epoch),
18370
18437
  session_id: handoff.session_id,
18371
- source_device_id: handoff.device_id ?? null
18438
+ source_device_id: handoff.device_id ?? null,
18439
+ source_agent: handoff.session_id ? sessionAgentById.get(handoff.session_id) ?? null : null
18372
18440
  })),
18373
18441
  ...sessions.filter((session) => Boolean(session.request || session.completed || session.current_thread)).map((session) => ({
18374
18442
  key: `session:${session.session_id}`,
@@ -18378,7 +18446,8 @@ function listRecallItems(db, input) {
18378
18446
  created_at_epoch: session.completed_at_epoch ?? session.started_at_epoch ?? 0,
18379
18447
  freshness: classifyFreshness(session.completed_at_epoch ?? session.started_at_epoch ?? null),
18380
18448
  session_id: session.session_id,
18381
- source_device_id: session.device_id ?? null
18449
+ source_device_id: session.device_id ?? null,
18450
+ source_agent: session.agent ?? null
18382
18451
  })),
18383
18452
  ...dedupeChatIndex(chat).map((message) => {
18384
18453
  const origin = getChatCaptureOrigin(message);
@@ -18390,7 +18459,8 @@ function listRecallItems(db, input) {
18390
18459
  created_at_epoch: message.created_at_epoch,
18391
18460
  freshness: classifyFreshness(message.created_at_epoch),
18392
18461
  session_id: message.session_id,
18393
- source_device_id: message.device_id ?? null
18462
+ source_device_id: message.device_id ?? null,
18463
+ source_agent: message.agent ?? null
18394
18464
  };
18395
18465
  }),
18396
18466
  ...observations.filter((obs) => obs.type !== "message").filter((obs) => !looksLikeFileOperationTitle3(obs.title)).map((obs) => ({
@@ -18401,7 +18471,8 @@ function listRecallItems(db, input) {
18401
18471
  created_at_epoch: obs.created_at_epoch,
18402
18472
  freshness: classifyFreshness(obs.created_at_epoch),
18403
18473
  session_id: obs.session_id ?? null,
18404
- source_device_id: obs.device_id ?? null
18474
+ source_device_id: obs.device_id ?? null,
18475
+ source_agent: null
18405
18476
  }))
18406
18477
  ];
18407
18478
  const deduped = dedupeRecallItems(items).sort((a, b) => compareRecallItems(a, b, input.current_device_id)).slice(0, limit);
@@ -18572,6 +18643,7 @@ function getProjectMemoryIndex(db, input) {
18572
18643
  }).handoffs;
18573
18644
  const rollingHandoffDraftsCount = recentHandoffsCount.filter((handoff) => isDraftHandoff(handoff)).length;
18574
18645
  const savedHandoffsCount = recentHandoffsCount.length - rollingHandoffDraftsCount;
18646
+ const recentInboxNotes = observations.filter((obs) => classifyMessageObservation(obs) === "inbox-note").slice(0, 5);
18575
18647
  const recentChat = getRecentChat(db, {
18576
18648
  cwd,
18577
18649
  project_scoped: true,
@@ -18589,8 +18661,9 @@ function getProjectMemoryIndex(db, input) {
18589
18661
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
18590
18662
  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
18663
  const captureSummary = summarizeCaptureState(recentSessions);
18664
+ const activeAgents = collectActiveAgents(recentSessions);
18592
18665
  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);
18666
+ const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.coverage_state, activeAgents);
18594
18667
  const estimatedReadTokens = estimateTokens([
18595
18668
  recentOutcomes.join(`
18596
18669
  `),
@@ -18606,6 +18679,8 @@ function getProjectMemoryIndex(db, input) {
18606
18679
  return {
18607
18680
  project: project.name,
18608
18681
  canonical_id: project.canonical_id,
18682
+ active_agents: activeAgents,
18683
+ cross_agent_active: activeAgents.length > 1,
18609
18684
  continuity_state: continuityState,
18610
18685
  continuity_summary: describeContinuityState(continuityState),
18611
18686
  recall_mode: recallIndex.continuity_mode,
@@ -18614,11 +18689,13 @@ function getProjectMemoryIndex(db, input) {
18614
18689
  key: item.key,
18615
18690
  kind: item.kind,
18616
18691
  freshness: item.freshness,
18617
- title: item.title
18692
+ title: item.title,
18693
+ source_agent: item.source_agent
18618
18694
  })),
18619
18695
  best_recall_key: bestRecallItem?.key ?? null,
18620
18696
  best_recall_title: bestRecallItem?.title ?? null,
18621
18697
  best_recall_kind: bestRecallItem?.kind ?? null,
18698
+ best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
18622
18699
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18623
18700
  resume_source_session_id: latestSession?.session_id ?? null,
18624
18701
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18631,6 +18708,8 @@ function getProjectMemoryIndex(db, input) {
18631
18708
  recent_handoffs_count: recentHandoffsCount.length,
18632
18709
  rolling_handoff_drafts_count: rollingHandoffDraftsCount,
18633
18710
  saved_handoffs_count: savedHandoffsCount,
18711
+ recent_inbox_notes_count: recentInboxNotes.length,
18712
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
18634
18713
  recent_chat_count: recentChatCount,
18635
18714
  recent_chat_sessions: recentChat.session_count,
18636
18715
  chat_source_summary: recentChat.source_summary,
@@ -18732,11 +18811,17 @@ function summarizeCaptureState(sessions) {
18732
18811
  }
18733
18812
  return summary;
18734
18813
  }
18735
- function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, chatCoverageState) {
18814
+ function collectActiveAgents(sessions) {
18815
+ return Array.from(new Set(sessions.map((session) => session.agent?.trim()).filter((agent) => Boolean(agent) && !agent.startsWith("engrm-")))).sort();
18816
+ }
18817
+ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, chatCoverageState, activeAgents) {
18736
18818
  const suggested = [];
18737
18819
  if (sessions.length > 0) {
18738
18820
  suggested.push("recent_sessions");
18739
18821
  }
18822
+ if (activeAgents.length > 1) {
18823
+ suggested.push("agent_memory_index");
18824
+ }
18740
18825
  if (requestCount > 0 || toolCount > 0) {
18741
18826
  suggested.push("activity_feed");
18742
18827
  }
@@ -18761,7 +18846,7 @@ function buildSuggestedTools(sessions, requestCount, toolCount, observationCount
18761
18846
  if (recentChatCount > 0) {
18762
18847
  suggested.push("recent_chat", "search_chat");
18763
18848
  }
18764
- return Array.from(new Set(suggested)).slice(0, 5);
18849
+ return Array.from(new Set(suggested)).slice(0, 6);
18765
18850
  }
18766
18851
 
18767
18852
  // src/tools/memory-console.ts
@@ -18802,6 +18887,7 @@ function getMemoryConsole(db, input) {
18802
18887
  }).handoffs;
18803
18888
  const rollingHandoffDrafts = recentHandoffs.filter((handoff) => isDraftHandoff(handoff)).length;
18804
18889
  const savedHandoffs = recentHandoffs.length - rollingHandoffDrafts;
18890
+ const recentInboxNotes = observations.filter((obs) => obs.message_kind === "inbox-note").slice(0, 3).map((obs) => ({ id: obs.id, title: obs.title, created_at_epoch: obs.created_at_epoch }));
18805
18891
  const recentChat = getRecentChat(db, {
18806
18892
  cwd,
18807
18893
  project_scoped: projectScoped,
@@ -18819,8 +18905,11 @@ function getMemoryConsole(db, input) {
18819
18905
  user_id: input.user_id
18820
18906
  }) : null;
18821
18907
  const continuityState = projectIndex?.continuity_state ?? classifyContinuityState(requests.length, tools.length, recentHandoffs.length, recentChat.messages.length, sessions, (projectIndex?.recent_outcomes ?? []).length);
18908
+ const activeAgents = projectIndex?.active_agents ?? collectActiveAgents(sessions);
18822
18909
  return {
18823
18910
  project: project?.name,
18911
+ active_agents: activeAgents,
18912
+ cross_agent_active: projectIndex?.cross_agent_active ?? activeAgents.length > 1,
18824
18913
  capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
18825
18914
  continuity_state: continuityState,
18826
18915
  continuity_summary: projectIndex?.continuity_summary ?? describeContinuityState(continuityState),
@@ -18830,11 +18919,13 @@ function getMemoryConsole(db, input) {
18830
18919
  key: item.key,
18831
18920
  kind: item.kind,
18832
18921
  freshness: item.freshness,
18833
- title: item.title
18922
+ title: item.title,
18923
+ source_agent: item.source_agent
18834
18924
  })),
18835
18925
  best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
18836
18926
  best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
18837
18927
  best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.kind ?? null,
18928
+ best_agent_resume_agent: projectIndex?.best_agent_resume_agent ?? (activeAgents.length > 1 ? sessions[0]?.agent ?? null : null),
18838
18929
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18839
18930
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18840
18931
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -18845,6 +18936,8 @@ function getMemoryConsole(db, input) {
18845
18936
  recent_handoffs: recentHandoffs,
18846
18937
  rolling_handoff_drafts: rollingHandoffDrafts,
18847
18938
  saved_handoffs: savedHandoffs,
18939
+ recent_inbox_notes: recentInboxNotes,
18940
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
18848
18941
  recent_chat: recentChat.messages,
18849
18942
  recent_chat_sessions: projectIndex?.recent_chat_sessions ?? recentChat.session_count,
18850
18943
  chat_source_summary: projectIndex?.chat_source_summary ?? recentChat.source_summary,
@@ -18858,13 +18951,15 @@ function getMemoryConsole(db, input) {
18858
18951
  assistant_checkpoint_types: projectIndex?.assistant_checkpoint_types ?? [],
18859
18952
  top_types: projectIndex?.top_types ?? [],
18860
18953
  estimated_read_tokens: projectIndex?.estimated_read_tokens,
18861
- suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.coverage_state)
18954
+ suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.coverage_state, activeAgents.length)
18862
18955
  };
18863
18956
  }
18864
- function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, chatCoverageState) {
18957
+ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, chatCoverageState, activeAgentCount) {
18865
18958
  const suggested = [];
18866
18959
  if (sessionCount > 0)
18867
18960
  suggested.push("recent_sessions");
18961
+ if (activeAgentCount > 1)
18962
+ suggested.push("agent_memory_index");
18868
18963
  if (requestCount > 0 || toolCount > 0)
18869
18964
  suggested.push("activity_feed");
18870
18965
  if (requestCount > 0 || chatCount > 0 || observationCount > 0)
@@ -18883,7 +18978,7 @@ function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, obse
18883
18978
  suggested.push("refresh_chat_recall");
18884
18979
  if (chatCount > 0)
18885
18980
  suggested.push("recent_chat", "search_chat");
18886
- return Array.from(new Set(suggested)).slice(0, 5);
18981
+ return Array.from(new Set(suggested)).slice(0, 6);
18887
18982
  }
18888
18983
 
18889
18984
  // src/tools/workspace-memory-index.ts
@@ -19111,6 +19206,7 @@ function toChatEvent(message) {
19111
19206
  };
19112
19207
  }
19113
19208
  function toObservationEvent(obs) {
19209
+ const messageKind = classifyMessageObservation(obs);
19114
19210
  if (looksLikeHandoff(obs)) {
19115
19211
  const handoffKind = isDraftHandoff(obs) ? "draft" : "saved";
19116
19212
  return {
@@ -19125,6 +19221,8 @@ function toObservationEvent(obs) {
19125
19221
  };
19126
19222
  }
19127
19223
  const detailBits = [];
19224
+ if (messageKind === "inbox-note")
19225
+ detailBits.push("inbox note");
19128
19226
  if (obs.source_tool)
19129
19227
  detailBits.push(`via ${obs.source_tool}`);
19130
19228
  if (typeof obs.source_prompt_number === "number") {
@@ -19529,8 +19627,214 @@ function getCaptureQuality(db, input = {}) {
19529
19627
  };
19530
19628
  }
19531
19629
 
19630
+ // src/tools/agent-memory-index.ts
19631
+ function getAgentMemoryIndex(db, input) {
19632
+ const cwd = input.cwd ?? process.cwd();
19633
+ const projectScoped = input.project_scoped !== false;
19634
+ let projectId = null;
19635
+ let projectName;
19636
+ if (projectScoped) {
19637
+ const detected = detectProject(cwd);
19638
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
19639
+ if (project) {
19640
+ projectId = project.id;
19641
+ projectName = project.name;
19642
+ }
19643
+ }
19644
+ const userFilter = input.user_id ? " AND s.user_id = ?" : "";
19645
+ const userArgs = input.user_id ? [input.user_id] : [];
19646
+ const projectFilter = projectId !== null ? " AND s.project_id = ?" : "";
19647
+ const projectArgs = projectId !== null ? [projectId] : [];
19648
+ const sessionRows = db.db.query(`SELECT
19649
+ s.agent as agent,
19650
+ COUNT(*) as session_count,
19651
+ SUM(CASE WHEN ss.request IS NOT NULL OR ss.completed IS NOT NULL THEN 1 ELSE 0 END) as summary_session_count,
19652
+ SUM(COALESCE(pc.prompt_count, 0)) as prompt_count,
19653
+ SUM(COALESCE(tc.tool_event_count, 0)) as tool_event_count,
19654
+ MAX(COALESCE(s.completed_at_epoch, s.started_at_epoch)) as last_seen_epoch
19655
+ FROM sessions s
19656
+ LEFT JOIN session_summaries ss ON ss.session_id = s.session_id
19657
+ LEFT JOIN (
19658
+ SELECT session_id, COUNT(*) as prompt_count
19659
+ FROM user_prompts
19660
+ GROUP BY session_id
19661
+ ) pc ON pc.session_id = s.session_id
19662
+ LEFT JOIN (
19663
+ SELECT session_id, COUNT(*) as tool_event_count
19664
+ FROM tool_events
19665
+ GROUP BY session_id
19666
+ ) tc ON tc.session_id = s.session_id
19667
+ WHERE 1 = 1${projectFilter}${userFilter}
19668
+ GROUP BY s.agent
19669
+ ORDER BY last_seen_epoch DESC, s.agent ASC`).all(...projectArgs, ...userArgs).filter((row) => !isInternalAgent(row.agent));
19670
+ const observationCounts = new Map;
19671
+ const observationRows = db.db.query(`SELECT
19672
+ COALESCE(s.agent, o.agent) as agent,
19673
+ COUNT(*) as observation_count,
19674
+ SUM(CASE WHEN o.type = 'message' AND o.concepts LIKE '%session-handoff%' THEN 1 ELSE 0 END) as handoff_count
19675
+ FROM observations o
19676
+ LEFT JOIN sessions s ON s.session_id = o.session_id
19677
+ WHERE o.lifecycle IN ('active', 'aging', 'pinned')
19678
+ AND o.superseded_by IS NULL
19679
+ ${projectId !== null ? "AND o.project_id = ?" : ""}
19680
+ ${input.user_id ? "AND (o.sensitivity != 'personal' OR o.user_id = ?)" : ""}
19681
+ GROUP BY COALESCE(s.agent, o.agent)`).all(...projectId !== null ? [projectId] : [], ...input.user_id ? [input.user_id] : []).filter((row) => !isInternalAgent(row.agent));
19682
+ for (const row of observationRows) {
19683
+ observationCounts.set(row.agent, {
19684
+ observation_count: row.observation_count,
19685
+ handoff_count: row.handoff_count
19686
+ });
19687
+ }
19688
+ const chatCoverage = new Map;
19689
+ const chatRows = db.db.query(`SELECT
19690
+ COALESCE(s.agent, cm.agent) as agent,
19691
+ CASE
19692
+ WHEN cm.source_kind = 'transcript' THEN 'transcript'
19693
+ WHEN cm.remote_source_id LIKE 'history:%' THEN 'history'
19694
+ ELSE 'hook'
19695
+ END as origin_kind,
19696
+ COUNT(*) as count
19697
+ FROM chat_messages cm
19698
+ LEFT JOIN sessions s ON s.session_id = cm.session_id
19699
+ WHERE 1 = 1
19700
+ ${projectId !== null ? "AND cm.project_id = ?" : ""}
19701
+ ${input.user_id ? "AND cm.user_id = ?" : ""}
19702
+ GROUP BY COALESCE(s.agent, cm.agent), origin_kind`).all(...projectId !== null ? [projectId] : [], ...input.user_id ? [input.user_id] : []).filter((row) => !isInternalAgent(row.agent));
19703
+ for (const row of chatRows) {
19704
+ const current = chatCoverage.get(row.agent) ?? {
19705
+ chat_message_count: 0,
19706
+ transcript_count: 0,
19707
+ history_count: 0,
19708
+ hook_count: 0
19709
+ };
19710
+ current.chat_message_count += row.count;
19711
+ if (row.origin_kind === "transcript")
19712
+ current.transcript_count += row.count;
19713
+ else if (row.origin_kind === "history")
19714
+ current.history_count += row.count;
19715
+ else
19716
+ current.hook_count += row.count;
19717
+ chatCoverage.set(row.agent, current);
19718
+ }
19719
+ const recentSessions = db.getRecentSessions(projectId, 200, input.user_id).filter((session) => !isInternalAgent(session.agent));
19720
+ const recallItems = listRecallItems(db, {
19721
+ cwd,
19722
+ project_scoped: projectScoped,
19723
+ user_id: input.user_id,
19724
+ limit: 30
19725
+ }).items;
19726
+ const latestByAgent = new Map;
19727
+ const devicesByAgent = new Map;
19728
+ for (const session of recentSessions) {
19729
+ if (!latestByAgent.has(session.agent))
19730
+ latestByAgent.set(session.agent, session);
19731
+ const devices = devicesByAgent.get(session.agent) ?? new Set;
19732
+ if (session.device_id)
19733
+ devices.add(session.device_id);
19734
+ devicesByAgent.set(session.agent, devices);
19735
+ }
19736
+ const knownAgents = new Set([
19737
+ ...sessionRows.map((row) => row.agent),
19738
+ ...Array.from(observationCounts.keys()),
19739
+ ...Array.from(chatCoverage.keys())
19740
+ ]);
19741
+ const agents = Array.from(knownAgents).map((agent) => {
19742
+ const session = sessionRows.find((row) => row.agent === agent) ?? {
19743
+ agent,
19744
+ session_count: 0,
19745
+ summary_session_count: 0,
19746
+ prompt_count: 0,
19747
+ tool_event_count: 0,
19748
+ last_seen_epoch: null
19749
+ };
19750
+ const obs = observationCounts.get(agent) ?? { observation_count: 0, handoff_count: 0 };
19751
+ const chat = chatCoverage.get(agent) ?? {
19752
+ chat_message_count: 0,
19753
+ transcript_count: 0,
19754
+ history_count: 0,
19755
+ hook_count: 0
19756
+ };
19757
+ const latestSession = latestByAgent.get(agent) ?? null;
19758
+ const bestRecall = pickBestRecallForAgent(recallItems, agent);
19759
+ return {
19760
+ agent,
19761
+ session_count: session.session_count,
19762
+ summary_session_count: session.summary_session_count,
19763
+ prompt_count: session.prompt_count,
19764
+ tool_event_count: session.tool_event_count,
19765
+ observation_count: obs.observation_count,
19766
+ handoff_count: obs.handoff_count,
19767
+ chat_message_count: chat.chat_message_count,
19768
+ chat_coverage_state: chat.transcript_count > 0 ? "transcript-backed" : chat.history_count > 0 ? "history-backed" : chat.hook_count > 0 ? "hook-only" : "none",
19769
+ continuity_state: classifyAgentContinuity(session.last_seen_epoch, session.prompt_count, session.tool_event_count, chat.chat_message_count, obs.handoff_count, obs.observation_count),
19770
+ capture_state: classifyAgentCaptureState(session.prompt_count, session.tool_event_count, session.summary_session_count, obs.observation_count, chat.chat_message_count),
19771
+ last_seen_epoch: session.last_seen_epoch,
19772
+ latest_session_id: latestSession?.session_id ?? null,
19773
+ latest_summary: latestSession?.current_thread ?? latestSession?.request ?? latestSession?.completed ?? null,
19774
+ devices: Array.from(devicesByAgent.get(agent) ?? []).sort(),
19775
+ best_recall_key: bestRecall?.key ?? null,
19776
+ best_recall_title: bestRecall?.title ?? null,
19777
+ best_recall_kind: bestRecall?.kind ?? null,
19778
+ resume_freshness: bestRecall?.freshness ?? "stale"
19779
+ };
19780
+ }).sort((a, b) => {
19781
+ const epochA = a.last_seen_epoch ?? 0;
19782
+ const epochB = b.last_seen_epoch ?? 0;
19783
+ return epochB - epochA || a.agent.localeCompare(b.agent);
19784
+ });
19785
+ return {
19786
+ project: projectName,
19787
+ agents,
19788
+ suggested_tools: buildSuggestedTools2(agents)
19789
+ };
19790
+ }
19791
+ function isInternalAgent(agent) {
19792
+ return !agent || agent.startsWith("engrm-");
19793
+ }
19794
+ function classifyAgentContinuity(lastSeenEpoch, promptCount, toolCount, chatCount, handoffCount, observationCount) {
19795
+ if (!lastSeenEpoch)
19796
+ return "cold";
19797
+ const ageMs = Date.now() - lastSeenEpoch * 1000;
19798
+ const hasStrongContinuity = promptCount > 0 || toolCount > 0 || chatCount > 0 || handoffCount > 0;
19799
+ if (ageMs <= 3 * 24 * 60 * 60 * 1000 && hasStrongContinuity)
19800
+ return "fresh";
19801
+ if (observationCount > 0 || promptCount > 0 || toolCount > 0 || chatCount > 0)
19802
+ return "thin";
19803
+ return "cold";
19804
+ }
19805
+ function classifyAgentCaptureState(promptCount, toolCount, summarySessionCount, observationCount, chatCount) {
19806
+ if (promptCount > 0 && toolCount > 0)
19807
+ return "rich";
19808
+ if (promptCount > 0 || toolCount > 0)
19809
+ return "partial";
19810
+ if (summarySessionCount > 0 || observationCount > 0 || chatCount > 0)
19811
+ return "summary-only";
19812
+ return "legacy";
19813
+ }
19814
+ function buildSuggestedTools2(agents) {
19815
+ if (agents.length === 0)
19816
+ return [];
19817
+ const suggestions = ["recent_sessions", "capture_quality"];
19818
+ if (agents.length > 1) {
19819
+ suggestions.push("list_recall_items");
19820
+ }
19821
+ if (agents.some((agent) => agent.best_recall_key)) {
19822
+ suggestions.push("load_recall_item");
19823
+ }
19824
+ if (agents.some((agent) => agent.continuity_state !== "fresh")) {
19825
+ suggestions.push("resume_thread");
19826
+ }
19827
+ if (agents.some((agent) => agent.chat_coverage_state === "hook-only")) {
19828
+ suggestions.push("repair_recall");
19829
+ }
19830
+ return suggestions;
19831
+ }
19832
+ function pickBestRecallForAgent(items, agent) {
19833
+ return items.find((item) => item.source_agent === agent) ?? null;
19834
+ }
19835
+
19532
19836
  // src/tools/tool-memory-index.ts
19533
- function parseConcepts(value) {
19837
+ function parseConcepts2(value) {
19534
19838
  if (!value)
19535
19839
  return [];
19536
19840
  try {
@@ -19599,7 +19903,7 @@ function getToolMemoryIndex(db, input = {}) {
19599
19903
  ORDER BY o.created_at_epoch DESC, o.id DESC
19600
19904
  LIMIT 50`).all(...rowParams);
19601
19905
  const topPlugins = Array.from(observationRows.reduce((acc, obs) => {
19602
- for (const concept of parseConcepts(obs.concepts)) {
19906
+ for (const concept of parseConcepts2(obs.concepts)) {
19603
19907
  if (!concept.startsWith("plugin:"))
19604
19908
  continue;
19605
19909
  const plugin = concept.slice("plugin:".length);
@@ -19628,7 +19932,7 @@ function getToolMemoryIndex(db, input = {}) {
19628
19932
  }
19629
19933
 
19630
19934
  // src/tools/session-tool-memory.ts
19631
- function parseConcepts2(value) {
19935
+ function parseConcepts3(value) {
19632
19936
  if (!value)
19633
19937
  return [];
19634
19938
  try {
@@ -19660,7 +19964,7 @@ function getSessionToolMemory(db, input) {
19660
19964
  }, new Map).entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
19661
19965
  const sampleTitles = groupedObservations.map((obs) => obs.title).filter((title, index, all) => all.indexOf(title) === index).slice(0, 4);
19662
19966
  const topPlugins = Array.from(groupedObservations.reduce((acc, obs) => {
19663
- for (const concept of parseConcepts2(obs.concepts)) {
19967
+ for (const concept of parseConcepts3(obs.concepts)) {
19664
19968
  if (!concept.startsWith("plugin:"))
19665
19969
  continue;
19666
19970
  const plugin = concept.slice("plugin:".length);
@@ -19716,6 +20020,13 @@ function getSessionContext(db, input) {
19716
20020
  user_id: input.user_id,
19717
20021
  limit: 8
19718
20022
  });
20023
+ const recentActivity = getRecentActivity(db, {
20024
+ cwd,
20025
+ project_scoped: true,
20026
+ user_id: input.user_id,
20027
+ limit: 12
20028
+ });
20029
+ const recentInboxNotes = recentActivity.observations.filter((obs) => obs.message_kind === "inbox-note").slice(0, 5);
19719
20030
  const recentChatMessages = recentChat.messages.length;
19720
20031
  const recallIndex = listRecallItems(db, {
19721
20032
  cwd,
@@ -19732,9 +20043,12 @@ function getSessionContext(db, input) {
19732
20043
  const latestChatEpoch = recentChat.messages.length > 0 ? recentChat.messages[recentChat.messages.length - 1]?.created_at_epoch ?? null : null;
19733
20044
  const resumeTimestamp = latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
19734
20045
  const bestRecallItem = recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null;
20046
+ const activeAgents = collectActiveAgents(context.recentSessions ?? []);
19735
20047
  return {
19736
20048
  project_name: context.project_name,
19737
20049
  canonical_id: context.canonical_id,
20050
+ active_agents: activeAgents,
20051
+ cross_agent_active: activeAgents.length > 1,
19738
20052
  continuity_state: continuityState,
19739
20053
  continuity_summary: describeContinuityState(continuityState),
19740
20054
  recall_mode: recallIndex.continuity_mode,
@@ -19743,11 +20057,13 @@ function getSessionContext(db, input) {
19743
20057
  key: item.key,
19744
20058
  kind: item.kind,
19745
20059
  freshness: item.freshness,
19746
- title: item.title
20060
+ title: item.title,
20061
+ source_agent: item.source_agent
19747
20062
  })),
19748
20063
  best_recall_key: bestRecallItem?.key ?? null,
19749
20064
  best_recall_title: bestRecallItem?.title ?? null,
19750
20065
  best_recall_kind: bestRecallItem?.kind ?? null,
20066
+ best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
19751
20067
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
19752
20068
  resume_source_session_id: latestSession?.session_id ?? null,
19753
20069
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -19761,6 +20077,8 @@ function getSessionContext(db, input) {
19761
20077
  rolling_handoff_drafts: rollingHandoffDrafts,
19762
20078
  saved_handoffs: savedHandoffs,
19763
20079
  latest_handoff_title: latestHandoffTitle,
20080
+ recent_inbox_notes: recentInboxNotes.length,
20081
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
19764
20082
  recent_chat_messages: recentChatMessages,
19765
20083
  recent_chat_sessions: recentChat.session_count,
19766
20084
  chat_source_summary: recentChat.source_summary,
@@ -19770,7 +20088,7 @@ function getSessionContext(db, input) {
19770
20088
  capture_state: captureState,
19771
20089
  raw_capture_active: recentRequests > 0 || recentTools > 0,
19772
20090
  estimated_read_tokens: estimateTokens(preview),
19773
- suggested_tools: buildSuggestedTools2(context, recentChat.coverage_state),
20091
+ suggested_tools: buildSuggestedTools3(context, recentChat.coverage_state, activeAgents.length),
19774
20092
  preview
19775
20093
  };
19776
20094
  }
@@ -19801,11 +20119,14 @@ function parseJsonArray3(value) {
19801
20119
  return [];
19802
20120
  }
19803
20121
  }
19804
- function buildSuggestedTools2(context, chatCoverageState) {
20122
+ function buildSuggestedTools3(context, chatCoverageState, activeAgentCount) {
19805
20123
  const tools = [];
19806
20124
  if ((context.recentSessions?.length ?? 0) > 0) {
19807
20125
  tools.push("recent_sessions");
19808
20126
  }
20127
+ if (activeAgentCount > 1) {
20128
+ tools.push("agent_memory_index");
20129
+ }
19809
20130
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
19810
20131
  tools.push("activity_feed");
19811
20132
  }
@@ -19833,7 +20154,7 @@ function buildSuggestedTools2(context, chatCoverageState) {
19833
20154
  if ((context.recentChatMessages?.length ?? 0) > 0) {
19834
20155
  tools.push("recent_chat", "search_chat");
19835
20156
  }
19836
- return Array.from(new Set(tools)).slice(0, 5);
20157
+ return Array.from(new Set(tools)).slice(0, 6);
19837
20158
  }
19838
20159
 
19839
20160
  // src/tools/load-recall-item.ts
@@ -19847,6 +20168,7 @@ function loadRecallItem(db, input) {
19847
20168
  detail: "Malformed recall key",
19848
20169
  session_id: null,
19849
20170
  source_device_id: null,
20171
+ source_agent: null,
19850
20172
  payload: null
19851
20173
  };
19852
20174
  }
@@ -19868,6 +20190,7 @@ function loadRecallItem(db, input) {
19868
20190
  detail: summarizeNarrative(result.handoff.narrative),
19869
20191
  session_id: result.handoff.session_id ?? null,
19870
20192
  source_device_id: result.handoff.device_id ?? null,
20193
+ source_agent: result.handoff.session_id ? lookupSessionAgent(db, result.handoff.session_id) : null,
19871
20194
  payload: {
19872
20195
  type: "handoff",
19873
20196
  handoff_id: result.handoff.id,
@@ -19887,6 +20210,7 @@ function loadRecallItem(db, input) {
19887
20210
  detail: story.summary?.next_steps ?? story.summary?.completed ?? null,
19888
20211
  session_id: story.session.session_id,
19889
20212
  source_device_id: story.session.device_id ?? null,
20213
+ source_agent: story.session.agent ?? null,
19890
20214
  payload: {
19891
20215
  type: "thread",
19892
20216
  latest_request: story.latest_request,
@@ -19916,6 +20240,7 @@ function loadRecallItem(db, input) {
19916
20240
  detail: message.content,
19917
20241
  session_id: message.session_id,
19918
20242
  source_device_id: message.device_id ?? null,
20243
+ source_agent: message.agent ?? null,
19919
20244
  payload: {
19920
20245
  type: "chat",
19921
20246
  role: message.role,
@@ -19941,6 +20266,7 @@ function loadRecallItem(db, input) {
19941
20266
  detail: obs.narrative ?? obs.facts ?? null,
19942
20267
  session_id: obs.session_id ?? null,
19943
20268
  source_device_id: obs.device_id ?? null,
20269
+ source_agent: obs.session_id ? lookupSessionAgent(db, obs.session_id) : obs.agent?.startsWith("engrm-") ? null : obs.agent ?? null,
19944
20270
  payload: {
19945
20271
  type: "memory",
19946
20272
  observation_id: obs.id,
@@ -19957,6 +20283,10 @@ function summarizeNarrative(value) {
19957
20283
  return null;
19958
20284
  return value.split(/\n+/).map((line) => line.trim()).find(Boolean) ?? null;
19959
20285
  }
20286
+ function lookupSessionAgent(db, sessionId) {
20287
+ const row = db.db.query("SELECT agent FROM sessions WHERE session_id = ? LIMIT 1").get(sessionId);
20288
+ return row?.agent ?? null;
20289
+ }
19960
20290
  function missing(key, kind) {
19961
20291
  return {
19962
20292
  kind,
@@ -19965,6 +20295,7 @@ function missing(key, kind) {
19965
20295
  detail: "Recall item not found",
19966
20296
  session_id: null,
19967
20297
  source_device_id: null,
20298
+ source_agent: null,
19968
20299
  payload: null
19969
20300
  };
19970
20301
  }
@@ -20391,6 +20722,9 @@ async function resumeThread(db, config2, input = {}) {
20391
20722
  const detected = detectProject(cwd);
20392
20723
  const project = db.getProjectByCanonicalId(detected.canonical_id);
20393
20724
  let snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20725
+ if (input.agent) {
20726
+ snapshot = filterResumeSnapshotByAgent(snapshot, input.agent, input.current_device_id);
20727
+ }
20394
20728
  let repairResult = null;
20395
20729
  const shouldRepair = repairIfNeeded && snapshot.recentChat.coverage_state !== "transcript-backed" && (snapshot.recentChat.messages.length > 0 || snapshot.recentSessions.length > 0 || snapshot.context?.continuity_state !== "cold");
20396
20730
  if (shouldRepair) {
@@ -20401,6 +20735,9 @@ async function resumeThread(db, config2, input = {}) {
20401
20735
  });
20402
20736
  if (repairResult.imported_chat_messages > 0) {
20403
20737
  snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20738
+ if (input.agent) {
20739
+ snapshot = filterResumeSnapshotByAgent(snapshot, input.agent, input.current_device_id);
20740
+ }
20404
20741
  }
20405
20742
  }
20406
20743
  const { context, handoff, recentChat, recentSessions, recall } = snapshot;
@@ -20441,6 +20778,7 @@ async function resumeThread(db, config2, input = {}) {
20441
20778
  ])).slice(0, 4);
20442
20779
  return {
20443
20780
  project_name: project?.name ?? context?.project_name ?? null,
20781
+ target_agent: input.agent ?? null,
20444
20782
  continuity_state: context?.continuity_state ?? "cold",
20445
20783
  continuity_summary: context?.continuity_summary ?? "No fresh repo-local continuity yet; older memory should be treated cautiously.",
20446
20784
  resume_freshness: classifyResumeFreshness2(sourceTimestamp),
@@ -20490,6 +20828,13 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20490
20828
  user_id: userId,
20491
20829
  current_device_id: currentDeviceId
20492
20830
  });
20831
+ const recentHandoffs = getRecentHandoffs(db, {
20832
+ cwd,
20833
+ project_scoped: true,
20834
+ user_id: userId,
20835
+ current_device_id: currentDeviceId,
20836
+ limit: Math.max(limit, 4)
20837
+ }).handoffs;
20493
20838
  const recentChat = getRecentChat(db, {
20494
20839
  cwd,
20495
20840
  project_scoped: true,
@@ -20519,15 +20864,50 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20519
20864
  return {
20520
20865
  context,
20521
20866
  handoff: handoffResult.handoff,
20867
+ recentHandoffs,
20522
20868
  recentChat,
20523
20869
  recentSessions,
20524
20870
  recall,
20525
20871
  recallIndex
20526
20872
  };
20527
20873
  }
20874
+ function filterResumeSnapshotByAgent(snapshot, agent, currentDeviceId) {
20875
+ const recentSessions = snapshot.recentSessions.filter((session) => session.agent === agent);
20876
+ const sessionIds = new Set(recentSessions.map((session) => session.session_id));
20877
+ const recentChatMessages = snapshot.recentChat.messages.filter((message) => message.agent === agent);
20878
+ const handoff = snapshot.recentHandoffs.filter((item) => item.session_id && sessionIds.has(item.session_id)).sort((a, b) => compareRecallCandidates(a.created_at_epoch, b.created_at_epoch, a.device_id ?? null, b.device_id ?? null, currentDeviceId))[0] ?? null;
20879
+ const recallIndex = {
20880
+ ...snapshot.recallIndex,
20881
+ items: snapshot.recallIndex.items.filter((item) => item.source_agent === agent)
20882
+ };
20883
+ const recall = {
20884
+ ...snapshot.recall,
20885
+ results: snapshot.recall.results.filter((entry) => entry.session_id ? sessionIds.has(entry.session_id) : false)
20886
+ };
20887
+ return {
20888
+ ...snapshot,
20889
+ handoff,
20890
+ recentHandoffs: snapshot.recentHandoffs.filter((item) => item.session_id && sessionIds.has(item.session_id)),
20891
+ recentSessions,
20892
+ recentChat: {
20893
+ ...snapshot.recentChat,
20894
+ messages: recentChatMessages,
20895
+ session_count: new Set(recentChatMessages.map((message) => message.session_id)).size
20896
+ },
20897
+ recall,
20898
+ recallIndex
20899
+ };
20900
+ }
20528
20901
  function pickBestRecallItem2(items) {
20529
20902
  return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
20530
20903
  }
20904
+ function compareRecallCandidates(epochA, epochB, deviceA, deviceB, currentDeviceId) {
20905
+ const remoteBoostA = currentDeviceId && deviceA && deviceA !== currentDeviceId ? 1 : 0;
20906
+ const remoteBoostB = currentDeviceId && deviceB && deviceB !== currentDeviceId ? 1 : 0;
20907
+ if (remoteBoostA !== remoteBoostB)
20908
+ return remoteBoostB - remoteBoostA;
20909
+ return epochB - epochA;
20910
+ }
20531
20911
  function extractCurrentThread(handoff) {
20532
20912
  const narrative = handoff?.narrative ?? "";
20533
20913
  const match = narrative.match(/Current thread:\s*(.+)/i);
@@ -20770,8 +21150,9 @@ function hasContent(value) {
20770
21150
  // src/tools/stats.ts
20771
21151
  function getMemoryStats(db) {
20772
21152
  const activeObservations = db.getActiveObservationCount();
20773
- const messages = db.db.query(`SELECT COUNT(*) as count FROM observations
20774
- WHERE type = 'message' AND lifecycle IN ('active', 'aging', 'pinned')`).get()?.count ?? 0;
21153
+ const handoffs = db.db.query(`SELECT COUNT(*) as count FROM observations
21154
+ WHERE ${getHandoffMessageFilterSql({ include_aging: true })}`).get()?.count ?? 0;
21155
+ const inboxMessages = getInboxMessageCount(db);
20775
21156
  const userPrompts = db.db.query("SELECT COUNT(*) as count FROM user_prompts").get()?.count ?? 0;
20776
21157
  const toolEvents = db.db.query("SELECT COUNT(*) as count FROM tool_events").get()?.count ?? 0;
20777
21158
  const sessionSummaries = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
@@ -20785,7 +21166,9 @@ function getMemoryStats(db) {
20785
21166
  active_observations: activeObservations,
20786
21167
  user_prompts: userPrompts,
20787
21168
  tool_events: toolEvents,
20788
- messages,
21169
+ messages: inboxMessages,
21170
+ inbox_messages: inboxMessages,
21171
+ handoffs,
20789
21172
  session_summaries: sessionSummaries,
20790
21173
  decisions: signals.decisions_count,
20791
21174
  lessons: signals.lessons_count,
@@ -22459,7 +22842,7 @@ process.on("SIGTERM", () => {
22459
22842
  });
22460
22843
  var server = new McpServer({
22461
22844
  name: "engrm",
22462
- version: "0.4.33"
22845
+ version: "0.4.35"
22463
22846
  });
22464
22847
  server.tool("save_observation", "Save an observation to memory", {
22465
22848
  type: exports_external.enum([
@@ -22929,7 +23312,7 @@ server.tool("list_recall_items", "USE FIRST when continuity feels fuzzy. List th
22929
23312
  }
22930
23313
  const projectLine = result.project ? `Project: ${result.project}
22931
23314
  ` : "";
22932
- const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
23315
+ const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
22933
23316
  ${item.detail}`).join(`
22934
23317
  `);
22935
23318
  return {
@@ -22974,6 +23357,7 @@ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact rec
22974
23357
  ` + `Title: ${result.title}
22975
23358
  ` + `Session: ${result.session_id ?? "(unknown)"}
22976
23359
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23360
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
22977
23361
 
22978
23362
  ` + `${result.payload.narrative ?? "(no narrative)"}`
22979
23363
  }
@@ -22993,6 +23377,7 @@ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact rec
22993
23377
  ` + `Title: ${result.title}
22994
23378
  ` + `Session: ${result.session_id ?? "(unknown)"}
22995
23379
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23380
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
22996
23381
  ` + `Latest request: ${result.payload.latest_request ?? "(none)"}
22997
23382
  ` + `Current thread: ${result.payload.current_thread ?? "(none)"}
22998
23383
 
@@ -23014,6 +23399,7 @@ ${hotFiles}`
23014
23399
  ` + `Title: ${result.title}
23015
23400
  ` + `Session: ${result.session_id ?? "(unknown)"}
23016
23401
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23402
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23017
23403
 
23018
23404
  ` + `${result.payload.content}`
23019
23405
  }
@@ -23028,6 +23414,7 @@ ${hotFiles}`
23028
23414
  ` + `Title: ${result.title}
23029
23415
  ` + `Session: ${result.session_id ?? "(unknown)"}
23030
23416
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23417
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23031
23418
  ` + `Type: ${result.payload.observation_type}
23032
23419
 
23033
23420
  ` + `${result.payload.narrative ?? result.payload.facts ?? "(no detail)"}`
@@ -23038,12 +23425,14 @@ ${hotFiles}`
23038
23425
  server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?' answer. Build a clear resume point for the current project by combining handoff, live recall, current thread, and recent chat continuity.", {
23039
23426
  cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
23040
23427
  limit: exports_external.number().optional().describe("Max recall hits/chat snippets to include"),
23428
+ agent: exports_external.string().optional().describe("Optional agent to resume specifically, such as claude-code or codex-cli"),
23041
23429
  user_id: exports_external.string().optional().describe("Optional user override"),
23042
23430
  repair_if_needed: exports_external.boolean().optional().describe("If true, attempt recall repair before resuming when continuity is still weak")
23043
23431
  }, async (params) => {
23044
23432
  const result = await resumeThread(db, config2, {
23045
23433
  cwd: params.cwd ?? process.cwd(),
23046
23434
  limit: params.limit,
23435
+ agent: params.agent,
23047
23436
  user_id: params.user_id ?? config2.user_id,
23048
23437
  current_device_id: config2.device_id,
23049
23438
  repair_if_needed: params.repair_if_needed
@@ -23085,7 +23474,8 @@ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?
23085
23474
  content: [
23086
23475
  {
23087
23476
  type: "text",
23088
- text: `${projectLine}` + `Continuity: ${result.continuity_state} ${result.continuity_summary}
23477
+ text: `${projectLine}` + `${result.target_agent ? `Target agent: ${result.target_agent}
23478
+ ` : ""}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23089
23479
  ` + `Freshness: ${result.resume_freshness}
23090
23480
  ` + `Source: ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23091
23481
  ` + `Resume confidence: ${result.resume_confidence}
@@ -23244,13 +23634,7 @@ server.tool("check_messages", "Check for messages sent from other devices or ses
23244
23634
  const markRead = params.mark_read !== false;
23245
23635
  const readKey = `messages_read_${config2.device_id}`;
23246
23636
  const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
23247
- const messages = db.db.query(`SELECT id, title, narrative, user_id, device_id, created_at FROM observations
23248
- WHERE type = 'message'
23249
- AND id > ?
23250
- AND lifecycle IN ('active', 'pinned')
23251
- AND device_id != ?
23252
- AND (sensitivity != 'personal' OR user_id = ?)
23253
- ORDER BY created_at_epoch DESC LIMIT 20`).all(lastReadId, config2.device_id, config2.user_id);
23637
+ const messages = getUnreadInboxMessages(db, config2.device_id, config2.user_id, lastReadId, 20);
23254
23638
  if (messages.length === 0) {
23255
23639
  return {
23256
23640
  content: [{ type: "text", text: "No new messages." }]
@@ -23297,7 +23681,7 @@ server.tool("send_message", "Leave a cross-device or team note in Engrm's shared
23297
23681
  ]
23298
23682
  };
23299
23683
  });
23300
- server.tool("recent_activity", "Inspect the most recent observations captured by Engrm", {
23684
+ server.tool("recent_activity", "Inspect the most recent observations, notes, and handoffs captured by Engrm", {
23301
23685
  limit: exports_external.number().optional().describe("Max observations to return (default: 10)"),
23302
23686
  project_scoped: exports_external.boolean().optional().describe("Scope to current project (default: true)"),
23303
23687
  type: exports_external.string().optional().describe("Optional observation type filter")
@@ -23320,12 +23704,21 @@ server.tool("recent_activity", "Inspect the most recent observations captured by
23320
23704
  const showProject = !result.project;
23321
23705
  const header = showProject ? "| ID | Project | Type | Title | Created |" : "| ID | Type | Title | Created |";
23322
23706
  const separator = showProject ? "|---|---|---|---|---|" : "|---|---|---|---|";
23707
+ const displayType = (obs) => {
23708
+ if (obs.message_kind === "draft-handoff")
23709
+ return "handoff:draft";
23710
+ if (obs.message_kind === "handoff")
23711
+ return "handoff";
23712
+ if (obs.message_kind === "inbox-note")
23713
+ return "note";
23714
+ return obs.type;
23715
+ };
23323
23716
  const rows = result.observations.map((obs) => {
23324
23717
  const date5 = obs.created_at.split("T")[0];
23325
23718
  if (showProject) {
23326
- return `| ${obs.id} | ${obs.project_name ?? "(unknown)"} | ${obs.type} | ${obs.title} | ${date5} |`;
23719
+ return `| ${obs.id} | ${obs.project_name ?? "(unknown)"} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23327
23720
  }
23328
- return `| ${obs.id} | ${obs.type} | ${obs.title} | ${date5} |`;
23721
+ return `| ${obs.id} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23329
23722
  });
23330
23723
  const projectLine = result.project ? `Project: ${result.project}
23331
23724
  ` : "";
@@ -23361,7 +23754,8 @@ server.tool("memory_stats", "Show high-level Engrm capture and sync statistics",
23361
23754
  text: `Active observations: ${stats.active_observations}
23362
23755
  ` + `User prompts: ${stats.user_prompts}
23363
23756
  ` + `Tool events: ${stats.tool_events}
23364
- ` + `Messages: ${stats.messages}
23757
+ ` + `Inbox notes: ${stats.inbox_messages}
23758
+ ` + `Handoffs: ${stats.handoffs}
23365
23759
  ` + `Session summaries: ${stats.session_summaries}
23366
23760
  ` + `Summary coverage: learned ${stats.summaries_with_learned}, completed ${stats.summaries_with_completed}, next steps ${stats.summaries_with_next_steps}
23367
23761
  ` + `Installed packs: ${packs}
@@ -23405,6 +23799,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23405
23799
  }).join(`
23406
23800
  `) : "- (none)";
23407
23801
  const handoffLines = result.recent_handoffs.length > 0 ? result.recent_handoffs.map((obs) => `- #${obs.id} ${obs.title}`).join(`
23802
+ `) : "- (none)";
23803
+ const inboxNoteLines = result.recent_inbox_notes.length > 0 ? result.recent_inbox_notes.map((obs) => `- #${obs.id} ${obs.title}`).join(`
23408
23804
  `) : "- (none)";
23409
23805
  const recentChatLines = result.recent_chat.length > 0 ? result.recent_chat.map((msg) => `- [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 180)}`).join(`
23410
23806
  `) : "- (none)";
@@ -23425,7 +23821,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23425
23821
  `) : "- (none)";
23426
23822
  const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
23427
23823
  `) : "- (none)";
23428
- const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
23824
+ const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}`).join(`
23429
23825
  `) : "- (none)";
23430
23826
  const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23431
23827
  ` : "";
@@ -23441,16 +23837,18 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23441
23837
  content: [
23442
23838
  {
23443
23839
  type: "text",
23444
- text: `${projectLine}` + `${captureLine}` + `Continuity: ${result.continuity_state} ${result.continuity_summary}
23840
+ text: `${projectLine}` + `${captureLine}` + `${result.active_agents.length > 0 ? `Agents active: ${result.active_agents.join(", ")}
23841
+ ` : ""}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23445
23842
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23446
23843
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23447
23844
  ` + `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})
23448
23845
  ` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
23449
23846
  ` : ""}` + `Handoffs: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
23847
+ ` + `Inbox notes: ${result.recent_inbox_notes.length}${result.latest_inbox_note_title ? ` · latest "${result.latest_inbox_note_title}"` : ""}
23450
23848
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
23451
23849
  ` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23452
23850
 
23453
- ` + openExactLine + `Recall preview:
23851
+ ` + openExactLine + resumeAgentLine + `Recall preview:
23454
23852
  ${recallPreviewLines}
23455
23853
 
23456
23854
  ` + `Next actions:
@@ -23472,6 +23870,9 @@ ${sessionLines}
23472
23870
  ` + `Recent handoffs:
23473
23871
  ${handoffLines}
23474
23872
 
23873
+ ` + `Recent inbox notes:
23874
+ ${inboxNoteLines}
23875
+
23475
23876
  ` + `Recent requests:
23476
23877
  ${requestLines}
23477
23878
 
@@ -23564,6 +23965,38 @@ ${projectLines}`
23564
23965
  ]
23565
23966
  };
23566
23967
  });
23968
+ server.tool("agent_memory_index", "Compare continuity and capture health across Claude Code, Codex, OpenClaw, and other agents for the current project or workspace.", {
23969
+ cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
23970
+ project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
23971
+ user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
23972
+ }, async (params) => {
23973
+ const result = getAgentMemoryIndex(db, {
23974
+ cwd: params.cwd ?? process.cwd(),
23975
+ project_scoped: params.project_scoped,
23976
+ user_id: params.user_id ?? config2.user_id
23977
+ });
23978
+ const rows = result.agents.length > 0 ? result.agents.map((agent) => {
23979
+ const lastSeen = agent.last_seen_epoch ? new Date(agent.last_seen_epoch * 1000).toISOString().replace("T", " ").slice(0, 16) : "unknown";
23980
+ const latest = agent.latest_summary ? ` latest="${agent.latest_summary.replace(/\s+/g, " ").trim().slice(0, 120)}"` : "";
23981
+ const devices = agent.devices.length > 0 ? ` devices=[${agent.devices.join(", ")}]` : "";
23982
+ const exact = agent.best_recall_key ? ` open=load_recall_item("${agent.best_recall_key}")` : "";
23983
+ return `- ${agent.agent}: continuity=${agent.continuity_state} capture=${agent.capture_state} resume=${agent.resume_freshness} 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}${exact} resume_call=resume_thread(agent="${agent.agent}")`;
23984
+ }).join(`
23985
+ `) : "- (none)";
23986
+ return {
23987
+ content: [
23988
+ {
23989
+ type: "text",
23990
+ text: `${result.project ? `Project: ${result.project}
23991
+
23992
+ ` : ""}` + `Agent memory index:
23993
+ ${rows}
23994
+
23995
+ ` + `Suggested next step: ${result.suggested_tools.join(", ") || "(none)"}`
23996
+ }
23997
+ ]
23998
+ };
23999
+ });
23567
24000
  server.tool("tool_memory_index", "Show which tools are actually producing durable memory, which plugins they exercise, and what memory types they create.", {
23568
24001
  cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
23569
24002
  project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
@@ -23649,10 +24082,12 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23649
24082
  type: "text",
23650
24083
  text: `Project: ${result.project_name}
23651
24084
  ` + `Canonical ID: ${result.canonical_id}
24085
+ ` + `Agents active: ${result.active_agents.join(", ") || "(none)"}
23652
24086
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23653
24087
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23654
24088
  ` + `Open exact: ${result.best_recall_key ? `load_recall_item("${result.best_recall_key}")` : "(none)"}
23655
- ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
24089
+ ` + `${result.best_agent_resume_agent ? `Resume agent: resume_thread(agent="${result.best_agent_resume_agent}")
24090
+ ` : ""}` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23656
24091
  ` + `Loaded observations: ${result.session_count}
23657
24092
  ` + `Searchable total: ${result.total_active}
23658
24093
  ` + `Recent requests: ${result.recent_requests}
@@ -23660,6 +24095,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23660
24095
  ` + `Recent sessions: ${result.recent_sessions}
23661
24096
  ` + `Recent handoffs: ${result.recent_handoffs}
23662
24097
  ` + `Handoff split: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
24098
+ ` + `Recent inbox notes: ${result.recent_inbox_notes}${result.latest_inbox_note_title ? ` · latest "${result.latest_inbox_note_title}"` : ""}
23663
24099
  ` + `Recent chat messages: ${result.recent_chat_messages}
23664
24100
  ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, history ${result.chat_source_summary.history}, hook ${result.chat_source_summary.hook})
23665
24101
  ` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
@@ -23730,7 +24166,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23730
24166
  `) : "- (none)";
23731
24167
  const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
23732
24168
  `) : "- (none)";
23733
- const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}`).join(`
24169
+ const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}`).join(`
23734
24170
  `) : "- (none)";
23735
24171
  const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23736
24172
  ` : "";
@@ -23740,6 +24176,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23740
24176
  type: "text",
23741
24177
  text: `Project: ${result.project}
23742
24178
  ` + `Canonical ID: ${result.canonical_id}
24179
+ ` + `Agents active: ${result.active_agents.join(", ") || "(none)"}
23743
24180
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23744
24181
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23745
24182
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
@@ -23748,6 +24185,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23748
24185
 
23749
24186
  ` + `Recent handoffs captured: ${result.recent_handoffs_count}
23750
24187
  ` + `Handoff split: ${result.saved_handoffs_count} saved, ${result.rolling_handoff_drafts_count} rolling drafts
24188
+ ` + `Recent inbox notes: ${result.recent_inbox_notes_count}${result.latest_inbox_note_title ? ` · latest "${result.latest_inbox_note_title}"` : ""}
23751
24189
  ` + `Recent chat messages captured: ${result.recent_chat_count}
23752
24190
  ` + `Chat recall: ${result.chat_coverage_state} · ${result.recent_chat_sessions} sessions (transcript ${result.chat_source_summary.transcript}, history ${result.chat_source_summary.history}, hook ${result.chat_source_summary.hook})
23753
24191
 
@@ -23757,7 +24195,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23757
24195
  ` + `Estimated read cost: ~${result.estimated_read_tokens}t
23758
24196
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23759
24197
 
23760
- ` + openExactLine + `Recall preview:
24198
+ ` + openExactLine + resumeAgentLine + `Recall preview:
23761
24199
  ${recallPreviewLines}
23762
24200
 
23763
24201
  ` + `Next actions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.33",
3
+ "version": "0.4.35",
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",