engrm 0.4.34 → 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
@@ -405,8 +405,12 @@ What each tool is good for:
405
405
  - `capture_quality` shows whether chat recall is transcript-backed, history-backed, or still hook-only across the workspace
406
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
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
408
410
  - `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
409
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
410
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
411
415
  - `load_recall_item` completes that protocol by letting agents open one exact recall key directly after listing
412
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.34";
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;
@@ -5888,10 +5976,12 @@ function formatInspectHints(context, visibleObservationIds = [], recallItems = [
5888
5976
  return [];
5889
5977
  const ids = visibleObservationIds.slice(0, 5);
5890
5978
  const openNowItem = recallItems.find((item) => item.kind !== "memory") ?? null;
5979
+ const resumeAgent = activeAgents.length > 1 ? context.recentSessions?.[0]?.agent ?? null : null;
5891
5980
  const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
5892
5981
  return [
5893
5982
  `${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
5894
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}")`] : [],
5895
5985
  ...fetchHint ? [`${c2.dim}Pull detail:${c2.reset} ${fetchHint}`] : []
5896
5986
  ];
5897
5987
  }
@@ -5904,7 +5994,7 @@ function formatStartupRecallPreview(recallItems) {
5904
5994
  return [];
5905
5995
  return [
5906
5996
  `${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)}`)
5997
+ ...items.map((item) => `${item.key} [${item.kind} \xB7 ${item.freshness}${item.source_agent ? ` \xB7 ${item.source_agent}` : ""}] ${truncateInline(item.title, 110)}`)
5908
5998
  ];
5909
5999
  }
5910
6000
  function buildStartupRecallItems(context) {
@@ -5919,6 +6009,7 @@ function buildStartupRecallItems(context) {
5919
6009
  kind: "handoff",
5920
6010
  freshness,
5921
6011
  title,
6012
+ source_agent: (context.recentSessions ?? []).find((session) => session.session_id === handoff.session_id)?.agent ?? null,
5922
6013
  score: freshnessScore(freshness) + 40
5923
6014
  });
5924
6015
  }
@@ -5933,6 +6024,7 @@ function buildStartupRecallItems(context) {
5933
6024
  kind: "thread",
5934
6025
  freshness,
5935
6026
  title,
6027
+ source_agent: session.agent ?? null,
5936
6028
  score: freshnessScore(freshness) + 30
5937
6029
  });
5938
6030
  }
@@ -5945,6 +6037,7 @@ function buildStartupRecallItems(context) {
5945
6037
  kind: "chat",
5946
6038
  freshness,
5947
6039
  title: `[${message.role}] ${message.content.replace(/\s+/g, " ").trim()}`,
6040
+ source_agent: message.agent ?? null,
5948
6041
  score: freshnessScore(freshness) + 20
5949
6042
  });
5950
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.34",
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,
@@ -18617,11 +18689,13 @@ function getProjectMemoryIndex(db, input) {
18617
18689
  key: item.key,
18618
18690
  kind: item.kind,
18619
18691
  freshness: item.freshness,
18620
- title: item.title
18692
+ title: item.title,
18693
+ source_agent: item.source_agent
18621
18694
  })),
18622
18695
  best_recall_key: bestRecallItem?.key ?? null,
18623
18696
  best_recall_title: bestRecallItem?.title ?? null,
18624
18697
  best_recall_kind: bestRecallItem?.kind ?? null,
18698
+ best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
18625
18699
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18626
18700
  resume_source_session_id: latestSession?.session_id ?? null,
18627
18701
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18634,6 +18708,8 @@ function getProjectMemoryIndex(db, input) {
18634
18708
  recent_handoffs_count: recentHandoffsCount.length,
18635
18709
  rolling_handoff_drafts_count: rollingHandoffDraftsCount,
18636
18710
  saved_handoffs_count: savedHandoffsCount,
18711
+ recent_inbox_notes_count: recentInboxNotes.length,
18712
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
18637
18713
  recent_chat_count: recentChatCount,
18638
18714
  recent_chat_sessions: recentChat.session_count,
18639
18715
  chat_source_summary: recentChat.source_summary,
@@ -18811,6 +18887,7 @@ function getMemoryConsole(db, input) {
18811
18887
  }).handoffs;
18812
18888
  const rollingHandoffDrafts = recentHandoffs.filter((handoff) => isDraftHandoff(handoff)).length;
18813
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 }));
18814
18891
  const recentChat = getRecentChat(db, {
18815
18892
  cwd,
18816
18893
  project_scoped: projectScoped,
@@ -18842,11 +18919,13 @@ function getMemoryConsole(db, input) {
18842
18919
  key: item.key,
18843
18920
  kind: item.kind,
18844
18921
  freshness: item.freshness,
18845
- title: item.title
18922
+ title: item.title,
18923
+ source_agent: item.source_agent
18846
18924
  })),
18847
18925
  best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
18848
18926
  best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
18849
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),
18850
18929
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18851
18930
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18852
18931
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -18857,6 +18936,8 @@ function getMemoryConsole(db, input) {
18857
18936
  recent_handoffs: recentHandoffs,
18858
18937
  rolling_handoff_drafts: rollingHandoffDrafts,
18859
18938
  saved_handoffs: savedHandoffs,
18939
+ recent_inbox_notes: recentInboxNotes,
18940
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
18860
18941
  recent_chat: recentChat.messages,
18861
18942
  recent_chat_sessions: projectIndex?.recent_chat_sessions ?? recentChat.session_count,
18862
18943
  chat_source_summary: projectIndex?.chat_source_summary ?? recentChat.source_summary,
@@ -19125,6 +19206,7 @@ function toChatEvent(message) {
19125
19206
  };
19126
19207
  }
19127
19208
  function toObservationEvent(obs) {
19209
+ const messageKind = classifyMessageObservation(obs);
19128
19210
  if (looksLikeHandoff(obs)) {
19129
19211
  const handoffKind = isDraftHandoff(obs) ? "draft" : "saved";
19130
19212
  return {
@@ -19139,6 +19221,8 @@ function toObservationEvent(obs) {
19139
19221
  };
19140
19222
  }
19141
19223
  const detailBits = [];
19224
+ if (messageKind === "inbox-note")
19225
+ detailBits.push("inbox note");
19142
19226
  if (obs.source_tool)
19143
19227
  detailBits.push(`via ${obs.source_tool}`);
19144
19228
  if (typeof obs.source_prompt_number === "number") {
@@ -19633,6 +19717,12 @@ function getAgentMemoryIndex(db, input) {
19633
19717
  chatCoverage.set(row.agent, current);
19634
19718
  }
19635
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;
19636
19726
  const latestByAgent = new Map;
19637
19727
  const devicesByAgent = new Map;
19638
19728
  for (const session of recentSessions) {
@@ -19665,6 +19755,7 @@ function getAgentMemoryIndex(db, input) {
19665
19755
  hook_count: 0
19666
19756
  };
19667
19757
  const latestSession = latestByAgent.get(agent) ?? null;
19758
+ const bestRecall = pickBestRecallForAgent(recallItems, agent);
19668
19759
  return {
19669
19760
  agent,
19670
19761
  session_count: session.session_count,
@@ -19680,7 +19771,11 @@ function getAgentMemoryIndex(db, input) {
19680
19771
  last_seen_epoch: session.last_seen_epoch,
19681
19772
  latest_session_id: latestSession?.session_id ?? null,
19682
19773
  latest_summary: latestSession?.current_thread ?? latestSession?.request ?? latestSession?.completed ?? null,
19683
- devices: Array.from(devicesByAgent.get(agent) ?? []).sort()
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"
19684
19779
  };
19685
19780
  }).sort((a, b) => {
19686
19781
  const epochA = a.last_seen_epoch ?? 0;
@@ -19720,6 +19815,12 @@ function buildSuggestedTools2(agents) {
19720
19815
  if (agents.length === 0)
19721
19816
  return [];
19722
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
+ }
19723
19824
  if (agents.some((agent) => agent.continuity_state !== "fresh")) {
19724
19825
  suggestions.push("resume_thread");
19725
19826
  }
@@ -19728,9 +19829,12 @@ function buildSuggestedTools2(agents) {
19728
19829
  }
19729
19830
  return suggestions;
19730
19831
  }
19832
+ function pickBestRecallForAgent(items, agent) {
19833
+ return items.find((item) => item.source_agent === agent) ?? null;
19834
+ }
19731
19835
 
19732
19836
  // src/tools/tool-memory-index.ts
19733
- function parseConcepts(value) {
19837
+ function parseConcepts2(value) {
19734
19838
  if (!value)
19735
19839
  return [];
19736
19840
  try {
@@ -19799,7 +19903,7 @@ function getToolMemoryIndex(db, input = {}) {
19799
19903
  ORDER BY o.created_at_epoch DESC, o.id DESC
19800
19904
  LIMIT 50`).all(...rowParams);
19801
19905
  const topPlugins = Array.from(observationRows.reduce((acc, obs) => {
19802
- for (const concept of parseConcepts(obs.concepts)) {
19906
+ for (const concept of parseConcepts2(obs.concepts)) {
19803
19907
  if (!concept.startsWith("plugin:"))
19804
19908
  continue;
19805
19909
  const plugin = concept.slice("plugin:".length);
@@ -19828,7 +19932,7 @@ function getToolMemoryIndex(db, input = {}) {
19828
19932
  }
19829
19933
 
19830
19934
  // src/tools/session-tool-memory.ts
19831
- function parseConcepts2(value) {
19935
+ function parseConcepts3(value) {
19832
19936
  if (!value)
19833
19937
  return [];
19834
19938
  try {
@@ -19860,7 +19964,7 @@ function getSessionToolMemory(db, input) {
19860
19964
  }, new Map).entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
19861
19965
  const sampleTitles = groupedObservations.map((obs) => obs.title).filter((title, index, all) => all.indexOf(title) === index).slice(0, 4);
19862
19966
  const topPlugins = Array.from(groupedObservations.reduce((acc, obs) => {
19863
- for (const concept of parseConcepts2(obs.concepts)) {
19967
+ for (const concept of parseConcepts3(obs.concepts)) {
19864
19968
  if (!concept.startsWith("plugin:"))
19865
19969
  continue;
19866
19970
  const plugin = concept.slice("plugin:".length);
@@ -19916,6 +20020,13 @@ function getSessionContext(db, input) {
19916
20020
  user_id: input.user_id,
19917
20021
  limit: 8
19918
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);
19919
20030
  const recentChatMessages = recentChat.messages.length;
19920
20031
  const recallIndex = listRecallItems(db, {
19921
20032
  cwd,
@@ -19946,11 +20057,13 @@ function getSessionContext(db, input) {
19946
20057
  key: item.key,
19947
20058
  kind: item.kind,
19948
20059
  freshness: item.freshness,
19949
- title: item.title
20060
+ title: item.title,
20061
+ source_agent: item.source_agent
19950
20062
  })),
19951
20063
  best_recall_key: bestRecallItem?.key ?? null,
19952
20064
  best_recall_title: bestRecallItem?.title ?? null,
19953
20065
  best_recall_kind: bestRecallItem?.kind ?? null,
20066
+ best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
19954
20067
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
19955
20068
  resume_source_session_id: latestSession?.session_id ?? null,
19956
20069
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -19964,6 +20077,8 @@ function getSessionContext(db, input) {
19964
20077
  rolling_handoff_drafts: rollingHandoffDrafts,
19965
20078
  saved_handoffs: savedHandoffs,
19966
20079
  latest_handoff_title: latestHandoffTitle,
20080
+ recent_inbox_notes: recentInboxNotes.length,
20081
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
19967
20082
  recent_chat_messages: recentChatMessages,
19968
20083
  recent_chat_sessions: recentChat.session_count,
19969
20084
  chat_source_summary: recentChat.source_summary,
@@ -20053,6 +20168,7 @@ function loadRecallItem(db, input) {
20053
20168
  detail: "Malformed recall key",
20054
20169
  session_id: null,
20055
20170
  source_device_id: null,
20171
+ source_agent: null,
20056
20172
  payload: null
20057
20173
  };
20058
20174
  }
@@ -20074,6 +20190,7 @@ function loadRecallItem(db, input) {
20074
20190
  detail: summarizeNarrative(result.handoff.narrative),
20075
20191
  session_id: result.handoff.session_id ?? null,
20076
20192
  source_device_id: result.handoff.device_id ?? null,
20193
+ source_agent: result.handoff.session_id ? lookupSessionAgent(db, result.handoff.session_id) : null,
20077
20194
  payload: {
20078
20195
  type: "handoff",
20079
20196
  handoff_id: result.handoff.id,
@@ -20093,6 +20210,7 @@ function loadRecallItem(db, input) {
20093
20210
  detail: story.summary?.next_steps ?? story.summary?.completed ?? null,
20094
20211
  session_id: story.session.session_id,
20095
20212
  source_device_id: story.session.device_id ?? null,
20213
+ source_agent: story.session.agent ?? null,
20096
20214
  payload: {
20097
20215
  type: "thread",
20098
20216
  latest_request: story.latest_request,
@@ -20122,6 +20240,7 @@ function loadRecallItem(db, input) {
20122
20240
  detail: message.content,
20123
20241
  session_id: message.session_id,
20124
20242
  source_device_id: message.device_id ?? null,
20243
+ source_agent: message.agent ?? null,
20125
20244
  payload: {
20126
20245
  type: "chat",
20127
20246
  role: message.role,
@@ -20147,6 +20266,7 @@ function loadRecallItem(db, input) {
20147
20266
  detail: obs.narrative ?? obs.facts ?? null,
20148
20267
  session_id: obs.session_id ?? null,
20149
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,
20150
20270
  payload: {
20151
20271
  type: "memory",
20152
20272
  observation_id: obs.id,
@@ -20163,6 +20283,10 @@ function summarizeNarrative(value) {
20163
20283
  return null;
20164
20284
  return value.split(/\n+/).map((line) => line.trim()).find(Boolean) ?? null;
20165
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
+ }
20166
20290
  function missing(key, kind) {
20167
20291
  return {
20168
20292
  kind,
@@ -20171,6 +20295,7 @@ function missing(key, kind) {
20171
20295
  detail: "Recall item not found",
20172
20296
  session_id: null,
20173
20297
  source_device_id: null,
20298
+ source_agent: null,
20174
20299
  payload: null
20175
20300
  };
20176
20301
  }
@@ -20597,6 +20722,9 @@ async function resumeThread(db, config2, input = {}) {
20597
20722
  const detected = detectProject(cwd);
20598
20723
  const project = db.getProjectByCanonicalId(detected.canonical_id);
20599
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
+ }
20600
20728
  let repairResult = null;
20601
20729
  const shouldRepair = repairIfNeeded && snapshot.recentChat.coverage_state !== "transcript-backed" && (snapshot.recentChat.messages.length > 0 || snapshot.recentSessions.length > 0 || snapshot.context?.continuity_state !== "cold");
20602
20730
  if (shouldRepair) {
@@ -20607,6 +20735,9 @@ async function resumeThread(db, config2, input = {}) {
20607
20735
  });
20608
20736
  if (repairResult.imported_chat_messages > 0) {
20609
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
+ }
20610
20741
  }
20611
20742
  }
20612
20743
  const { context, handoff, recentChat, recentSessions, recall } = snapshot;
@@ -20647,6 +20778,7 @@ async function resumeThread(db, config2, input = {}) {
20647
20778
  ])).slice(0, 4);
20648
20779
  return {
20649
20780
  project_name: project?.name ?? context?.project_name ?? null,
20781
+ target_agent: input.agent ?? null,
20650
20782
  continuity_state: context?.continuity_state ?? "cold",
20651
20783
  continuity_summary: context?.continuity_summary ?? "No fresh repo-local continuity yet; older memory should be treated cautiously.",
20652
20784
  resume_freshness: classifyResumeFreshness2(sourceTimestamp),
@@ -20696,6 +20828,13 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20696
20828
  user_id: userId,
20697
20829
  current_device_id: currentDeviceId
20698
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;
20699
20838
  const recentChat = getRecentChat(db, {
20700
20839
  cwd,
20701
20840
  project_scoped: true,
@@ -20725,15 +20864,50 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20725
20864
  return {
20726
20865
  context,
20727
20866
  handoff: handoffResult.handoff,
20867
+ recentHandoffs,
20728
20868
  recentChat,
20729
20869
  recentSessions,
20730
20870
  recall,
20731
20871
  recallIndex
20732
20872
  };
20733
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
+ }
20734
20901
  function pickBestRecallItem2(items) {
20735
20902
  return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
20736
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
+ }
20737
20911
  function extractCurrentThread(handoff) {
20738
20912
  const narrative = handoff?.narrative ?? "";
20739
20913
  const match = narrative.match(/Current thread:\s*(.+)/i);
@@ -20976,8 +21150,9 @@ function hasContent(value) {
20976
21150
  // src/tools/stats.ts
20977
21151
  function getMemoryStats(db) {
20978
21152
  const activeObservations = db.getActiveObservationCount();
20979
- const messages = db.db.query(`SELECT COUNT(*) as count FROM observations
20980
- 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);
20981
21156
  const userPrompts = db.db.query("SELECT COUNT(*) as count FROM user_prompts").get()?.count ?? 0;
20982
21157
  const toolEvents = db.db.query("SELECT COUNT(*) as count FROM tool_events").get()?.count ?? 0;
20983
21158
  const sessionSummaries = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
@@ -20991,7 +21166,9 @@ function getMemoryStats(db) {
20991
21166
  active_observations: activeObservations,
20992
21167
  user_prompts: userPrompts,
20993
21168
  tool_events: toolEvents,
20994
- messages,
21169
+ messages: inboxMessages,
21170
+ inbox_messages: inboxMessages,
21171
+ handoffs,
20995
21172
  session_summaries: sessionSummaries,
20996
21173
  decisions: signals.decisions_count,
20997
21174
  lessons: signals.lessons_count,
@@ -22665,7 +22842,7 @@ process.on("SIGTERM", () => {
22665
22842
  });
22666
22843
  var server = new McpServer({
22667
22844
  name: "engrm",
22668
- version: "0.4.34"
22845
+ version: "0.4.35"
22669
22846
  });
22670
22847
  server.tool("save_observation", "Save an observation to memory", {
22671
22848
  type: exports_external.enum([
@@ -23135,7 +23312,7 @@ server.tool("list_recall_items", "USE FIRST when continuity feels fuzzy. List th
23135
23312
  }
23136
23313
  const projectLine = result.project ? `Project: ${result.project}
23137
23314
  ` : "";
23138
- 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})` : ""}
23139
23316
  ${item.detail}`).join(`
23140
23317
  `);
23141
23318
  return {
@@ -23180,6 +23357,7 @@ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact rec
23180
23357
  ` + `Title: ${result.title}
23181
23358
  ` + `Session: ${result.session_id ?? "(unknown)"}
23182
23359
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23360
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23183
23361
 
23184
23362
  ` + `${result.payload.narrative ?? "(no narrative)"}`
23185
23363
  }
@@ -23199,6 +23377,7 @@ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact rec
23199
23377
  ` + `Title: ${result.title}
23200
23378
  ` + `Session: ${result.session_id ?? "(unknown)"}
23201
23379
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23380
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23202
23381
  ` + `Latest request: ${result.payload.latest_request ?? "(none)"}
23203
23382
  ` + `Current thread: ${result.payload.current_thread ?? "(none)"}
23204
23383
 
@@ -23220,6 +23399,7 @@ ${hotFiles}`
23220
23399
  ` + `Title: ${result.title}
23221
23400
  ` + `Session: ${result.session_id ?? "(unknown)"}
23222
23401
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23402
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23223
23403
 
23224
23404
  ` + `${result.payload.content}`
23225
23405
  }
@@ -23234,6 +23414,7 @@ ${hotFiles}`
23234
23414
  ` + `Title: ${result.title}
23235
23415
  ` + `Session: ${result.session_id ?? "(unknown)"}
23236
23416
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23417
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23237
23418
  ` + `Type: ${result.payload.observation_type}
23238
23419
 
23239
23420
  ` + `${result.payload.narrative ?? result.payload.facts ?? "(no detail)"}`
@@ -23244,12 +23425,14 @@ ${hotFiles}`
23244
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.", {
23245
23426
  cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
23246
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"),
23247
23429
  user_id: exports_external.string().optional().describe("Optional user override"),
23248
23430
  repair_if_needed: exports_external.boolean().optional().describe("If true, attempt recall repair before resuming when continuity is still weak")
23249
23431
  }, async (params) => {
23250
23432
  const result = await resumeThread(db, config2, {
23251
23433
  cwd: params.cwd ?? process.cwd(),
23252
23434
  limit: params.limit,
23435
+ agent: params.agent,
23253
23436
  user_id: params.user_id ?? config2.user_id,
23254
23437
  current_device_id: config2.device_id,
23255
23438
  repair_if_needed: params.repair_if_needed
@@ -23291,7 +23474,8 @@ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?
23291
23474
  content: [
23292
23475
  {
23293
23476
  type: "text",
23294
- 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}
23295
23479
  ` + `Freshness: ${result.resume_freshness}
23296
23480
  ` + `Source: ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23297
23481
  ` + `Resume confidence: ${result.resume_confidence}
@@ -23450,13 +23634,7 @@ server.tool("check_messages", "Check for messages sent from other devices or ses
23450
23634
  const markRead = params.mark_read !== false;
23451
23635
  const readKey = `messages_read_${config2.device_id}`;
23452
23636
  const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
23453
- const messages = db.db.query(`SELECT id, title, narrative, user_id, device_id, created_at FROM observations
23454
- WHERE type = 'message'
23455
- AND id > ?
23456
- AND lifecycle IN ('active', 'pinned')
23457
- AND device_id != ?
23458
- AND (sensitivity != 'personal' OR user_id = ?)
23459
- 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);
23460
23638
  if (messages.length === 0) {
23461
23639
  return {
23462
23640
  content: [{ type: "text", text: "No new messages." }]
@@ -23503,7 +23681,7 @@ server.tool("send_message", "Leave a cross-device or team note in Engrm's shared
23503
23681
  ]
23504
23682
  };
23505
23683
  });
23506
- 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", {
23507
23685
  limit: exports_external.number().optional().describe("Max observations to return (default: 10)"),
23508
23686
  project_scoped: exports_external.boolean().optional().describe("Scope to current project (default: true)"),
23509
23687
  type: exports_external.string().optional().describe("Optional observation type filter")
@@ -23526,12 +23704,21 @@ server.tool("recent_activity", "Inspect the most recent observations captured by
23526
23704
  const showProject = !result.project;
23527
23705
  const header = showProject ? "| ID | Project | Type | Title | Created |" : "| ID | Type | Title | Created |";
23528
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
+ };
23529
23716
  const rows = result.observations.map((obs) => {
23530
23717
  const date5 = obs.created_at.split("T")[0];
23531
23718
  if (showProject) {
23532
- 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} |`;
23533
23720
  }
23534
- return `| ${obs.id} | ${obs.type} | ${obs.title} | ${date5} |`;
23721
+ return `| ${obs.id} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23535
23722
  });
23536
23723
  const projectLine = result.project ? `Project: ${result.project}
23537
23724
  ` : "";
@@ -23567,7 +23754,8 @@ server.tool("memory_stats", "Show high-level Engrm capture and sync statistics",
23567
23754
  text: `Active observations: ${stats.active_observations}
23568
23755
  ` + `User prompts: ${stats.user_prompts}
23569
23756
  ` + `Tool events: ${stats.tool_events}
23570
- ` + `Messages: ${stats.messages}
23757
+ ` + `Inbox notes: ${stats.inbox_messages}
23758
+ ` + `Handoffs: ${stats.handoffs}
23571
23759
  ` + `Session summaries: ${stats.session_summaries}
23572
23760
  ` + `Summary coverage: learned ${stats.summaries_with_learned}, completed ${stats.summaries_with_completed}, next steps ${stats.summaries_with_next_steps}
23573
23761
  ` + `Installed packs: ${packs}
@@ -23611,6 +23799,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23611
23799
  }).join(`
23612
23800
  `) : "- (none)";
23613
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(`
23614
23804
  `) : "- (none)";
23615
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(`
23616
23806
  `) : "- (none)";
@@ -23631,7 +23821,7 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23631
23821
  `) : "- (none)";
23632
23822
  const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
23633
23823
  `) : "- (none)";
23634
- 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(`
23635
23825
  `) : "- (none)";
23636
23826
  const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23637
23827
  ` : "";
@@ -23654,10 +23844,11 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23654
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})
23655
23845
  ` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
23656
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}"` : ""}
23657
23848
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
23658
23849
  ` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23659
23850
 
23660
- ` + openExactLine + `Recall preview:
23851
+ ` + openExactLine + resumeAgentLine + `Recall preview:
23661
23852
  ${recallPreviewLines}
23662
23853
 
23663
23854
  ` + `Next actions:
@@ -23679,6 +23870,9 @@ ${sessionLines}
23679
23870
  ` + `Recent handoffs:
23680
23871
  ${handoffLines}
23681
23872
 
23873
+ ` + `Recent inbox notes:
23874
+ ${inboxNoteLines}
23875
+
23682
23876
  ` + `Recent requests:
23683
23877
  ${requestLines}
23684
23878
 
@@ -23785,7 +23979,8 @@ server.tool("agent_memory_index", "Compare continuity and capture health across
23785
23979
  const lastSeen = agent.last_seen_epoch ? new Date(agent.last_seen_epoch * 1000).toISOString().replace("T", " ").slice(0, 16) : "unknown";
23786
23980
  const latest = agent.latest_summary ? ` latest="${agent.latest_summary.replace(/\s+/g, " ").trim().slice(0, 120)}"` : "";
23787
23981
  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}`;
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}")`;
23789
23984
  }).join(`
23790
23985
  `) : "- (none)";
23791
23986
  return {
@@ -23891,7 +24086,8 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23891
24086
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23892
24087
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23893
24088
  ` + `Open exact: ${result.best_recall_key ? `load_recall_item("${result.best_recall_key}")` : "(none)"}
23894
- ` + `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})` : ""}
23895
24091
  ` + `Loaded observations: ${result.session_count}
23896
24092
  ` + `Searchable total: ${result.total_active}
23897
24093
  ` + `Recent requests: ${result.recent_requests}
@@ -23899,6 +24095,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23899
24095
  ` + `Recent sessions: ${result.recent_sessions}
23900
24096
  ` + `Recent handoffs: ${result.recent_handoffs}
23901
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}"` : ""}
23902
24099
  ` + `Recent chat messages: ${result.recent_chat_messages}
23903
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})
23904
24101
  ` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
@@ -23969,7 +24166,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23969
24166
  `) : "- (none)";
23970
24167
  const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
23971
24168
  `) : "- (none)";
23972
- 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(`
23973
24170
  `) : "- (none)";
23974
24171
  const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23975
24172
  ` : "";
@@ -23988,6 +24185,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23988
24185
 
23989
24186
  ` + `Recent handoffs captured: ${result.recent_handoffs_count}
23990
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}"` : ""}
23991
24189
  ` + `Recent chat messages captured: ${result.recent_chat_count}
23992
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})
23993
24191
 
@@ -23997,7 +24195,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23997
24195
  ` + `Estimated read cost: ~${result.estimated_read_tokens}t
23998
24196
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23999
24197
 
24000
- ` + openExactLine + `Recall preview:
24198
+ ` + openExactLine + resumeAgentLine + `Recall preview:
24001
24199
  ${recallPreviewLines}
24002
24200
 
24003
24201
  ` + `Next actions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.34",
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",