cogmem 3.7.0 → 3.7.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MEMORY_ATLAS.md +16 -6
  3. package/README.md +16 -11
  4. package/RELEASE_CHECKLIST.md +4 -4
  5. package/dist/agent/AgentMemoryBackend.d.ts +11 -2
  6. package/dist/agent/AgentMemoryBackend.js +185 -36
  7. package/dist/agent/AgentRecallQueryCompiler.d.ts +1 -1
  8. package/dist/agent/AgentRecallQueryCompiler.js +13 -1
  9. package/dist/agent/MemoryUsageReceipt.js +8 -10
  10. package/dist/agent/SessionWorkingState.js +3 -2
  11. package/dist/agent/UntrustedMemorySerializer.d.ts +2 -0
  12. package/dist/agent/UntrustedMemorySerializer.js +14 -0
  13. package/dist/atlas/ActionFrameExtractor.js +2 -0
  14. package/dist/atlas/EpisodeTitleGenerator.js +42 -3
  15. package/dist/atlas/FacetQueryPlanner.d.ts +2 -0
  16. package/dist/atlas/FacetQueryPlanner.js +57 -16
  17. package/dist/atlas/GraphCurator.d.ts +3 -0
  18. package/dist/atlas/GraphCurator.js +118 -14
  19. package/dist/atlas/MemoryAtlasIndexer.d.ts +12 -0
  20. package/dist/atlas/MemoryAtlasIndexer.js +36 -0
  21. package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
  22. package/dist/atlas/MemoryAtlasService.js +66 -22
  23. package/dist/bin/memory.js +14 -4
  24. package/dist/factory.d.ts +18 -0
  25. package/dist/factory.js +41 -2
  26. package/dist/host/openclaw/AutoMemoryPluginInstaller.js +71 -44
  27. package/dist/mcp/server.js +1 -1
  28. package/dist/recall/RecallExplanation.d.ts +1 -1
  29. package/dist/store/MemoryAtlasStore.d.ts +3 -1
  30. package/dist/store/MemoryAtlasStore.js +149 -25
  31. package/dist/utils/ActionKindRegistry.d.ts +10 -0
  32. package/dist/utils/ActionKindRegistry.js +18 -0
  33. package/dist/utils/EntityCueExtractor.d.ts +13 -0
  34. package/dist/utils/EntityCueExtractor.js +81 -0
  35. package/examples/hermes-backend/AGENTS.md +1 -1
  36. package/examples/hermes-backend/README.md +1 -1
  37. package/examples/hermes-backend/SKILL.md +10 -3
  38. package/examples/hermes-backend/references/operations.md +9 -8
  39. package/examples/openclaw-backend/AGENTS.md +1 -1
  40. package/examples/openclaw-backend/README.md +2 -2
  41. package/examples/openclaw-backend/SKILL.md +24 -7
  42. package/examples/openclaw-backend/references/operations.md +26 -10
  43. package/package.json +1 -1
package/dist/factory.js CHANGED
@@ -83,7 +83,7 @@ import { SqliteVecStore } from './store/SqliteVecStore.js';
83
83
  import { VectorStore } from './store/VectorStore.js';
84
84
  import { config } from './utils/Config.js';
85
85
  import { KernelRunningError, SnapshotExporter, SnapshotImporter, } from './snapshot/index.js';
86
- const CORE_VERSION = '3.7.0';
86
+ const CORE_VERSION = '3.7.1';
87
87
  const LATEST_SCHEMA_VERSION = 27;
88
88
  export class MemoryKernel {
89
89
  options;
@@ -1056,6 +1056,7 @@ export class MemoryKernel {
1056
1056
  const affected = new Set();
1057
1057
  const before = {};
1058
1058
  const previousStatuses = new Map();
1059
+ let requeuedDream = input.operation === 'requeue-dream' || input.operation === 'invalidate-dream-run';
1059
1060
  if (input.operation === 'move-event') {
1060
1061
  const link = this.episodeStore.getEventLink(input.eventId);
1061
1062
  const source = link ? this.episodeStore.getEpisode(link.episodeId) : undefined;
@@ -1169,11 +1170,30 @@ export class MemoryKernel {
1169
1170
  const episode = this.episodeStore.getEpisode(episodeId);
1170
1171
  if (episode?.eventCount && episode.status === 'sealed') {
1171
1172
  this.episodeStore.requeueDreamForRepair(episodeId, input.operation === 'invalidate-dream-run' ? input.mode || 'normal' : 'normal', now);
1173
+ requeuedDream = true;
1172
1174
  }
1173
1175
  }
1174
1176
  const after = Object.fromEntries([...affected].map((episodeId) => [episodeId, this.episodeStore.getEpisode(episodeId)]));
1175
1177
  const repairId = this.episodeStore.recordRepairAudit({ projectId: input.projectId, operation: input.operation, payload: input, before, after, now });
1176
- return { repairId, operation: input.operation, affectedEpisodeIds: [...affected], staleCandidateIds };
1178
+ const affectedEpisodeIds = [...affected];
1179
+ const nextCommands = affectedEpisodeIds.length
1180
+ ? affectedEpisodeIds.map((episodeId) => `cogmem memory graph-reindex --project ${input.projectId} --episode ${episodeId} --json`)
1181
+ : [];
1182
+ return {
1183
+ repairId,
1184
+ applied: true,
1185
+ operation: input.operation,
1186
+ affectedEpisodeIds,
1187
+ changedFields: changedFieldsForRepair(input),
1188
+ staleCandidateIds,
1189
+ requeuedDream,
1190
+ graphRefreshNeeded: affectedEpisodeIds.length > 0,
1191
+ nextCommands: [
1192
+ ...nextCommands,
1193
+ `cogmem memory graph-explore --project ${input.projectId} --query "<query>" --json`,
1194
+ ],
1195
+ note: 'repairId is an audit id, not a dream candidate id; do not run memory dream --promote just because repairId exists.',
1196
+ };
1177
1197
  }
1178
1198
  listDreamCandidates(options = {}) {
1179
1199
  return this.deepWriteCandidateStore.listCandidates(options);
@@ -1265,6 +1285,9 @@ export class MemoryKernel {
1265
1285
  rebuildMemoryAtlas(options = {}) {
1266
1286
  return this.memoryAtlasIndexer.rebuild(options);
1267
1287
  }
1288
+ reindexMemoryAtlas(options) {
1289
+ return this.memoryAtlasIndexer.reindex(options);
1290
+ }
1268
1291
  ensureMemoryAtlas(options) {
1269
1292
  return this.memoryAtlasIndexer.ensureFresh(options);
1270
1293
  }
@@ -2002,6 +2025,22 @@ export function createMemoryKernelFromConfig(input = {}) {
2002
2025
  const { configPath: _configPath, cwd: _cwd, env: _env, ...explicitOptions } = options;
2003
2026
  return createMemoryKernel({ ...loaded.options, ...explicitOptions });
2004
2027
  }
2028
+ function changedFieldsForRepair(input) {
2029
+ if (input.operation === 'reclassify') {
2030
+ return [
2031
+ input.episodeType ? 'episodeType' : '',
2032
+ input.topicPath ? 'topicPath' : '',
2033
+ input.importance !== undefined ? 'importance' : '',
2034
+ ].filter(Boolean);
2035
+ }
2036
+ if (input.operation === 'move-event')
2037
+ return ['episodeEvents'];
2038
+ if (input.operation === 'split' || input.operation === 'merge')
2039
+ return ['episodeEvents', 'crossRefs'];
2040
+ if (input.operation === 'requeue-dream' || input.operation === 'invalidate-dream-run')
2041
+ return ['dreamQueue'];
2042
+ return [];
2043
+ }
2005
2044
  function requiredGovernancePayloadString(payload, field) {
2006
2045
  const value = payload[field];
2007
2046
  if (typeof value !== 'string' || value.trim() === '')
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { resolveCogmemConfigPath } from '../../config/CogmemConfig.js';
7
7
  const PLUGIN_ID = 'cogmem-auto-memory';
8
- const PLUGIN_VERSION = '0.7.0';
8
+ const PLUGIN_VERSION = '0.7.1';
9
9
  function defaultPublicEntrypoint() {
10
10
  return join(resolve(dirname(fileURLToPath(import.meta.url)), '../..'), 'public.js');
11
11
  }
@@ -516,15 +516,29 @@ function createMemoryUsageReceipt(input) {
516
516
 
517
517
  function formatSourceAnchor(anchor) {
518
518
  return [
519
- anchor.memoryId ? 'memory:' + anchor.memoryId : '',
520
- anchor.eventId ? 'event:' + anchor.eventId : '',
521
- anchor.sessionId ? 'session:' + anchor.sessionId : '',
522
- anchor.role ? 'role:' + anchor.role : '',
519
+ anchor.memoryId ? 'memory:' + serializeUntrustedMemory(anchor.memoryId, 160) : '',
520
+ anchor.eventId ? 'event:' + serializeUntrustedMemory(anchor.eventId, 160) : '',
521
+ anchor.sessionId ? 'session:' + serializeUntrustedMemory(anchor.sessionId, 160) : '',
522
+ anchor.role ? 'role:' + serializeUntrustedMemory(anchor.role, 80) : '',
523
523
  ].filter(Boolean).join('; ');
524
524
  }
525
525
 
526
526
  function listLines(values) {
527
- return values.length ? values.map((value) => '- ' + value) : ['- none'];
527
+ return values.length ? values.map((value) => '- ' + serializeUntrustedMemory(value, 260)) : ['- none'];
528
+ }
529
+
530
+ function serializeUntrustedMemory(input, limit) {
531
+ const clean = stripCogmemRecallBlocks(String(input || '')).text
532
+ .replace(new RegExp('</?COGMEM_[A-Z0-9_:-]+[^>]*>', 'gi'), '')
533
+ .replace(new RegExp('[\\\\u0000-\\\\u0008\\\\u000b\\\\u000c\\\\u000e-\\\\u001f\\\\u007f]', 'g'), ' ')
534
+ .replace(new RegExp('\\\\s+', 'g'), ' ')
535
+ .trim()
536
+ .replace(/&/g, '&amp;')
537
+ .replace(/</g, '&lt;')
538
+ .replace(/>/g, '&gt;')
539
+ .replace(/"/g, '&quot;')
540
+ .replace(/'/g, '&#39;');
541
+ return clean.slice(0, Math.max(0, Number(limit || 500)));
528
542
  }
529
543
 
530
544
  function clampBlock(text, closingTag, maxChars) {
@@ -535,14 +549,14 @@ function clampBlock(text, closingTag, maxChars) {
535
549
 
536
550
  function formatMemoryUsageBridge(receipt, maxChars) {
537
551
  const lines = [
538
- '<COGMEM_TURN_BRIDGE turn_id="' + String(receipt.turnId).replace(/"/g, '&quot;') + '" source="cogmem" compact="true" ttl_turns="' + receipt.ttlTurns + '" compile_allowed="false">',
552
+ '<COGMEM_TURN_BRIDGE turn_id="' + serializeUntrustedMemory(receipt.turnId, 180) + '" source="cogmem" compact="true" ttl_turns="' + receipt.ttlTurns + '" compile_allowed="false">',
539
553
  'Previous assistant answer used Cogmem memory.',
540
554
  '',
541
555
  'Used memory themes:',
542
556
  ...listLines(receipt.usedThemes || []),
543
557
  '',
544
558
  'Working conclusion produced in that turn:',
545
- '- ' + (receipt.workingConclusion || 'No compact conclusion recorded.'),
559
+ '- ' + serializeUntrustedMemory(receipt.workingConclusion || 'No compact conclusion recorded.', 260),
546
560
  '',
547
561
  'Source anchors:',
548
562
  ...listLines((receipt.sourceAnchors || []).map(formatSourceAnchor)),
@@ -608,7 +622,7 @@ function formatSessionWorkingState(state, maxChars) {
608
622
  const lines = [
609
623
  '<COGMEM_SESSION_STATE scope="current_session" compact="true" persistence="session_only" compile_allowed="false">',
610
624
  'Current working topic:',
611
- '- ' + (state.currentTopic || 'unspecified'),
625
+ '- ' + serializeUntrustedMemory(state.currentTopic || 'unspecified', 180),
612
626
  '',
613
627
  'Current design direction:',
614
628
  ...listLines(state.designDirection || []),
@@ -663,6 +677,9 @@ function classifyRecallIntent(query) {
663
677
  if (/原话|怎么说的|完整对话|上一句|下一句|exact quote|verbatim/.test(text)) {
664
678
  return 'forensic_quote';
665
679
  }
680
+ if (/(让你.{0,20}(对|给|把)?.{0,20}(做过|做了|启动|安装|配置|修改|重启|停止|操作|处理|执行)|对.{0,30}(做过什么|做了什么|哪些操作)|what did (i ask you to do|you do) to|operations? on)/i.test(text)) {
681
+ return 'action_history';
682
+ }
666
683
  if (/记得.{0,12}(聊过|讨论过|说过)|还记得|之前.{0,12}(聊过|讨论过|说过)|以前.{0,12}(聊过|讨论过|说过)|(过去|几个月前|半年前|上个月|前几天|昨天|上次|上个).{0,20}(聊过|讨论过|说过)|当时.{0,12}(聊|说|问)|那次.{0,12}(聊|说|问)|有没有.{0,12}(记录|聊过|讨论过)|have we discussed|did we talk about|previously discussed/.test(text)) {
667
684
  return 'historical_discussion';
668
685
  }
@@ -1314,7 +1331,7 @@ async function recallPayload(input, config, kernel, memory, formatStrategyContex
1314
1331
  let result = await memory.recall({
1315
1332
  agentId: config.agentId || 'openclaw', projectId: config.projectId || 'openclaw',
1316
1333
  query: input.query || '', sessionId: input.sessionId, threadId: input.threadId,
1317
- excludeSessionId: input.excludeSessionId, intent: input.intent || 'memory_recall',
1334
+ excludeSessionId: input.excludeSessionId, intent: input.intent || undefined,
1318
1335
  anchorEventId: input.anchorEventId, anchorText: input.anchorText,
1319
1336
  limit: Number(config.limit || 3), retrievalPolicy: strategyCapsule && strategyCapsule.retrievalPolicy,
1320
1337
  });
@@ -1327,7 +1344,7 @@ async function recallPayload(input, config, kernel, memory, formatStrategyContex
1327
1344
  result = await memory.recall({
1328
1345
  agentId: config.agentId || 'openclaw', projectId: config.projectId || 'openclaw',
1329
1346
  query: input.query || '', sessionId: input.sessionId, threadId: input.threadId,
1330
- excludeSessionId: input.excludeSessionId, intent: input.intent || 'memory_recall',
1347
+ excludeSessionId: input.excludeSessionId, intent: input.intent || undefined,
1331
1348
  anchorEventId: input.anchorEventId, anchorText: input.anchorText,
1332
1349
  limit: Number(config.limit || 3), retrievalPolicy: strategyCapsule && strategyCapsule.retrievalPolicy,
1333
1350
  });
@@ -1368,7 +1385,7 @@ async function recallPayload(input, config, kernel, memory, formatStrategyContex
1368
1385
  return {
1369
1386
  context: recallContext ? (strategyCapsule ? formatStrategyContext(strategyCapsule) + '\n\n' : '') + recallContext : '',
1370
1387
  items: compactRecallItems(plannedResult.items, config), itemCount: plannedResult.items.length,
1371
- recallMode: result.recallMode, fallbackUsed: result.fallbackUsed, intent: input.intent || 'memory_recall',
1388
+ recallMode: result.recallMode, fallbackUsed: result.fallbackUsed, intent: (result.queryPlan && result.queryPlan.intent) || input.intent || 'memory_recall',
1372
1389
  anchorEventId: anchorItem && anchorItem.sourceAnchor && anchorItem.sourceAnchor.eventId,
1373
1390
  anchorText: anchorItem && anchorItem.text, queryPlan: result.queryPlan, decisionTrace: result.decisionTrace,
1374
1391
  atlasCards: result.atlasCards, selectedEpisodeCards,
@@ -1470,10 +1487,20 @@ function formatAtlasContext(result, maxChars) {
1470
1487
  }
1471
1488
 
1472
1489
  function safeAtlasText(input, limit) {
1490
+ return serializeUntrustedMemory(input, limit);
1491
+ }
1492
+
1493
+ function serializeUntrustedMemory(input, limit) {
1473
1494
  const clean = stripCogmemRecallBlocks(String(input || '')).text
1474
- .replace(/[<>]/g, (value) => value === '<' ? '\\u003c' : '\\u003e')
1495
+ .replace(/<\/?COGMEM_[A-Z0-9_:-]+[^>]*>/gi, '')
1496
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, ' ')
1475
1497
  .replace(/\s+/g, ' ')
1476
- .trim();
1498
+ .trim()
1499
+ .replace(/&/g, '&amp;')
1500
+ .replace(/</g, '&lt;')
1501
+ .replace(/>/g, '&gt;')
1502
+ .replace(/"/g, '&quot;')
1503
+ .replace(/'/g, '&#39;');
1477
1504
  return clean.slice(0, Math.max(0, Number(limit || 500)));
1478
1505
  }
1479
1506
 
@@ -1773,43 +1800,43 @@ function formatRecallContext(result, config) {
1773
1800
  lines.push('selectedMemoryCards=' + result.atlasCards.slice(0, Number(config.memoryContextMaxItems || config.limit || 3)).map(formatAtlasCard).join(' | '));
1774
1801
  }
1775
1802
  if (Array.isArray(result.relaxationTrace) && result.relaxationTrace.length) {
1776
- lines.push('relaxationTrace=' + result.relaxationTrace.map((step) => step.from + ' -> ' + step.to + ' (' + step.reason + ')').join(' | '));
1803
+ lines.push('relaxationTrace=' + result.relaxationTrace.map((step) => serializeUntrustedMemory(step.from, 80) + ' -> ' + serializeUntrustedMemory(step.to, 80) + ' (' + serializeUntrustedMemory(step.reason, 160) + ')').join(' | '));
1777
1804
  }
1778
1805
  if (Array.isArray(result.relatedButNotSelected) && result.relatedButNotSelected.length) {
1779
- lines.push('relatedButNotSelected=' + result.relatedButNotSelected.slice(0, 4).map((item) => item.displayTitle + ' [' + item.reason + ']').join(' | '));
1806
+ lines.push('relatedButNotSelected=' + result.relatedButNotSelected.slice(0, 4).map((item) => serializeUntrustedMemory(item.displayTitle, 160) + ' [' + serializeUntrustedMemory(item.reason, 160) + ']').join(' | '));
1780
1807
  }
1781
1808
  lines.push('');
1782
1809
  if (result.narrative && result.narrative.summary) {
1783
- lines.push(result.narrative.summary);
1810
+ lines.push(serializeUntrustedMemory(result.narrative.summary, 800));
1784
1811
  }
1785
1812
  const maxItems = Number(config.memoryContextMaxItems || config.limit || 3);
1786
1813
  const sourceWindowMaxChars = Number(config.sourceWindowMaxChars || 1200);
1787
1814
  const includeSourceWindow = config.includeSourceWindowByDefault === true;
1788
1815
  for (const item of result.items.slice(0, maxItems)) {
1789
- const source = item.source ? ' [' + item.source + ']' : '';
1790
- lines.push('- ' + item.text + source);
1816
+ const source = item.source ? ' [' + serializeUntrustedMemory(item.source, 220) + ']' : '';
1817
+ lines.push('- ' + serializeUntrustedMemory(item.text, 500) + source);
1791
1818
  const sourceType = item.sourceType || 'compiled_memory';
1792
1819
  const quote = item.canAnswerExactQuote === true ? 'true' : 'false';
1793
1820
  const confidence = Number.isFinite(item.confidence) ? String(item.confidence) : 'unknown';
1794
- const anchor = item.sourceAnchor ? '; anchorEvent=' + (item.sourceAnchor.eventId || 'unknown')
1795
- + (item.sourceAnchor.sessionId ? '; session=' + item.sourceAnchor.sessionId : '')
1796
- + (item.sourceAnchor.role ? '; role=' + item.sourceAnchor.role : '') : '';
1797
- const why = item.whyMatched ? '; whyMatched=' + item.whyMatched : '';
1798
- const canonical = item.canonicalId ? '; canonicalId=' + item.canonicalId : '';
1821
+ const anchor = item.sourceAnchor ? '; anchorEvent=' + serializeUntrustedMemory(item.sourceAnchor.eventId || 'unknown', 160)
1822
+ + (item.sourceAnchor.sessionId ? '; session=' + serializeUntrustedMemory(item.sourceAnchor.sessionId, 160) : '')
1823
+ + (item.sourceAnchor.role ? '; role=' + serializeUntrustedMemory(item.sourceAnchor.role, 80) : '') : '';
1824
+ const why = item.whyMatched ? '; whyMatched=' + serializeUntrustedMemory(item.whyMatched, 180) : '';
1825
+ const canonical = item.canonicalId ? '; canonicalId=' + serializeUntrustedMemory(item.canonicalId, 180) : '';
1799
1826
  const matchedFacets = Array.isArray(item.matchedFacets) && item.matchedFacets.length
1800
- ? '; matchedFacets=' + item.matchedFacets.map((facet) => facet.type + ':' + facet.value).join(',')
1827
+ ? '; matchedFacets=' + item.matchedFacets.map((facet) => serializeUntrustedMemory(facet.type, 80) + ':' + serializeUntrustedMemory(facet.value, 160)).join(',')
1801
1828
  : '';
1802
- lines.push(' sourceType=' + sourceType + '; confidence=' + confidence + '; canAnswerExactQuote=' + quote + canonical + matchedFacets + anchor + why);
1829
+ lines.push(' sourceType=' + serializeUntrustedMemory(sourceType, 120) + '; confidence=' + confidence + '; canAnswerExactQuote=' + quote + canonical + matchedFacets + anchor + why);
1803
1830
  if (item.sourceContext && item.sourceContext.event) {
1804
1831
  const anchorEvent = item.sourceContext.event;
1805
1832
  const anchorFormatted = formatContextEvent(anchorEvent, Math.min(220, sourceWindowMaxChars));
1806
1833
  lines.push(' sourceContext=' + anchorFormatted.line);
1807
1834
  lines.push(' sourceWindow=' + formatSourceWindow(item.sourceContext.window, item.sourceContext));
1808
1835
  if (item.sourceContext.locator && item.sourceContext.locator.command) {
1809
- lines.push(' sourceLocator=' + item.sourceContext.locator.command);
1836
+ lines.push(' sourceLocator=' + serializeUntrustedMemory(item.sourceContext.locator.command, 500));
1810
1837
  } else if (item.sourceContext.event.eventId) {
1811
1838
  const project = item.sourceContext.event.projectId ? ' --project ' + item.sourceContext.event.projectId : '';
1812
- lines.push(' sourceLocator=cogmem memory show --event ' + item.sourceContext.event.eventId + project + ' --before 2 --after 2 --json');
1839
+ lines.push(' sourceLocator=' + serializeUntrustedMemory('cogmem memory show --event ' + item.sourceContext.event.eventId + project + ' --before 2 --after 2 --json', 500));
1813
1840
  }
1814
1841
  const seenEventIds = new Set([anchorEvent.eventId].filter(Boolean));
1815
1842
  const before = uniqueWindowEvents(Array.isArray(item.sourceContext.before) ? item.sourceContext.before : [], seenEventIds).slice(-2);
@@ -1854,16 +1881,16 @@ function formatRecallDecision(trace) {
1854
1881
 
1855
1882
  function formatAtlasCard(card) {
1856
1883
  const facets = Array.isArray(card && card.matchedFacets)
1857
- ? card.matchedFacets.map((facet) => facet.type + ':' + facet.value).join(',')
1884
+ ? card.matchedFacets.map((facet) => serializeUntrustedMemory(facet.type, 80) + ':' + serializeUntrustedMemory(facet.value, 160)).join(',')
1858
1885
  : '';
1859
1886
  const paths = Array.isArray(card && card.matchedPaths)
1860
- ? card.matchedPaths.map((path) => path.relation + '@' + (path.facet && path.facet.nodeId || 'facet')).slice(0, 6).join(',')
1887
+ ? card.matchedPaths.map((path) => serializeUntrustedMemory(path.relation, 80) + '@' + serializeUntrustedMemory(path.facet && path.facet.nodeId || 'facet', 180)).slice(0, 6).join(',')
1861
1888
  : '';
1862
- const locator = card && card.sourceLocator && card.sourceLocator.command ? '; sourceLocator=' + card.sourceLocator.command : '';
1863
- const why = card && card.whyMatched ? '; whyMatched=' + truncateLineWithMeta(card.whyMatched, 180).text : '';
1864
- return (card && card.canonicalId || 'episode:unknown')
1865
- + '; title=' + truncateLineWithMeta(card && card.displayTitle, 120).text
1866
- + '; summary=' + truncateLineWithMeta(card && card.oneLineSummary, 180).text
1889
+ const locator = card && card.sourceLocator && card.sourceLocator.command ? '; sourceLocator=' + serializeUntrustedMemory(card.sourceLocator.command, 500) : '';
1890
+ const why = card && card.whyMatched ? '; whyMatched=' + serializeUntrustedMemory(card.whyMatched, 180) : '';
1891
+ return serializeUntrustedMemory(card && card.canonicalId || 'episode:unknown', 180)
1892
+ + '; title=' + serializeUntrustedMemory(card && card.displayTitle, 120)
1893
+ + '; summary=' + serializeUntrustedMemory(card && card.oneLineSummary, 180)
1867
1894
  + (facets ? '; matchedFacets=' + facets : '')
1868
1895
  + (paths ? '; matchedPaths=' + paths : '')
1869
1896
  + why
@@ -1878,10 +1905,10 @@ function clampRecallContext(text, maxChars) {
1878
1905
  }
1879
1906
 
1880
1907
  function formatContextEvent(event, limit) {
1881
- const truncation = truncateLineWithMeta(event && event.text, limit);
1882
- const label = contextEventLabel(event);
1883
- const role = event && event.role ? event.role : 'unknown';
1884
- const eventId = event && event.eventId ? event.eventId : 'unknown';
1908
+ const truncation = truncateLineWithMeta(serializeUntrustedMemory(event && event.text, limit), limit);
1909
+ const label = serializeUntrustedMemory(contextEventLabel(event), 120);
1910
+ const role = serializeUntrustedMemory(event && event.role ? event.role : 'unknown', 80);
1911
+ const eventId = serializeUntrustedMemory(event && event.eventId ? event.eventId : 'unknown', 160);
1885
1912
  const charRange = event && event.charRange ? '; charRange=' + event.charRange.start + '-' + event.charRange.end : '';
1886
1913
  const sourceRange = formatSourceRange(event && event.sourceRange);
1887
1914
  const textLength = Number.isFinite(event && event.textLength) ? event.textLength : truncation.originalChars;
@@ -1903,17 +1930,17 @@ function formatSourceWindow(window, context) {
1903
1930
  const dropped = Array.isArray(window && window.droppedOverlapEventIds) ? window.droppedOverlapEventIds : [];
1904
1931
  return 'before=' + formatWindowSide(before)
1905
1932
  + '; after=' + formatWindowSide(after)
1906
- + '; overlap=' + (overlapEventIds.length ? overlapEventIds.join(',') : 'none')
1907
- + '; droppedOverlap=' + (dropped.length ? dropped.join(',') : 'none')
1908
- + '; overlapHandling=' + ((window && window.overlapHandling) || 'drop_from_after');
1933
+ + '; overlap=' + (overlapEventIds.length ? overlapEventIds.map((id) => serializeUntrustedMemory(id, 120)).join(',') : 'none')
1934
+ + '; droppedOverlap=' + (dropped.length ? dropped.map((id) => serializeUntrustedMemory(id, 120)).join(',') : 'none')
1935
+ + '; overlapHandling=' + serializeUntrustedMemory((window && window.overlapHandling) || 'drop_from_after', 120);
1909
1936
  }
1910
1937
 
1911
1938
  function formatWindowSide(side) {
1912
1939
  return 'requestedCount=' + Number(side.requestedCount || 0)
1913
1940
  + ', count=' + Number(side.count || 0)
1914
1941
  + ', excludesAnchor=' + (side.excludesAnchor !== false)
1915
- + ', ordering=' + (side.ordering || 'chronological')
1916
- + ', roleFilter=' + (side.roleFilter || 'all');
1942
+ + ', ordering=' + serializeUntrustedMemory(side.ordering || 'chronological', 80)
1943
+ + ', roleFilter=' + serializeUntrustedMemory(side.roleFilter || 'all', 80);
1917
1944
  }
1918
1945
 
1919
1946
  function uniqueWindowEvents(events, seenEventIds) {
@@ -5,7 +5,7 @@ import { callCogmemMcpTool, listCogmemMcpTools, } from './CoreMcpTools.js';
5
5
  export function createCogmemMcpServer(runtime = {}) {
6
6
  const server = new Server({
7
7
  name: 'cogmem-core',
8
- version: '3.7.0',
8
+ version: '3.7.1',
9
9
  }, {
10
10
  capabilities: {
11
11
  tools: {},
@@ -44,7 +44,7 @@ export interface RecallExplanation {
44
44
  projectId?: string;
45
45
  agentId?: string;
46
46
  collection?: string;
47
- recallMode: MemoryKernelNavigationResult['recallMode'] | 'raw_ledger_fallback';
47
+ recallMode: MemoryKernelNavigationResult['recallMode'] | 'raw_ledger_fallback' | 'atlas_facet_recall' | 'atlas_raw_grounded_recall';
48
48
  fallbackUsed: boolean;
49
49
  narrative?: NonNullable<MemoryKernelNavigationResult['navigation']>['narrative'];
50
50
  pulseTrace?: NonNullable<MemoryKernelNavigationResult['navigation']>['pulse']['trace'];
@@ -2,7 +2,7 @@ import type Database from 'bun:sqlite';
2
2
  import type { FacetQueryPlan } from '../atlas/FacetQueryPlanner.js';
3
3
  import type { MemoryAtlasAction, MemoryAtlasCard, MemoryAtlasEdge, MemoryAtlasNode, MemoryAtlasRelatedCard } from '../atlas/MemoryAtlasTypes.js';
4
4
  export declare const MEMORY_ATLAS_PROJECTION_NAME = "memory_atlas.v2";
5
- export declare const MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION = "3.7.0";
5
+ export declare const MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION = "3.7.2";
6
6
  export declare class MemoryAtlasStore {
7
7
  readonly db: Database;
8
8
  constructor(db: Database);
@@ -52,6 +52,8 @@ export declare class MemoryAtlasStore {
52
52
  decay(projectId?: string, factor?: number, now?: number): number;
53
53
  projectionNeedsRefresh(projectId: string): boolean;
54
54
  markProjectionClean(projectId: string, metadata?: Record<string, unknown>, now?: number): void;
55
+ markProjectionDirty(projectId: string, metadata?: Record<string, unknown>, now?: number): void;
56
+ aggregateFacetNodeSupport(projectId: string, now?: number): void;
55
57
  markProjectionFailed(projectId: string, error: string, now?: number): void;
56
58
  getProjectionState(projectId: string): {
57
59
  status: string;
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  export const MEMORY_ATLAS_PROJECTION_NAME = 'memory_atlas.v2';
3
- export const MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION = '3.7.0';
3
+ export const MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION = '3.7.2';
4
4
  export class MemoryAtlasStore {
5
5
  db;
6
6
  constructor(db) {
@@ -23,12 +23,13 @@ export class MemoryAtlasStore {
23
23
  this.refreshFtsNode(input.id);
24
24
  }
25
25
  getNode(nodeId, projectId) {
26
+ const scopedNodeId = scopedEntityNodeId(this.db, nodeId, projectId);
26
27
  const row = this.db.prepare(`
27
28
  SELECT d.*, COALESCE(a.activation, 0) AS activation
28
29
  FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
29
30
  ON a.project_id=d.project_id AND a.node_id=d.node_id
30
31
  WHERE d.node_id=? AND d.project_id=?
31
- `).get(nodeId, projectId);
32
+ `).get(scopedNodeId, projectId);
32
33
  return row ? mapNode(row, 0) : null;
33
34
  }
34
35
  listNodes(projectId, limit) {
@@ -89,23 +90,25 @@ export class MemoryAtlasStore {
89
90
  if (!plannedFacets.length)
90
91
  return [];
91
92
  const facetMatches = plannedFacets.map((facet) => ({ facet, rows: this.episodeEdgesForFacet(projectId, facet) }));
92
- if (plan.operator === 'intersection' && facetMatches.some((match) => match.rows.length === 0))
93
+ const facetGroups = groupFacetMatches(facetMatches);
94
+ if (plan.operator === 'intersection' && facetGroups.some((group) => group.every((match) => match.rows.length === 0)))
93
95
  return [];
94
96
  const candidateIds = plan.operator === 'intersection'
95
- ? intersectSets(facetMatches.map((match) => new Set(match.rows.map((row) => row.source_id))))
97
+ ? intersectSets(facetGroups.map((group) => new Set(group.flatMap((match) => match.rows.map((row) => row.source_id)))))
96
98
  : new Set(facetMatches.flatMap((match) => match.rows.map((row) => row.source_id)));
97
99
  if (!candidateIds.size)
98
100
  return [];
99
101
  const rows = this.episodeRows(projectId, Array.from(candidateIds));
100
- const cards = rows.map((row) => {
102
+ const rankedCards = rows.map((row) => {
101
103
  const candidateEdges = facetMatches.flatMap((match) => match.rows.filter((edge) => edge.source_id === row.source_id));
102
104
  return this.cardFromEpisodeRow(row, candidateEdges, projectId);
103
- });
105
+ }).sort((left, right) => cardRank(right) - cardRank(left) || left.canonicalId.localeCompare(right.canonicalId));
106
+ const cards = dedupeCardsByEvidence(rankedCards).slice(0, Math.max(1, Math.min(limit, 100)));
104
107
  const selectedIds = new Set(cards.map((card) => card.canonicalId));
105
108
  for (const card of cards) {
106
- card.relatedButNotSelected = mergeRelatedCards(this.relatedEpisodeCards(projectId, card.canonicalId, selectedIds, 4), this.topicRelatedCardsForPlan(projectId, plan, selectedIds, 4)).slice(0, 4);
109
+ card.relatedButNotSelected = mergeRelatedCards(card.relatedButNotSelected ?? [], this.relatedEpisodeCards(projectId, card.canonicalId, selectedIds, 4), this.topicRelatedCardsForPlan(projectId, plan, selectedIds, 4)).slice(0, 4);
107
110
  }
108
- return cards.sort((left, right) => scoreCard(right) - scoreCard(left)).slice(0, Math.max(1, Math.min(limit, 100)));
111
+ return cards;
109
112
  }
110
113
  relatedEpisodeCards(projectId, canonicalId, selectedIds, limit) {
111
114
  const parsed = parseNodeId(canonicalId, projectId);
@@ -483,6 +486,47 @@ export class MemoryAtlasStore {
483
486
  last_error=NULL, metadata_json=excluded.metadata_json
484
487
  `).run(projectId, String(now), now, JSON.stringify({ compatibilityAliasFor: MEMORY_ATLAS_PROJECTION_NAME }));
485
488
  }
489
+ markProjectionDirty(projectId, metadata = {}, now = Date.now()) {
490
+ this.db.prepare(`
491
+ INSERT INTO memory_atlas_projection_state(project_id,projection_name,cursor_value,status,last_rebuild_at,last_error,metadata_json)
492
+ VALUES(?, ?, NULL, 'dirty', ?, NULL, ?)
493
+ ON CONFLICT(project_id,projection_name) DO UPDATE SET
494
+ status='dirty', cursor_value=NULL, last_error=NULL, metadata_json=excluded.metadata_json
495
+ `).run(projectId, MEMORY_ATLAS_PROJECTION_NAME, now, JSON.stringify({
496
+ ...metadata,
497
+ projectionName: MEMORY_ATLAS_PROJECTION_NAME,
498
+ projectionSchemaVersion: MEMORY_ATLAS_PROJECTION_SCHEMA_VERSION,
499
+ }));
500
+ this.db.prepare(`
501
+ INSERT INTO memory_atlas_projection_state(project_id,projection_name,cursor_value,status,last_rebuild_at,last_error,metadata_json)
502
+ VALUES(?, 'memory_atlas.v1', NULL, 'dirty', ?, NULL, ?)
503
+ ON CONFLICT(project_id,projection_name) DO UPDATE SET status='dirty', cursor_value=NULL, last_error=NULL, metadata_json=excluded.metadata_json
504
+ `).run(projectId, now, JSON.stringify({ compatibilityAliasFor: MEMORY_ATLAS_PROJECTION_NAME, dirtyBecause: 'atlas_v2_targeted_reindex' }));
505
+ }
506
+ aggregateFacetNodeSupport(projectId, now = Date.now()) {
507
+ const rows = this.db.prepare(`
508
+ SELECT target_type,target_id,evidence_event_ids_json
509
+ FROM memory_edges
510
+ WHERE project_id=? AND source_type='episode'
511
+ AND target_type IN ('topic','time','issue','entity','session','thread','memoryKind','actionKind')
512
+ AND status IN ('active','weak')
513
+ ORDER BY target_type,target_id
514
+ `).all(projectId);
515
+ const buckets = new Map();
516
+ for (const row of rows) {
517
+ const id = nodeId(row.target_type, row.target_id, projectId);
518
+ const bucket = buckets.get(id) ?? { count: 0, evidence: new Set() };
519
+ bucket.count += 1;
520
+ for (const eventId of parseStringArray(row.evidence_event_ids_json).slice(0, 5))
521
+ bucket.evidence.add(eventId);
522
+ buckets.set(id, bucket);
523
+ }
524
+ const update = this.db.prepare(`UPDATE memory_atlas_documents SET support_count=?, evidence_event_ids_json=?, updated_at=? WHERE project_id=? AND node_id=?`);
525
+ for (const [nodeIdValue, bucket] of buckets) {
526
+ update.run(bucket.count, JSON.stringify(Array.from(bucket.evidence).slice(0, 100)), now, projectId, nodeIdValue);
527
+ this.refreshFtsNode(nodeIdValue);
528
+ }
529
+ }
486
530
  markProjectionFailed(projectId, error, now = Date.now()) {
487
531
  this.db.prepare(`
488
532
  INSERT INTO memory_atlas_projection_state(project_id,projection_name,status,last_rebuild_at,last_error,metadata_json)
@@ -547,16 +591,22 @@ export class MemoryAtlasStore {
547
591
  `).all(...params);
548
592
  }
549
593
  episodeRows(projectId, episodeIds) {
550
- const bounded = Array.from(new Set(episodeIds)).slice(0, 500);
551
- if (!bounded.length)
552
- return [];
553
- return this.db.prepare(`
554
- SELECT d.*, COALESCE(a.activation, 0) AS activation
555
- FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
556
- ON a.project_id=d.project_id AND a.node_id=d.node_id
557
- WHERE d.project_id=? AND d.node_id IN (${bounded.map(() => '?').join(',')})
558
- AND d.node_type='episode' AND d.status NOT IN ('rejected','archived')
559
- `).all(projectId, ...bounded.map((id) => `episode:${id}`));
594
+ const ids = Array.from(new Set(episodeIds));
595
+ const rows = [];
596
+ for (let index = 0; index < ids.length; index += 400) {
597
+ const chunk = ids.slice(index, index + 400);
598
+ if (!chunk.length)
599
+ continue;
600
+ rows.push(...this.db.prepare(`
601
+ SELECT d.*, COALESCE(a.activation, 0) AS activation
602
+ FROM memory_atlas_documents d LEFT JOIN memory_atlas_activation a
603
+ ON a.project_id=d.project_id AND a.node_id=d.node_id
604
+ WHERE d.project_id=? AND d.node_id IN (${chunk.map(() => '?').join(',')})
605
+ AND d.node_type='episode' AND d.status NOT IN ('rejected','archived')
606
+ ORDER BY d.occurred_at DESC, d.support_count DESC, d.node_id ASC
607
+ `).all(projectId, ...chunk.map((id) => `episode:${id}`)));
608
+ }
609
+ return rows;
560
610
  }
561
611
  cardFromEpisodeRow(row, edges, projectId) {
562
612
  const metadata = parseMetadata(row.metadata_json);
@@ -568,6 +618,7 @@ export class MemoryAtlasStore {
568
618
  });
569
619
  const issueHints = stringArray(metadata.issueHints);
570
620
  const topicHints = stringArray(metadata.topicHints);
621
+ const matchedTopicPaths = matchedFacets.filter((facet) => facet.type === 'topic').map((facet) => facet.value);
571
622
  return {
572
623
  canonicalId: row.node_id,
573
624
  nodeType: 'episode',
@@ -575,7 +626,7 @@ export class MemoryAtlasStore {
575
626
  oneLineSummary: row.summary || undefined,
576
627
  matchedFacets,
577
628
  matchedPaths,
578
- parentTopics: topicHints.length ? topicHints : row.topic_path ? [row.topic_path] : [],
629
+ parentTopics: matchedTopicPaths.length ? matchedTopicPaths : topicHints.length ? topicHints : row.topic_path ? [row.topic_path] : [],
579
630
  issueType: issueHints[0],
580
631
  eventKind: optionalMetadataString(metadata.eventKind),
581
632
  localDate: optionalMetadataString(metadata.localDate) ?? (row.occurred_at ? new Date(row.occurred_at).toISOString().slice(0, 10) : undefined),
@@ -629,6 +680,8 @@ catch {
629
680
  } }
630
681
  function escapeLike(value) { return value.replace(/[\\%_]/g, '\\$&'); }
631
682
  function nodeId(type, id, projectId) {
683
+ if (type === 'entity')
684
+ return id.startsWith('facet:') ? `entity:${projectId}:${id}` : `entity:${id}`;
632
685
  if (['topic', 'time', 'issue', 'session', 'thread', 'memoryKind', 'actionKind'].includes(type))
633
686
  return `${type}:${projectId}:${id}`;
634
687
  return `${type}:${id}`;
@@ -636,8 +689,18 @@ function nodeId(type, id, projectId) {
636
689
  function timeNodeId(projectId, occurredAt) {
637
690
  return `time:${projectId}:${new Date(occurredAt).getUTCFullYear()}`;
638
691
  }
692
+ function scopedEntityNodeId(db, value, projectId) {
693
+ if (!value.startsWith('entity:') || value.startsWith(`entity:${projectId}:`))
694
+ return value;
695
+ const id = value.slice('entity:'.length);
696
+ if (!id.startsWith('facet:'))
697
+ return value;
698
+ const scoped = `entity:${projectId}:${id}`;
699
+ const row = db.prepare(`SELECT 1 FROM memory_atlas_documents WHERE project_id=? AND node_id=?`).get(projectId, scoped);
700
+ return row ? scoped : value;
701
+ }
639
702
  function parseNodeId(value, projectId) {
640
- for (const type of ['topic', 'time', 'issue', 'session', 'thread', 'memoryKind', 'actionKind']) {
703
+ for (const type of ['topic', 'time', 'issue', 'entity', 'session', 'thread', 'memoryKind', 'actionKind']) {
641
704
  const prefix = `${type}:${projectId}:`;
642
705
  if (value.startsWith(prefix))
643
706
  return { type, id: value.slice(prefix.length) };
@@ -686,6 +749,29 @@ function facetLabel(type, value) {
686
749
  }
687
750
  return value;
688
751
  }
752
+ function groupFacetMatches(matches) {
753
+ const groups = new Map();
754
+ for (const match of matches) {
755
+ const targetSlug = equivalentTargetSlug(match.facet);
756
+ const key = targetSlug
757
+ ? `target:${targetSlug}`
758
+ : match.facet.type === 'actionKind' || match.facet.type === 'memoryKind'
759
+ ? `${match.facet.type}:${match.facet.relation || ''}`
760
+ : `${match.facet.type}:${match.facet.value}:${match.facet.relation || ''}`;
761
+ const group = groups.get(key) ?? [];
762
+ group.push(match);
763
+ groups.set(key, group);
764
+ }
765
+ return Array.from(groups.values());
766
+ }
767
+ function equivalentTargetSlug(facet) {
768
+ if (facet.type === 'entity' && facet.value.startsWith('facet:'))
769
+ return facet.value.slice('facet:'.length);
770
+ if (facet.type !== 'topic')
771
+ return undefined;
772
+ const match = facet.value.match(/^PROJECT\/[^/]+\/([^/]+)$/u);
773
+ return match?.[1];
774
+ }
689
775
  function intersectSets(sets) {
690
776
  if (!sets.length)
691
777
  return new Set();
@@ -697,12 +783,50 @@ function intersectSets(sets) {
697
783
  }
698
784
  return result;
699
785
  }
786
+ function cardRank(card) {
787
+ return scoreCard(card) + Math.min(card.evidenceTotal || 0, 10) * 0.01;
788
+ }
700
789
  function scoreCard(card) {
701
- const issuePriority = card.issueType === 'memory-context-blackbox' ? 4
702
- : card.issueType === 'graph-runtime-blackbox' ? 2
703
- : card.issueType === 'atlas-readability' ? 1
704
- : 0;
705
- return card.matchedFacets.length * 10 + issuePriority + card.matchedPaths.reduce((sum, path) => sum + path.confidence, 0) + (card.localDate ? 0.1 : 0);
790
+ return card.matchedFacets.length * 10 + card.matchedPaths.reduce((sum, path) => sum + path.confidence, 0) + (card.localDate ? 0.1 : 0);
791
+ }
792
+ function dedupeCardsByEvidence(cards) {
793
+ const bySignature = new Map();
794
+ const out = [];
795
+ for (const card of cards) {
796
+ const signature = evidenceSignature(card);
797
+ if (!signature) {
798
+ out.push(card);
799
+ continue;
800
+ }
801
+ const kept = bySignature.get(signature);
802
+ if (!kept) {
803
+ bySignature.set(signature, card);
804
+ out.push(card);
805
+ continue;
806
+ }
807
+ kept.relatedButNotSelected = mergeRelatedCards(kept.relatedButNotSelected ?? [], [{
808
+ canonicalId: card.canonicalId,
809
+ displayTitle: card.displayTitle,
810
+ reason: 'same primary raw evidence',
811
+ }]);
812
+ }
813
+ return out;
814
+ }
815
+ function evidenceSignature(card) {
816
+ const primary = card.sourceLocator?.eventId || card.evidenceEventIds[0];
817
+ if (!primary)
818
+ return undefined;
819
+ const facets = (type) => card.matchedFacets
820
+ .filter((facet) => facet.type === type)
821
+ .map((facet) => facet.value)
822
+ .sort()
823
+ .join(',');
824
+ return [
825
+ primary,
826
+ card.localDate || '',
827
+ facets('entity'),
828
+ facets('actionKind'),
829
+ ].join('|');
706
830
  }
707
831
  function mergeRelatedCards(...groups) {
708
832
  const merged = new Map();