engrm 0.4.34 → 0.4.36

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
package/dist/cli.js CHANGED
@@ -1592,7 +1592,12 @@ class MemDatabase {
1592
1592
  vecChatInsert(chatMessageId, embedding) {
1593
1593
  if (!this.vecAvailable)
1594
1594
  return;
1595
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1595
+ const normalizedId = Number(chatMessageId);
1596
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
1597
+ return;
1598
+ try {
1599
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
1600
+ } catch {}
1596
1601
  }
1597
1602
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1598
1603
  if (!this.vecAvailable)
@@ -2428,7 +2428,12 @@ class MemDatabase {
2428
2428
  vecChatInsert(chatMessageId, embedding) {
2429
2429
  if (!this.vecAvailable)
2430
2430
  return;
2431
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
2431
+ const normalizedId = Number(chatMessageId);
2432
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
2433
+ return;
2434
+ try {
2435
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
2436
+ } catch {}
2432
2437
  }
2433
2438
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
2434
2439
  if (!this.vecAvailable)
@@ -1770,7 +1770,12 @@ class MemDatabase {
1770
1770
  vecChatInsert(chatMessageId, embedding) {
1771
1771
  if (!this.vecAvailable)
1772
1772
  return;
1773
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1773
+ const normalizedId = Number(chatMessageId);
1774
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
1775
+ return;
1776
+ try {
1777
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
1778
+ } catch {}
1774
1779
  }
1775
1780
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1776
1781
  if (!this.vecAvailable)
@@ -1564,7 +1564,12 @@ class MemDatabase {
1564
1564
  vecChatInsert(chatMessageId, embedding) {
1565
1565
  if (!this.vecAvailable)
1566
1566
  return;
1567
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1567
+ const normalizedId = Number(chatMessageId);
1568
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
1569
+ return;
1570
+ try {
1571
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
1572
+ } catch {}
1568
1573
  }
1569
1574
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1570
1575
  if (!this.vecAvailable)
@@ -1640,7 +1640,12 @@ class MemDatabase {
1640
1640
  vecChatInsert(chatMessageId, embedding) {
1641
1641
  if (!this.vecAvailable)
1642
1642
  return;
1643
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1643
+ const normalizedId = Number(chatMessageId);
1644
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
1645
+ return;
1646
+ try {
1647
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
1648
+ } catch {}
1644
1649
  }
1645
1650
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1646
1651
  if (!this.vecAvailable)
@@ -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.36";
3148
3229
  function hashFile(filePath) {
3149
3230
  try {
3150
3231
  if (!existsSync3(filePath))
@@ -5143,7 +5224,12 @@ class MemDatabase {
5143
5224
  vecChatInsert(chatMessageId, embedding) {
5144
5225
  if (!this.vecAvailable)
5145
5226
  return;
5146
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
5227
+ const normalizedId = Number(chatMessageId);
5228
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
5229
+ return;
5230
+ try {
5231
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
5232
+ } catch {}
5147
5233
  }
5148
5234
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
5149
5235
  if (!this.vecAvailable)
@@ -5494,12 +5580,7 @@ async function main() {
5494
5580
  try {
5495
5581
  const readKey = `messages_read_${config.device_id}`;
5496
5582
  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;
5583
+ msgCount = getUnreadInboxMessageCount(db, config.device_id, config.user_id, lastReadId);
5503
5584
  } catch {}
5504
5585
  const splash = formatSplashScreen({
5505
5586
  projectName: context.project_name,
@@ -5653,6 +5734,7 @@ function formatVisibleStartupBrief(context) {
5653
5734
  const projectSignals = buildProjectSignalLine(context);
5654
5735
  const shownItems = new Set;
5655
5736
  const latestHandoffLines = buildLatestHandoffLines(context);
5737
+ const inboxNoteLines = buildRecentInboxNoteLines(context);
5656
5738
  const freshContinuity = hasFreshContinuitySignal(context);
5657
5739
  if (latestHandoffLines.length > 0) {
5658
5740
  lines.push(`${c2.cyan}Latest handoff:${c2.reset}`);
@@ -5661,6 +5743,13 @@ function formatVisibleStartupBrief(context) {
5661
5743
  rememberShownItem(shownItems, item);
5662
5744
  }
5663
5745
  }
5746
+ if (inboxNoteLines.length > 0) {
5747
+ lines.push(`${c2.cyan}Inbox notes:${c2.reset}`);
5748
+ for (const item of inboxNoteLines) {
5749
+ lines.push(` - ${truncateInline(item, 160)}`);
5750
+ rememberShownItem(shownItems, item);
5751
+ }
5752
+ }
5664
5753
  lines.push(`${c2.cyan}Continuity:${c2.reset} ${continuityState} \u2014 ${truncateInline(describeContinuityState(continuityState), 160)}`);
5665
5754
  const resumeReadiness = buildResumeReadinessLine(context);
5666
5755
  if (resumeReadiness) {
@@ -5786,6 +5875,10 @@ function buildLatestHandoffLines(context) {
5786
5875
  }
5787
5876
  return Array.from(new Set(lines.filter(Boolean))).slice(0, 2);
5788
5877
  }
5878
+ function buildRecentInboxNoteLines(context) {
5879
+ const notes = context.observations.filter((obs) => classifyMessageObservation(obs) === "inbox-note").slice(0, 2);
5880
+ return Array.from(new Set(notes.map((obs) => obs.title.trim()).filter((title) => title.length > 0)));
5881
+ }
5789
5882
  function buildResumeReadinessLine(context) {
5790
5883
  const latestSession = context.recentSessions?.[0] ?? null;
5791
5884
  const latestHandoff = context.recentHandoffs?.[0] ?? null;
@@ -5888,10 +5981,12 @@ function formatInspectHints(context, visibleObservationIds = [], recallItems = [
5888
5981
  return [];
5889
5982
  const ids = visibleObservationIds.slice(0, 5);
5890
5983
  const openNowItem = recallItems.find((item) => item.kind !== "memory") ?? null;
5984
+ const resumeAgent = activeAgents.length > 1 ? context.recentSessions?.[0]?.agent ?? null : null;
5891
5985
  const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
5892
5986
  return [
5893
5987
  `${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
5894
5988
  ...openNowItem ? [`${c2.dim}Open now:${c2.reset} load_recall_item("${openNowItem.key}")`] : [],
5989
+ ...resumeAgent ? [`${c2.dim}Resume agent:${c2.reset} resume_thread(agent="${resumeAgent}")`] : [],
5895
5990
  ...fetchHint ? [`${c2.dim}Pull detail:${c2.reset} ${fetchHint}`] : []
5896
5991
  ];
5897
5992
  }
@@ -5904,7 +5999,7 @@ function formatStartupRecallPreview(recallItems) {
5904
5999
  return [];
5905
6000
  return [
5906
6001
  `${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)}`)
6002
+ ...items.map((item) => `${item.key} [${item.kind} \xB7 ${item.freshness}${item.source_agent ? ` \xB7 ${item.source_agent}` : ""}] ${truncateInline(item.title, 110)}`)
5908
6003
  ];
5909
6004
  }
5910
6005
  function buildStartupRecallItems(context) {
@@ -5919,6 +6014,7 @@ function buildStartupRecallItems(context) {
5919
6014
  kind: "handoff",
5920
6015
  freshness,
5921
6016
  title,
6017
+ source_agent: (context.recentSessions ?? []).find((session) => session.session_id === handoff.session_id)?.agent ?? null,
5922
6018
  score: freshnessScore(freshness) + 40
5923
6019
  });
5924
6020
  }
@@ -5933,6 +6029,7 @@ function buildStartupRecallItems(context) {
5933
6029
  kind: "thread",
5934
6030
  freshness,
5935
6031
  title,
6032
+ source_agent: session.agent ?? null,
5936
6033
  score: freshnessScore(freshness) + 30
5937
6034
  });
5938
6035
  }
@@ -5945,6 +6042,7 @@ function buildStartupRecallItems(context) {
5945
6042
  kind: "chat",
5946
6043
  freshness,
5947
6044
  title: `[${message.role}] ${message.content.replace(/\s+/g, " ").trim()}`,
6045
+ source_agent: message.agent ?? null,
5948
6046
  score: freshnessScore(freshness) + 20
5949
6047
  });
5950
6048
  }
@@ -1797,7 +1797,12 @@ class MemDatabase {
1797
1797
  vecChatInsert(chatMessageId, embedding) {
1798
1798
  if (!this.vecAvailable)
1799
1799
  return;
1800
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1800
+ const normalizedId = Number(chatMessageId);
1801
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
1802
+ return;
1803
+ try {
1804
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
1805
+ } catch {}
1801
1806
  }
1802
1807
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1803
1808
  if (!this.vecAvailable)
@@ -3082,7 +3087,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3082
3087
  sentinel_used: valueSignals.security_findings_count > 0,
3083
3088
  risk_score: riskScore,
3084
3089
  stacks_detected: stacks,
3085
- client_version: "0.4.34",
3090
+ client_version: "0.4.36",
3086
3091
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3087
3092
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3088
3093
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -1708,7 +1708,12 @@ class MemDatabase {
1708
1708
  vecChatInsert(chatMessageId, embedding) {
1709
1709
  if (!this.vecAvailable)
1710
1710
  return;
1711
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1711
+ const normalizedId = Number(chatMessageId);
1712
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
1713
+ return;
1714
+ try {
1715
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
1716
+ } catch {}
1712
1717
  }
1713
1718
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1714
1719
  if (!this.vecAvailable)
package/dist/server.js CHANGED
@@ -15114,7 +15114,12 @@ class MemDatabase {
15114
15114
  vecChatInsert(chatMessageId, embedding) {
15115
15115
  if (!this.vecAvailable)
15116
15116
  return;
15117
- this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
15117
+ const normalizedId = Number(chatMessageId);
15118
+ if (!Number.isInteger(normalizedId) || normalizedId <= 0)
15119
+ return;
15120
+ try {
15121
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(normalizedId, new Uint8Array(embedding.buffer));
15122
+ } catch {}
15118
15123
  }
15119
15124
  searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
15120
15125
  if (!this.vecAvailable)
@@ -16827,6 +16832,69 @@ function pinObservation(db, input) {
16827
16832
  return { success: true };
16828
16833
  }
16829
16834
 
16835
+ // src/tools/inbox-messages.ts
16836
+ function buildHandoffMessageFilterSql(lifecycle) {
16837
+ return `
16838
+ type = 'message'
16839
+ AND lifecycle IN (${lifecycle})
16840
+ AND (
16841
+ COALESCE(source_tool, '') IN ('create_handoff', 'rolling_handoff')
16842
+ OR title LIKE 'Handoff:%'
16843
+ OR title LIKE 'Handoff Draft:%'
16844
+ OR (
16845
+ concepts IS NOT NULL AND (
16846
+ concepts LIKE '%"handoff"%'
16847
+ OR concepts LIKE '%"session-handoff"%'
16848
+ OR concepts LIKE '%"draft-handoff"%'
16849
+ )
16850
+ )
16851
+ )
16852
+ `;
16853
+ }
16854
+ var HANDOFF_MESSAGE_FILTER_SQL = buildHandoffMessageFilterSql("'active', 'pinned'");
16855
+ var INBOX_MESSAGE_FILTER_SQL = `
16856
+ type = 'message'
16857
+ AND lifecycle IN ('active', 'pinned')
16858
+ AND NOT (${HANDOFF_MESSAGE_FILTER_SQL})
16859
+ `;
16860
+ function getHandoffMessageFilterSql(options) {
16861
+ return buildHandoffMessageFilterSql(options?.include_aging ? "'active', 'aging', 'pinned'" : "'active', 'pinned'");
16862
+ }
16863
+ function classifyMessageObservation(observation) {
16864
+ if (observation.type !== "message")
16865
+ return null;
16866
+ const concepts = parseConcepts(observation.concepts);
16867
+ const isDraft = observation.title.startsWith("Handoff Draft:") || observation.source_tool === "rolling_handoff" || concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
16868
+ if (isDraft)
16869
+ return "draft-handoff";
16870
+ const isHandoff = observation.title.startsWith("Handoff:") || observation.source_tool === "create_handoff" || concepts.includes("handoff") || concepts.includes("session-handoff");
16871
+ if (isHandoff)
16872
+ return "handoff";
16873
+ return "inbox-note";
16874
+ }
16875
+ function parseConcepts(value) {
16876
+ if (!value)
16877
+ return [];
16878
+ try {
16879
+ const parsed = JSON.parse(value);
16880
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
16881
+ } catch {
16882
+ return [];
16883
+ }
16884
+ }
16885
+ function getInboxMessageCount(db) {
16886
+ return db.db.query(`SELECT COUNT(*) as c FROM observations
16887
+ WHERE ${INBOX_MESSAGE_FILTER_SQL}`).get()?.c ?? 0;
16888
+ }
16889
+ function getUnreadInboxMessages(db, currentDeviceId, userId, lastReadId, limit = 20) {
16890
+ return db.db.query(`SELECT id, title, narrative, user_id, device_id, created_at FROM observations
16891
+ WHERE ${INBOX_MESSAGE_FILTER_SQL}
16892
+ AND id > ?
16893
+ AND device_id != ?
16894
+ AND (sensitivity != 'personal' OR user_id = ?)
16895
+ ORDER BY created_at_epoch DESC LIMIT ?`).all(lastReadId, currentDeviceId, userId, limit);
16896
+ }
16897
+
16830
16898
  // src/tools/recent.ts
16831
16899
  function getRecentActivity(db, input) {
16832
16900
  const limit = Math.max(1, Math.min(input.limit ?? 10, 50));
@@ -16865,7 +16933,10 @@ function getRecentActivity(db, input) {
16865
16933
  LEFT JOIN projects ON projects.id = observations.project_id
16866
16934
  WHERE ${conditions.join(" AND ")}
16867
16935
  ORDER BY observations.created_at_epoch DESC
16868
- LIMIT ?`).all(...params);
16936
+ LIMIT ?`).all(...params).map((observation) => ({
16937
+ ...observation,
16938
+ message_kind: classifyMessageObservation(observation)
16939
+ }));
16869
16940
  return {
16870
16941
  observations,
16871
16942
  project: projectName
@@ -18359,6 +18430,7 @@ function listRecallItems(db, input) {
18359
18430
  user_id: input.user_id,
18360
18431
  limit: Math.min(limit * 2, 24)
18361
18432
  }).observations;
18433
+ const sessionAgentById = new Map(sessions.filter((session) => Boolean(session.session_id)).map((session) => [session.session_id, session.agent ?? null]));
18362
18434
  const items = [
18363
18435
  ...handoffs.map((handoff) => ({
18364
18436
  key: `handoff:${handoff.id}`,
@@ -18368,7 +18440,8 @@ function listRecallItems(db, input) {
18368
18440
  created_at_epoch: handoff.created_at_epoch,
18369
18441
  freshness: classifyFreshness(handoff.created_at_epoch),
18370
18442
  session_id: handoff.session_id,
18371
- source_device_id: handoff.device_id ?? null
18443
+ source_device_id: handoff.device_id ?? null,
18444
+ source_agent: handoff.session_id ? sessionAgentById.get(handoff.session_id) ?? null : null
18372
18445
  })),
18373
18446
  ...sessions.filter((session) => Boolean(session.request || session.completed || session.current_thread)).map((session) => ({
18374
18447
  key: `session:${session.session_id}`,
@@ -18378,7 +18451,8 @@ function listRecallItems(db, input) {
18378
18451
  created_at_epoch: session.completed_at_epoch ?? session.started_at_epoch ?? 0,
18379
18452
  freshness: classifyFreshness(session.completed_at_epoch ?? session.started_at_epoch ?? null),
18380
18453
  session_id: session.session_id,
18381
- source_device_id: session.device_id ?? null
18454
+ source_device_id: session.device_id ?? null,
18455
+ source_agent: session.agent ?? null
18382
18456
  })),
18383
18457
  ...dedupeChatIndex(chat).map((message) => {
18384
18458
  const origin = getChatCaptureOrigin(message);
@@ -18390,7 +18464,8 @@ function listRecallItems(db, input) {
18390
18464
  created_at_epoch: message.created_at_epoch,
18391
18465
  freshness: classifyFreshness(message.created_at_epoch),
18392
18466
  session_id: message.session_id,
18393
- source_device_id: message.device_id ?? null
18467
+ source_device_id: message.device_id ?? null,
18468
+ source_agent: message.agent ?? null
18394
18469
  };
18395
18470
  }),
18396
18471
  ...observations.filter((obs) => obs.type !== "message").filter((obs) => !looksLikeFileOperationTitle3(obs.title)).map((obs) => ({
@@ -18401,7 +18476,8 @@ function listRecallItems(db, input) {
18401
18476
  created_at_epoch: obs.created_at_epoch,
18402
18477
  freshness: classifyFreshness(obs.created_at_epoch),
18403
18478
  session_id: obs.session_id ?? null,
18404
- source_device_id: obs.device_id ?? null
18479
+ source_device_id: obs.device_id ?? null,
18480
+ source_agent: null
18405
18481
  }))
18406
18482
  ];
18407
18483
  const deduped = dedupeRecallItems(items).sort((a, b) => compareRecallItems(a, b, input.current_device_id)).slice(0, limit);
@@ -18572,6 +18648,7 @@ function getProjectMemoryIndex(db, input) {
18572
18648
  }).handoffs;
18573
18649
  const rollingHandoffDraftsCount = recentHandoffsCount.filter((handoff) => isDraftHandoff(handoff)).length;
18574
18650
  const savedHandoffsCount = recentHandoffsCount.length - rollingHandoffDraftsCount;
18651
+ const recentInboxNotes = observations.filter((obs) => classifyMessageObservation(obs) === "inbox-note").slice(0, 5);
18575
18652
  const recentChat = getRecentChat(db, {
18576
18653
  cwd,
18577
18654
  project_scoped: true,
@@ -18617,11 +18694,13 @@ function getProjectMemoryIndex(db, input) {
18617
18694
  key: item.key,
18618
18695
  kind: item.kind,
18619
18696
  freshness: item.freshness,
18620
- title: item.title
18697
+ title: item.title,
18698
+ source_agent: item.source_agent
18621
18699
  })),
18622
18700
  best_recall_key: bestRecallItem?.key ?? null,
18623
18701
  best_recall_title: bestRecallItem?.title ?? null,
18624
18702
  best_recall_kind: bestRecallItem?.kind ?? null,
18703
+ best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
18625
18704
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18626
18705
  resume_source_session_id: latestSession?.session_id ?? null,
18627
18706
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18634,6 +18713,8 @@ function getProjectMemoryIndex(db, input) {
18634
18713
  recent_handoffs_count: recentHandoffsCount.length,
18635
18714
  rolling_handoff_drafts_count: rollingHandoffDraftsCount,
18636
18715
  saved_handoffs_count: savedHandoffsCount,
18716
+ recent_inbox_notes_count: recentInboxNotes.length,
18717
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
18637
18718
  recent_chat_count: recentChatCount,
18638
18719
  recent_chat_sessions: recentChat.session_count,
18639
18720
  chat_source_summary: recentChat.source_summary,
@@ -18811,6 +18892,7 @@ function getMemoryConsole(db, input) {
18811
18892
  }).handoffs;
18812
18893
  const rollingHandoffDrafts = recentHandoffs.filter((handoff) => isDraftHandoff(handoff)).length;
18813
18894
  const savedHandoffs = recentHandoffs.length - rollingHandoffDrafts;
18895
+ 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
18896
  const recentChat = getRecentChat(db, {
18815
18897
  cwd,
18816
18898
  project_scoped: projectScoped,
@@ -18842,11 +18924,13 @@ function getMemoryConsole(db, input) {
18842
18924
  key: item.key,
18843
18925
  kind: item.kind,
18844
18926
  freshness: item.freshness,
18845
- title: item.title
18927
+ title: item.title,
18928
+ source_agent: item.source_agent
18846
18929
  })),
18847
18930
  best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
18848
18931
  best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
18849
18932
  best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.kind ?? null,
18933
+ best_agent_resume_agent: projectIndex?.best_agent_resume_agent ?? (activeAgents.length > 1 ? sessions[0]?.agent ?? null : null),
18850
18934
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18851
18935
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18852
18936
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -18857,6 +18941,8 @@ function getMemoryConsole(db, input) {
18857
18941
  recent_handoffs: recentHandoffs,
18858
18942
  rolling_handoff_drafts: rollingHandoffDrafts,
18859
18943
  saved_handoffs: savedHandoffs,
18944
+ recent_inbox_notes: recentInboxNotes,
18945
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
18860
18946
  recent_chat: recentChat.messages,
18861
18947
  recent_chat_sessions: projectIndex?.recent_chat_sessions ?? recentChat.session_count,
18862
18948
  chat_source_summary: projectIndex?.chat_source_summary ?? recentChat.source_summary,
@@ -18866,6 +18952,7 @@ function getMemoryConsole(db, input) {
18866
18952
  recent_outcomes: projectIndex?.recent_outcomes ?? [],
18867
18953
  hot_files: projectIndex?.hot_files ?? [],
18868
18954
  provenance_summary: projectIndex?.provenance_summary ?? [],
18955
+ provenance_type_mix: collectProvenanceTypeMix(observations),
18869
18956
  assistant_checkpoint_count: projectIndex?.assistant_checkpoint_count,
18870
18957
  assistant_checkpoint_types: projectIndex?.assistant_checkpoint_types ?? [],
18871
18958
  top_types: projectIndex?.top_types ?? [],
@@ -18873,6 +18960,24 @@ function getMemoryConsole(db, input) {
18873
18960
  suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.coverage_state, activeAgents.length)
18874
18961
  };
18875
18962
  }
18963
+ function collectProvenanceTypeMix(observations) {
18964
+ const grouped = new Map;
18965
+ for (const observation of observations) {
18966
+ if (!observation.source_tool)
18967
+ continue;
18968
+ const typeCounts = grouped.get(observation.source_tool) ?? new Map;
18969
+ typeCounts.set(observation.type, (typeCounts.get(observation.type) ?? 0) + 1);
18970
+ grouped.set(observation.source_tool, typeCounts);
18971
+ }
18972
+ return Array.from(grouped.entries()).map(([tool, typeCounts]) => {
18973
+ const topTypes = Array.from(typeCounts.entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 4);
18974
+ return {
18975
+ tool,
18976
+ count: topTypes.reduce((sum, item) => sum + item.count, 0),
18977
+ top_types: topTypes
18978
+ };
18979
+ }).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
18980
+ }
18876
18981
  function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount, handoffCount, chatCount, chatCoverageState, activeAgentCount) {
18877
18982
  const suggested = [];
18878
18983
  if (sessionCount > 0)
@@ -19125,6 +19230,7 @@ function toChatEvent(message) {
19125
19230
  };
19126
19231
  }
19127
19232
  function toObservationEvent(obs) {
19233
+ const messageKind = classifyMessageObservation(obs);
19128
19234
  if (looksLikeHandoff(obs)) {
19129
19235
  const handoffKind = isDraftHandoff(obs) ? "draft" : "saved";
19130
19236
  return {
@@ -19139,6 +19245,8 @@ function toObservationEvent(obs) {
19139
19245
  };
19140
19246
  }
19141
19247
  const detailBits = [];
19248
+ if (messageKind === "inbox-note")
19249
+ detailBits.push("inbox note");
19142
19250
  if (obs.source_tool)
19143
19251
  detailBits.push(`via ${obs.source_tool}`);
19144
19252
  if (typeof obs.source_prompt_number === "number") {
@@ -19633,6 +19741,12 @@ function getAgentMemoryIndex(db, input) {
19633
19741
  chatCoverage.set(row.agent, current);
19634
19742
  }
19635
19743
  const recentSessions = db.getRecentSessions(projectId, 200, input.user_id).filter((session) => !isInternalAgent(session.agent));
19744
+ const recallItems = listRecallItems(db, {
19745
+ cwd,
19746
+ project_scoped: projectScoped,
19747
+ user_id: input.user_id,
19748
+ limit: 30
19749
+ }).items;
19636
19750
  const latestByAgent = new Map;
19637
19751
  const devicesByAgent = new Map;
19638
19752
  for (const session of recentSessions) {
@@ -19665,6 +19779,7 @@ function getAgentMemoryIndex(db, input) {
19665
19779
  hook_count: 0
19666
19780
  };
19667
19781
  const latestSession = latestByAgent.get(agent) ?? null;
19782
+ const bestRecall = pickBestRecallForAgent(recallItems, agent);
19668
19783
  return {
19669
19784
  agent,
19670
19785
  session_count: session.session_count,
@@ -19680,7 +19795,11 @@ function getAgentMemoryIndex(db, input) {
19680
19795
  last_seen_epoch: session.last_seen_epoch,
19681
19796
  latest_session_id: latestSession?.session_id ?? null,
19682
19797
  latest_summary: latestSession?.current_thread ?? latestSession?.request ?? latestSession?.completed ?? null,
19683
- devices: Array.from(devicesByAgent.get(agent) ?? []).sort()
19798
+ devices: Array.from(devicesByAgent.get(agent) ?? []).sort(),
19799
+ best_recall_key: bestRecall?.key ?? null,
19800
+ best_recall_title: bestRecall?.title ?? null,
19801
+ best_recall_kind: bestRecall?.kind ?? null,
19802
+ resume_freshness: bestRecall?.freshness ?? "stale"
19684
19803
  };
19685
19804
  }).sort((a, b) => {
19686
19805
  const epochA = a.last_seen_epoch ?? 0;
@@ -19720,6 +19839,12 @@ function buildSuggestedTools2(agents) {
19720
19839
  if (agents.length === 0)
19721
19840
  return [];
19722
19841
  const suggestions = ["recent_sessions", "capture_quality"];
19842
+ if (agents.length > 1) {
19843
+ suggestions.push("list_recall_items");
19844
+ }
19845
+ if (agents.some((agent) => agent.best_recall_key)) {
19846
+ suggestions.push("load_recall_item");
19847
+ }
19723
19848
  if (agents.some((agent) => agent.continuity_state !== "fresh")) {
19724
19849
  suggestions.push("resume_thread");
19725
19850
  }
@@ -19728,9 +19853,12 @@ function buildSuggestedTools2(agents) {
19728
19853
  }
19729
19854
  return suggestions;
19730
19855
  }
19856
+ function pickBestRecallForAgent(items, agent) {
19857
+ return items.find((item) => item.source_agent === agent) ?? null;
19858
+ }
19731
19859
 
19732
19860
  // src/tools/tool-memory-index.ts
19733
- function parseConcepts(value) {
19861
+ function parseConcepts2(value) {
19734
19862
  if (!value)
19735
19863
  return [];
19736
19864
  try {
@@ -19799,7 +19927,7 @@ function getToolMemoryIndex(db, input = {}) {
19799
19927
  ORDER BY o.created_at_epoch DESC, o.id DESC
19800
19928
  LIMIT 50`).all(...rowParams);
19801
19929
  const topPlugins = Array.from(observationRows.reduce((acc, obs) => {
19802
- for (const concept of parseConcepts(obs.concepts)) {
19930
+ for (const concept of parseConcepts2(obs.concepts)) {
19803
19931
  if (!concept.startsWith("plugin:"))
19804
19932
  continue;
19805
19933
  const plugin = concept.slice("plugin:".length);
@@ -19828,7 +19956,7 @@ function getToolMemoryIndex(db, input = {}) {
19828
19956
  }
19829
19957
 
19830
19958
  // src/tools/session-tool-memory.ts
19831
- function parseConcepts2(value) {
19959
+ function parseConcepts3(value) {
19832
19960
  if (!value)
19833
19961
  return [];
19834
19962
  try {
@@ -19860,7 +19988,7 @@ function getSessionToolMemory(db, input) {
19860
19988
  }, new Map).entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
19861
19989
  const sampleTitles = groupedObservations.map((obs) => obs.title).filter((title, index, all) => all.indexOf(title) === index).slice(0, 4);
19862
19990
  const topPlugins = Array.from(groupedObservations.reduce((acc, obs) => {
19863
- for (const concept of parseConcepts2(obs.concepts)) {
19991
+ for (const concept of parseConcepts3(obs.concepts)) {
19864
19992
  if (!concept.startsWith("plugin:"))
19865
19993
  continue;
19866
19994
  const plugin = concept.slice("plugin:".length);
@@ -19916,6 +20044,13 @@ function getSessionContext(db, input) {
19916
20044
  user_id: input.user_id,
19917
20045
  limit: 8
19918
20046
  });
20047
+ const recentActivity = getRecentActivity(db, {
20048
+ cwd,
20049
+ project_scoped: true,
20050
+ user_id: input.user_id,
20051
+ limit: 12
20052
+ });
20053
+ const recentInboxNotes = recentActivity.observations.filter((obs) => obs.message_kind === "inbox-note").slice(0, 5);
19919
20054
  const recentChatMessages = recentChat.messages.length;
19920
20055
  const recallIndex = listRecallItems(db, {
19921
20056
  cwd,
@@ -19946,11 +20081,13 @@ function getSessionContext(db, input) {
19946
20081
  key: item.key,
19947
20082
  kind: item.kind,
19948
20083
  freshness: item.freshness,
19949
- title: item.title
20084
+ title: item.title,
20085
+ source_agent: item.source_agent
19950
20086
  })),
19951
20087
  best_recall_key: bestRecallItem?.key ?? null,
19952
20088
  best_recall_title: bestRecallItem?.title ?? null,
19953
20089
  best_recall_kind: bestRecallItem?.kind ?? null,
20090
+ best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
19954
20091
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
19955
20092
  resume_source_session_id: latestSession?.session_id ?? null,
19956
20093
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -19964,6 +20101,8 @@ function getSessionContext(db, input) {
19964
20101
  rolling_handoff_drafts: rollingHandoffDrafts,
19965
20102
  saved_handoffs: savedHandoffs,
19966
20103
  latest_handoff_title: latestHandoffTitle,
20104
+ recent_inbox_notes: recentInboxNotes.length,
20105
+ latest_inbox_note_title: recentInboxNotes[0]?.title ?? null,
19967
20106
  recent_chat_messages: recentChatMessages,
19968
20107
  recent_chat_sessions: recentChat.session_count,
19969
20108
  chat_source_summary: recentChat.source_summary,
@@ -20053,6 +20192,7 @@ function loadRecallItem(db, input) {
20053
20192
  detail: "Malformed recall key",
20054
20193
  session_id: null,
20055
20194
  source_device_id: null,
20195
+ source_agent: null,
20056
20196
  payload: null
20057
20197
  };
20058
20198
  }
@@ -20074,6 +20214,7 @@ function loadRecallItem(db, input) {
20074
20214
  detail: summarizeNarrative(result.handoff.narrative),
20075
20215
  session_id: result.handoff.session_id ?? null,
20076
20216
  source_device_id: result.handoff.device_id ?? null,
20217
+ source_agent: result.handoff.session_id ? lookupSessionAgent(db, result.handoff.session_id) : null,
20077
20218
  payload: {
20078
20219
  type: "handoff",
20079
20220
  handoff_id: result.handoff.id,
@@ -20093,6 +20234,7 @@ function loadRecallItem(db, input) {
20093
20234
  detail: story.summary?.next_steps ?? story.summary?.completed ?? null,
20094
20235
  session_id: story.session.session_id,
20095
20236
  source_device_id: story.session.device_id ?? null,
20237
+ source_agent: story.session.agent ?? null,
20096
20238
  payload: {
20097
20239
  type: "thread",
20098
20240
  latest_request: story.latest_request,
@@ -20122,6 +20264,7 @@ function loadRecallItem(db, input) {
20122
20264
  detail: message.content,
20123
20265
  session_id: message.session_id,
20124
20266
  source_device_id: message.device_id ?? null,
20267
+ source_agent: message.agent ?? null,
20125
20268
  payload: {
20126
20269
  type: "chat",
20127
20270
  role: message.role,
@@ -20147,6 +20290,7 @@ function loadRecallItem(db, input) {
20147
20290
  detail: obs.narrative ?? obs.facts ?? null,
20148
20291
  session_id: obs.session_id ?? null,
20149
20292
  source_device_id: obs.device_id ?? null,
20293
+ source_agent: obs.session_id ? lookupSessionAgent(db, obs.session_id) : obs.agent?.startsWith("engrm-") ? null : obs.agent ?? null,
20150
20294
  payload: {
20151
20295
  type: "memory",
20152
20296
  observation_id: obs.id,
@@ -20163,6 +20307,10 @@ function summarizeNarrative(value) {
20163
20307
  return null;
20164
20308
  return value.split(/\n+/).map((line) => line.trim()).find(Boolean) ?? null;
20165
20309
  }
20310
+ function lookupSessionAgent(db, sessionId) {
20311
+ const row = db.db.query("SELECT agent FROM sessions WHERE session_id = ? LIMIT 1").get(sessionId);
20312
+ return row?.agent ?? null;
20313
+ }
20166
20314
  function missing(key, kind) {
20167
20315
  return {
20168
20316
  kind,
@@ -20171,6 +20319,7 @@ function missing(key, kind) {
20171
20319
  detail: "Recall item not found",
20172
20320
  session_id: null,
20173
20321
  source_device_id: null,
20322
+ source_agent: null,
20174
20323
  payload: null
20175
20324
  };
20176
20325
  }
@@ -20561,7 +20710,12 @@ async function repairRecall(db, config2, input = {}) {
20561
20710
  let sessionsWithImports = 0;
20562
20711
  for (const session of targetSessions) {
20563
20712
  const sessionCwd = session.project_id !== null ? db.getProjectById(session.project_id)?.local_path ?? cwd : cwd;
20564
- const syncResult = await syncTranscriptChat(db, config2, session.session_id, sessionCwd, input.transcript_path);
20713
+ let syncResult = { imported: 0, total: 0 };
20714
+ try {
20715
+ syncResult = await syncTranscriptChat(db, config2, session.session_id, sessionCwd, input.transcript_path);
20716
+ } catch {
20717
+ syncResult = { imported: 0, total: 0 };
20718
+ }
20565
20719
  const chatMessages = db.getSessionChatMessages(session.session_id, 200);
20566
20720
  const prompts = db.getSessionUserPrompts(session.session_id, 200);
20567
20721
  const sourceSummary = summarizeChatSources(chatMessages);
@@ -20597,6 +20751,9 @@ async function resumeThread(db, config2, input = {}) {
20597
20751
  const detected = detectProject(cwd);
20598
20752
  const project = db.getProjectByCanonicalId(detected.canonical_id);
20599
20753
  let snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20754
+ if (input.agent) {
20755
+ snapshot = filterResumeSnapshotByAgent(snapshot, input.agent, input.current_device_id);
20756
+ }
20600
20757
  let repairResult = null;
20601
20758
  const shouldRepair = repairIfNeeded && snapshot.recentChat.coverage_state !== "transcript-backed" && (snapshot.recentChat.messages.length > 0 || snapshot.recentSessions.length > 0 || snapshot.context?.continuity_state !== "cold");
20602
20759
  if (shouldRepair) {
@@ -20607,6 +20764,9 @@ async function resumeThread(db, config2, input = {}) {
20607
20764
  });
20608
20765
  if (repairResult.imported_chat_messages > 0) {
20609
20766
  snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20767
+ if (input.agent) {
20768
+ snapshot = filterResumeSnapshotByAgent(snapshot, input.agent, input.current_device_id);
20769
+ }
20610
20770
  }
20611
20771
  }
20612
20772
  const { context, handoff, recentChat, recentSessions, recall } = snapshot;
@@ -20647,6 +20807,7 @@ async function resumeThread(db, config2, input = {}) {
20647
20807
  ])).slice(0, 4);
20648
20808
  return {
20649
20809
  project_name: project?.name ?? context?.project_name ?? null,
20810
+ target_agent: input.agent ?? null,
20650
20811
  continuity_state: context?.continuity_state ?? "cold",
20651
20812
  continuity_summary: context?.continuity_summary ?? "No fresh repo-local continuity yet; older memory should be treated cautiously.",
20652
20813
  resume_freshness: classifyResumeFreshness2(sourceTimestamp),
@@ -20696,6 +20857,13 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20696
20857
  user_id: userId,
20697
20858
  current_device_id: currentDeviceId
20698
20859
  });
20860
+ const recentHandoffs = getRecentHandoffs(db, {
20861
+ cwd,
20862
+ project_scoped: true,
20863
+ user_id: userId,
20864
+ current_device_id: currentDeviceId,
20865
+ limit: Math.max(limit, 4)
20866
+ }).handoffs;
20699
20867
  const recentChat = getRecentChat(db, {
20700
20868
  cwd,
20701
20869
  project_scoped: true,
@@ -20725,15 +20893,50 @@ async function buildResumeSnapshot(db, cwd, userId, currentDeviceId, limit) {
20725
20893
  return {
20726
20894
  context,
20727
20895
  handoff: handoffResult.handoff,
20896
+ recentHandoffs,
20728
20897
  recentChat,
20729
20898
  recentSessions,
20730
20899
  recall,
20731
20900
  recallIndex
20732
20901
  };
20733
20902
  }
20903
+ function filterResumeSnapshotByAgent(snapshot, agent, currentDeviceId) {
20904
+ const recentSessions = snapshot.recentSessions.filter((session) => session.agent === agent);
20905
+ const sessionIds = new Set(recentSessions.map((session) => session.session_id));
20906
+ const recentChatMessages = snapshot.recentChat.messages.filter((message) => message.agent === agent);
20907
+ 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;
20908
+ const recallIndex = {
20909
+ ...snapshot.recallIndex,
20910
+ items: snapshot.recallIndex.items.filter((item) => item.source_agent === agent)
20911
+ };
20912
+ const recall = {
20913
+ ...snapshot.recall,
20914
+ results: snapshot.recall.results.filter((entry) => entry.session_id ? sessionIds.has(entry.session_id) : false)
20915
+ };
20916
+ return {
20917
+ ...snapshot,
20918
+ handoff,
20919
+ recentHandoffs: snapshot.recentHandoffs.filter((item) => item.session_id && sessionIds.has(item.session_id)),
20920
+ recentSessions,
20921
+ recentChat: {
20922
+ ...snapshot.recentChat,
20923
+ messages: recentChatMessages,
20924
+ session_count: new Set(recentChatMessages.map((message) => message.session_id)).size
20925
+ },
20926
+ recall,
20927
+ recallIndex
20928
+ };
20929
+ }
20734
20930
  function pickBestRecallItem2(items) {
20735
20931
  return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
20736
20932
  }
20933
+ function compareRecallCandidates(epochA, epochB, deviceA, deviceB, currentDeviceId) {
20934
+ const remoteBoostA = currentDeviceId && deviceA && deviceA !== currentDeviceId ? 1 : 0;
20935
+ const remoteBoostB = currentDeviceId && deviceB && deviceB !== currentDeviceId ? 1 : 0;
20936
+ if (remoteBoostA !== remoteBoostB)
20937
+ return remoteBoostB - remoteBoostA;
20938
+ return epochB - epochA;
20939
+ }
20737
20940
  function extractCurrentThread(handoff) {
20738
20941
  const narrative = handoff?.narrative ?? "";
20739
20942
  const match = narrative.match(/Current thread:\s*(.+)/i);
@@ -20976,8 +21179,9 @@ function hasContent(value) {
20976
21179
  // src/tools/stats.ts
20977
21180
  function getMemoryStats(db) {
20978
21181
  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;
21182
+ const handoffs = db.db.query(`SELECT COUNT(*) as count FROM observations
21183
+ WHERE ${getHandoffMessageFilterSql({ include_aging: true })}`).get()?.count ?? 0;
21184
+ const inboxMessages = getInboxMessageCount(db);
20981
21185
  const userPrompts = db.db.query("SELECT COUNT(*) as count FROM user_prompts").get()?.count ?? 0;
20982
21186
  const toolEvents = db.db.query("SELECT COUNT(*) as count FROM tool_events").get()?.count ?? 0;
20983
21187
  const sessionSummaries = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
@@ -20991,7 +21195,9 @@ function getMemoryStats(db) {
20991
21195
  active_observations: activeObservations,
20992
21196
  user_prompts: userPrompts,
20993
21197
  tool_events: toolEvents,
20994
- messages,
21198
+ messages: inboxMessages,
21199
+ inbox_messages: inboxMessages,
21200
+ handoffs,
20995
21201
  session_summaries: sessionSummaries,
20996
21202
  decisions: signals.decisions_count,
20997
21203
  lessons: signals.lessons_count,
@@ -22665,7 +22871,7 @@ process.on("SIGTERM", () => {
22665
22871
  });
22666
22872
  var server = new McpServer({
22667
22873
  name: "engrm",
22668
- version: "0.4.34"
22874
+ version: "0.4.36"
22669
22875
  });
22670
22876
  server.tool("save_observation", "Save an observation to memory", {
22671
22877
  type: exports_external.enum([
@@ -23135,7 +23341,7 @@ server.tool("list_recall_items", "USE FIRST when continuity feels fuzzy. List th
23135
23341
  }
23136
23342
  const projectLine = result.project ? `Project: ${result.project}
23137
23343
  ` : "";
23138
- const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
23344
+ 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
23345
  ${item.detail}`).join(`
23140
23346
  `);
23141
23347
  return {
@@ -23180,6 +23386,7 @@ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact rec
23180
23386
  ` + `Title: ${result.title}
23181
23387
  ` + `Session: ${result.session_id ?? "(unknown)"}
23182
23388
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23389
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23183
23390
 
23184
23391
  ` + `${result.payload.narrative ?? "(no narrative)"}`
23185
23392
  }
@@ -23199,6 +23406,7 @@ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact rec
23199
23406
  ` + `Title: ${result.title}
23200
23407
  ` + `Session: ${result.session_id ?? "(unknown)"}
23201
23408
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23409
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23202
23410
  ` + `Latest request: ${result.payload.latest_request ?? "(none)"}
23203
23411
  ` + `Current thread: ${result.payload.current_thread ?? "(none)"}
23204
23412
 
@@ -23220,6 +23428,7 @@ ${hotFiles}`
23220
23428
  ` + `Title: ${result.title}
23221
23429
  ` + `Session: ${result.session_id ?? "(unknown)"}
23222
23430
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23431
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23223
23432
 
23224
23433
  ` + `${result.payload.content}`
23225
23434
  }
@@ -23234,6 +23443,7 @@ ${hotFiles}`
23234
23443
  ` + `Title: ${result.title}
23235
23444
  ` + `Session: ${result.session_id ?? "(unknown)"}
23236
23445
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23446
+ ` + `Agent: ${result.source_agent ?? "(unknown)"}
23237
23447
  ` + `Type: ${result.payload.observation_type}
23238
23448
 
23239
23449
  ` + `${result.payload.narrative ?? result.payload.facts ?? "(no detail)"}`
@@ -23244,12 +23454,14 @@ ${hotFiles}`
23244
23454
  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
23455
  cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
23246
23456
  limit: exports_external.number().optional().describe("Max recall hits/chat snippets to include"),
23457
+ agent: exports_external.string().optional().describe("Optional agent to resume specifically, such as claude-code or codex-cli"),
23247
23458
  user_id: exports_external.string().optional().describe("Optional user override"),
23248
23459
  repair_if_needed: exports_external.boolean().optional().describe("If true, attempt recall repair before resuming when continuity is still weak")
23249
23460
  }, async (params) => {
23250
23461
  const result = await resumeThread(db, config2, {
23251
23462
  cwd: params.cwd ?? process.cwd(),
23252
23463
  limit: params.limit,
23464
+ agent: params.agent,
23253
23465
  user_id: params.user_id ?? config2.user_id,
23254
23466
  current_device_id: config2.device_id,
23255
23467
  repair_if_needed: params.repair_if_needed
@@ -23291,7 +23503,8 @@ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?
23291
23503
  content: [
23292
23504
  {
23293
23505
  type: "text",
23294
- text: `${projectLine}` + `Continuity: ${result.continuity_state} ${result.continuity_summary}
23506
+ text: `${projectLine}` + `${result.target_agent ? `Target agent: ${result.target_agent}
23507
+ ` : ""}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23295
23508
  ` + `Freshness: ${result.resume_freshness}
23296
23509
  ` + `Source: ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23297
23510
  ` + `Resume confidence: ${result.resume_confidence}
@@ -23450,13 +23663,7 @@ server.tool("check_messages", "Check for messages sent from other devices or ses
23450
23663
  const markRead = params.mark_read !== false;
23451
23664
  const readKey = `messages_read_${config2.device_id}`;
23452
23665
  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);
23666
+ const messages = getUnreadInboxMessages(db, config2.device_id, config2.user_id, lastReadId, 20);
23460
23667
  if (messages.length === 0) {
23461
23668
  return {
23462
23669
  content: [{ type: "text", text: "No new messages." }]
@@ -23503,7 +23710,7 @@ server.tool("send_message", "Leave a cross-device or team note in Engrm's shared
23503
23710
  ]
23504
23711
  };
23505
23712
  });
23506
- server.tool("recent_activity", "Inspect the most recent observations captured by Engrm", {
23713
+ server.tool("recent_activity", "Inspect the most recent observations, notes, and handoffs captured by Engrm", {
23507
23714
  limit: exports_external.number().optional().describe("Max observations to return (default: 10)"),
23508
23715
  project_scoped: exports_external.boolean().optional().describe("Scope to current project (default: true)"),
23509
23716
  type: exports_external.string().optional().describe("Optional observation type filter")
@@ -23526,12 +23733,21 @@ server.tool("recent_activity", "Inspect the most recent observations captured by
23526
23733
  const showProject = !result.project;
23527
23734
  const header = showProject ? "| ID | Project | Type | Title | Created |" : "| ID | Type | Title | Created |";
23528
23735
  const separator = showProject ? "|---|---|---|---|---|" : "|---|---|---|---|";
23736
+ const displayType = (obs) => {
23737
+ if (obs.message_kind === "draft-handoff")
23738
+ return "handoff:draft";
23739
+ if (obs.message_kind === "handoff")
23740
+ return "handoff";
23741
+ if (obs.message_kind === "inbox-note")
23742
+ return "note";
23743
+ return obs.type;
23744
+ };
23529
23745
  const rows = result.observations.map((obs) => {
23530
23746
  const date5 = obs.created_at.split("T")[0];
23531
23747
  if (showProject) {
23532
- return `| ${obs.id} | ${obs.project_name ?? "(unknown)"} | ${obs.type} | ${obs.title} | ${date5} |`;
23748
+ return `| ${obs.id} | ${obs.project_name ?? "(unknown)"} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23533
23749
  }
23534
- return `| ${obs.id} | ${obs.type} | ${obs.title} | ${date5} |`;
23750
+ return `| ${obs.id} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23535
23751
  });
23536
23752
  const projectLine = result.project ? `Project: ${result.project}
23537
23753
  ` : "";
@@ -23567,7 +23783,8 @@ server.tool("memory_stats", "Show high-level Engrm capture and sync statistics",
23567
23783
  text: `Active observations: ${stats.active_observations}
23568
23784
  ` + `User prompts: ${stats.user_prompts}
23569
23785
  ` + `Tool events: ${stats.tool_events}
23570
- ` + `Messages: ${stats.messages}
23786
+ ` + `Inbox notes: ${stats.inbox_messages}
23787
+ ` + `Handoffs: ${stats.handoffs}
23571
23788
  ` + `Session summaries: ${stats.session_summaries}
23572
23789
  ` + `Summary coverage: learned ${stats.summaries_with_learned}, completed ${stats.summaries_with_completed}, next steps ${stats.summaries_with_next_steps}
23573
23790
  ` + `Installed packs: ${packs}
@@ -23611,6 +23828,8 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23611
23828
  }).join(`
23612
23829
  `) : "- (none)";
23613
23830
  const handoffLines = result.recent_handoffs.length > 0 ? result.recent_handoffs.map((obs) => `- #${obs.id} ${obs.title}`).join(`
23831
+ `) : "- (none)";
23832
+ const inboxNoteLines = result.recent_inbox_notes.length > 0 ? result.recent_inbox_notes.map((obs) => `- #${obs.id} ${obs.title}`).join(`
23614
23833
  `) : "- (none)";
23615
23834
  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
23835
  `) : "- (none)";
@@ -23625,13 +23844,13 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23625
23844
  `) : "- (none)";
23626
23845
  const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
23627
23846
  `) : "- (none)";
23628
- const provenanceMixLines2 = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
23847
+ const provenanceMixLines = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
23629
23848
  `) : "- (none)";
23630
23849
  const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
23631
23850
  `) : "- (none)";
23632
23851
  const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
23633
23852
  `) : "- (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(`
23853
+ 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
23854
  `) : "- (none)";
23636
23855
  const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23637
23856
  ` : "";
@@ -23654,10 +23873,11 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
23654
23873
  ` + `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
23874
  ` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
23656
23875
  ` : ""}` + `Handoffs: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
23876
+ ` + `Inbox notes: ${result.recent_inbox_notes.length}${result.latest_inbox_note_title ? ` · latest "${result.latest_inbox_note_title}"` : ""}
23657
23877
  ` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
23658
23878
  ` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23659
23879
 
23660
- ` + openExactLine + `Recall preview:
23880
+ ` + openExactLine + resumeAgentLine + `Recall preview:
23661
23881
  ${recallPreviewLines}
23662
23882
 
23663
23883
  ` + `Next actions:
@@ -23679,6 +23899,9 @@ ${sessionLines}
23679
23899
  ` + `Recent handoffs:
23680
23900
  ${handoffLines}
23681
23901
 
23902
+ ` + `Recent inbox notes:
23903
+ ${inboxNoteLines}
23904
+
23682
23905
  ` + `Recent requests:
23683
23906
  ${requestLines}
23684
23907
 
@@ -23741,6 +23964,8 @@ server.tool("capture_quality", "Show how healthy Engrm capture is across the wor
23741
23964
  const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
23742
23965
  `) : "- (none)";
23743
23966
  const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
23967
+ `) : "- (none)";
23968
+ const provenanceMixLines = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
23744
23969
  `) : "- (none)";
23745
23970
  const projectLines = result.top_projects.length > 0 ? result.top_projects.map((project) => `- ${project.name} [${project.raw_capture_state}] obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count} checkpoints=${project.assistant_checkpoint_count} chat=${project.chat_message_count} (${project.chat_coverage_state})`).join(`
23746
23971
  `) : "- (none)";
@@ -23785,7 +24010,8 @@ server.tool("agent_memory_index", "Compare continuity and capture health across
23785
24010
  const lastSeen = agent.last_seen_epoch ? new Date(agent.last_seen_epoch * 1000).toISOString().replace("T", " ").slice(0, 16) : "unknown";
23786
24011
  const latest = agent.latest_summary ? ` latest="${agent.latest_summary.replace(/\s+/g, " ").trim().slice(0, 120)}"` : "";
23787
24012
  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}`;
24013
+ const exact = agent.best_recall_key ? ` open=load_recall_item("${agent.best_recall_key}")` : "";
24014
+ 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
24015
  }).join(`
23790
24016
  `) : "- (none)";
23791
24017
  return {
@@ -23891,7 +24117,8 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23891
24117
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23892
24118
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
23893
24119
  ` + `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})` : ""}
24120
+ ` + `${result.best_agent_resume_agent ? `Resume agent: resume_thread(agent="${result.best_agent_resume_agent}")
24121
+ ` : ""}` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
23895
24122
  ` + `Loaded observations: ${result.session_count}
23896
24123
  ` + `Searchable total: ${result.total_active}
23897
24124
  ` + `Recent requests: ${result.recent_requests}
@@ -23899,6 +24126,7 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
23899
24126
  ` + `Recent sessions: ${result.recent_sessions}
23900
24127
  ` + `Recent handoffs: ${result.recent_handoffs}
23901
24128
  ` + `Handoff split: ${result.saved_handoffs} saved, ${result.rolling_handoff_drafts} rolling drafts
24129
+ ` + `Recent inbox notes: ${result.recent_inbox_notes}${result.latest_inbox_note_title ? ` · latest "${result.latest_inbox_note_title}"` : ""}
23902
24130
  ` + `Recent chat messages: ${result.recent_chat_messages}
23903
24131
  ` + `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
24132
  ` + `Latest handoff: ${result.latest_handoff_title ?? "(none)"}
@@ -23969,7 +24197,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23969
24197
  `) : "- (none)";
23970
24198
  const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
23971
24199
  `) : "- (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(`
24200
+ 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
24201
  `) : "- (none)";
23974
24202
  const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23975
24203
  ` : "";
@@ -23988,6 +24216,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23988
24216
 
23989
24217
  ` + `Recent handoffs captured: ${result.recent_handoffs_count}
23990
24218
  ` + `Handoff split: ${result.saved_handoffs_count} saved, ${result.rolling_handoff_drafts_count} rolling drafts
24219
+ ` + `Recent inbox notes: ${result.recent_inbox_notes_count}${result.latest_inbox_note_title ? ` · latest "${result.latest_inbox_note_title}"` : ""}
23991
24220
  ` + `Recent chat messages captured: ${result.recent_chat_count}
23992
24221
  ` + `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
24222
 
@@ -23997,7 +24226,7 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
23997
24226
  ` + `Estimated read cost: ~${result.estimated_read_tokens}t
23998
24227
  ` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
23999
24228
 
24000
- ` + openExactLine + `Recall preview:
24229
+ ` + openExactLine + resumeAgentLine + `Recall preview:
24001
24230
  ${recallPreviewLines}
24002
24231
 
24003
24232
  ` + `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.36",
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",