cogmem 3.6.5 → 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 (46) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/MEMORY_ATLAS.md +59 -12
  3. package/README.md +21 -13
  4. package/RELEASE_CHECKLIST.md +5 -4
  5. package/dist/agent/AgentMemoryBackend.d.ts +24 -3
  6. package/dist/agent/AgentMemoryBackend.js +285 -55
  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.d.ts +31 -0
  15. package/dist/atlas/EpisodeTitleGenerator.js +191 -0
  16. package/dist/atlas/FacetQueryPlanner.d.ts +33 -0
  17. package/dist/atlas/FacetQueryPlanner.js +229 -0
  18. package/dist/atlas/GraphCurator.d.ts +30 -0
  19. package/dist/atlas/GraphCurator.js +501 -0
  20. package/dist/atlas/MemoryAtlasIndexer.d.ts +15 -0
  21. package/dist/atlas/MemoryAtlasIndexer.js +55 -6
  22. package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
  23. package/dist/atlas/MemoryAtlasService.d.ts +4 -1
  24. package/dist/atlas/MemoryAtlasService.js +174 -11
  25. package/dist/atlas/MemoryAtlasTypes.d.ts +75 -11
  26. package/dist/bin/memory.js +18 -5
  27. package/dist/factory.d.ts +18 -0
  28. package/dist/factory.js +41 -2
  29. package/dist/host/openclaw/AutoMemoryPluginInstaller.js +135 -39
  30. package/dist/mcp/server.js +1 -1
  31. package/dist/recall/RecallExplanation.d.ts +1 -1
  32. package/dist/store/MemoryAtlasStore.d.ts +12 -1
  33. package/dist/store/MemoryAtlasStore.js +353 -15
  34. package/dist/utils/ActionKindRegistry.d.ts +10 -0
  35. package/dist/utils/ActionKindRegistry.js +18 -0
  36. package/dist/utils/EntityCueExtractor.d.ts +13 -0
  37. package/dist/utils/EntityCueExtractor.js +81 -0
  38. package/examples/hermes-backend/AGENTS.md +3 -3
  39. package/examples/hermes-backend/README.md +1 -1
  40. package/examples/hermes-backend/SKILL.md +14 -5
  41. package/examples/hermes-backend/references/operations.md +12 -9
  42. package/examples/openclaw-backend/AGENTS.md +3 -2
  43. package/examples/openclaw-backend/README.md +3 -2
  44. package/examples/openclaw-backend/SKILL.md +31 -9
  45. package/examples/openclaw-backend/references/operations.md +29 -11
  46. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import { memoryEventCharRange, memoryEventLabel, memoryEventSourceRange, normalizeSourceContextWindow, } from '../recall/SourceContextMetadata.js';
2
2
  import { isOperationalNoiseText, isRecallableMemoryEvidence } from '../recall/RecallGovernance.js';
3
3
  import { compileAgentRecallQuery, } from './AgentRecallQueryCompiler.js';
4
+ import { extractEntityCues } from '../utils/EntityCueExtractor.js';
5
+ import { inferActionKinds } from '../utils/ActionKindRegistry.js';
4
6
  export class KernelAgentMemoryBackend {
5
7
  kernel;
6
8
  constructor(kernel) {
@@ -248,13 +250,13 @@ export class KernelAgentMemoryBackend {
248
250
  intent: query.intent,
249
251
  anchorText: query.anchorText,
250
252
  });
251
- if (query.intent === 'previous_session_summary') {
253
+ if (queryPlan.intent === 'previous_session_summary') {
252
254
  return this.recallPreviousSession(query, queryPlan);
253
255
  }
254
- if (query.intent === 'forensic_quote') {
256
+ if (queryPlan.intent === 'forensic_quote') {
255
257
  return this.recallForensicQuote(query, queryPlan);
256
258
  }
257
- if (queryPlan.intent === 'historical_discussion') {
259
+ if (queryPlan.intent === 'historical_discussion' || queryPlan.intent === 'action_history') {
258
260
  return this.recallHistoricalDiscussion(query, queryPlan);
259
261
  }
260
262
  const limit = query.limit ?? 5;
@@ -431,8 +433,7 @@ export class KernelAgentMemoryBackend {
431
433
  ? this.getSessionEvents(previousSessionId, query, Math.max(limit * 3, 24))
432
434
  : [];
433
435
  const items = events
434
- .filter((event) => this.isAgentRawEvent(event, query.agentId))
435
- .filter((event) => this.isAllowedRawEventCollection(event, query.collection))
436
+ .filter((event) => this.isRawEventInRecallScope(event, query, queryPlan.intent))
436
437
  .filter((event) => !this.isOperationalNoiseRawEvent(event))
437
438
  .filter((event) => this.hasReadableEventText(event))
438
439
  .slice(0, limit)
@@ -457,26 +458,29 @@ export class KernelAgentMemoryBackend {
457
458
  }
458
459
  recallForensicQuote(query, queryPlan) {
459
460
  const limit = query.limit ?? 5;
460
- const anchorItems = this.recallForensicAnchor(query, limit);
461
+ const anchorItems = this.recallForensicAnchor(query, queryPlan, limit);
461
462
  const rawEvents = anchorItems.length > 0 && (queryPlan.anchorUsed || !!query.anchorEventId)
462
463
  ? []
463
- : this.searchRawEventsByQueryPlan(queryPlan, query, Math.max(limit * 4, 20));
464
+ : [
465
+ ...this.rawEventsForLocalDateCue(query, Math.max(limit * 4, 20)),
466
+ ...this.searchRawEventsByQueryPlan(queryPlan, query, Math.max(limit * 4, 20)),
467
+ ];
468
+ const facetQuoteItems = anchorItems.length === 0 ? this.facetGraphQuoteItemsForQuery(query, queryPlan, limit) : [];
464
469
  const items = [
465
470
  ...anchorItems,
466
471
  ...rawEvents
467
472
  .filter((event) => this.isAgentRawEvent(event, query.agentId))
468
- .filter((event) => this.isAllowedSession(event, query))
469
- .filter((event) => this.isAllowedRawEventCollection(event, query.collection))
470
- .filter((event) => !this.isOperationalNoiseRawEvent(event))
473
+ .filter((event) => this.isRawEventInRecallScope(event, query, queryPlan.intent))
471
474
  .filter((event) => this.isQuoteSourceEvent(event))
472
475
  .filter((event) => this.hasReadableEventText(event))
473
- .sort((a, b) => this.quoteEventPriority(a) - this.quoteEventPriority(b))
476
+ .sort((a, b) => this.quoteEventPriority(a, queryPlan) - this.quoteEventPriority(b, queryPlan))
474
477
  .slice(0, limit)
475
478
  .map((event) => this.toAgentRawRecallItem(event, {
476
479
  sourceType: 'raw_ledger',
477
480
  whyMatched: 'forensic_quote_raw_event',
478
481
  canAnswerExactQuote: true,
479
482
  })),
483
+ ...facetQuoteItems,
480
484
  ].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index)
481
485
  .slice(0, limit);
482
486
  return {
@@ -498,42 +502,117 @@ export class KernelAgentMemoryBackend {
498
502
  const allowsGraph = laneAllowed(query.retrievalPolicy, 'graph');
499
503
  const allowsCompiled = laneAllowed(query.retrievalPolicy, 'compiled');
500
504
  const allowsRawSource = laneAllowed(query.retrievalPolicy, 'raw_source');
505
+ const facetResult = allowsGraph ? this.facetGraphItemsForQuery(query, limit) : { items: [], cards: [], relatedButNotSelected: [], relaxationTrace: [] };
506
+ const facetItems = allowsRawSource ? facetResult.items : [];
501
507
  const graphItems = allowsGraph ? this.memoryBindingGraphItemsForQuery(query, queryPlan, limit) : [];
502
508
  const rawItems = allowsRawSource ? this.rawLedgerFallbackItemsForQuery(queryPlan, query, Math.max(limit * 2, 10)) : [];
503
- const retrievalLimit = Math.max(limit * 4, 24);
504
509
  const compiledItems = allowsCompiled
505
- ? this.filterAgentEvidence(this.kernel.navigateMemory(queryPlan.primarySearchText, {
506
- projectId: query.projectId,
507
- limit: retrievalLimit,
508
- startTime: query.startTime,
509
- endTime: query.endTime,
510
- }).rawEvidence, query.agentId, query.collection, query.excludeSessionId)
511
- .slice(0, limit)
512
- .map((neuron) => this.toAgentRecallItem(neuron))
510
+ ? this.compiledItemsForHistoricalQuery(queryPlan, query, limit)
513
511
  : [];
514
- const items = this.mergeRecallItems(rawItems, this.mergeRecallItems(graphItems, compiledItems, limit), limit);
515
- const selectedLane = rawItems.length > 0
516
- ? 'raw_ledger'
517
- : graphItems.length > 0
518
- ? 'graph'
519
- : compiledItems.length > 0
520
- ? 'compiled'
521
- : 'none';
512
+ const relevantCompiledItems = this.filterCompiledItemsByQueryCues(compiledItems, queryPlan);
513
+ const items = this.mergeHistoricalRecallItems(facetItems, rawItems, graphItems, relevantCompiledItems, limit);
514
+ const selectedLane = facetItems.length > 0
515
+ ? 'facet_graph_raw_ledger'
516
+ : rawItems.length > 0
517
+ ? 'raw_ledger'
518
+ : graphItems.length > 0
519
+ ? 'graph'
520
+ : relevantCompiledItems.length > 0
521
+ ? 'compiled'
522
+ : 'none';
522
523
  return {
523
- recallMode: rawItems.length > 0 ? 'raw_ledger_fallback' : 'brain_recall_fallback',
524
+ recallMode: facetItems.length > 0
525
+ ? (rawItems.length > 0 ? 'atlas_raw_grounded_recall' : 'atlas_facet_recall')
526
+ : rawItems.length > 0 ? 'raw_ledger_fallback' : 'brain_recall_fallback',
524
527
  items,
525
- fallbackUsed: true,
528
+ fallbackUsed: facetItems.length === 0,
526
529
  queryPlan,
527
- decisionTrace: recallDecisionTrace(selectedLane, 'historical_discussion', {
528
- graph: graphItems.length,
529
- navigation: compiledItems.length,
530
- scopedNavigation: compiledItems.length,
530
+ atlasCards: facetResult.cards,
531
+ relatedButNotSelected: facetResult.relatedButNotSelected,
532
+ relaxationTrace: facetResult.relaxationTrace,
533
+ decisionTrace: recallDecisionTrace(selectedLane, queryPlan.intent === 'action_history' ? 'action_history' : 'historical_discussion', {
534
+ graph: graphItems.length + facetItems.length,
535
+ navigation: relevantCompiledItems.length,
536
+ scopedNavigation: relevantCompiledItems.length,
531
537
  brainFallback: 0,
532
- rawLedger: rawItems.length,
538
+ rawLedger: rawItems.length + facetItems.length,
533
539
  }, items.length),
534
540
  };
535
541
  }
536
- recallForensicAnchor(query, limit) {
542
+ facetGraphQuoteItemsForQuery(query, queryPlan, limit) {
543
+ try {
544
+ const atlas = this.kernel.graphExplore(query.query, {
545
+ projectId: query.projectId,
546
+ limit: Math.max(limit * 2, 6),
547
+ includeEvidence: true,
548
+ evidenceLimit: 2,
549
+ refresh: true,
550
+ staleOk: true,
551
+ });
552
+ const cards = (atlas.cards ?? []).slice(0, Math.max(limit * 2, 6));
553
+ const events = cards
554
+ .map((card) => card.sourceLocator?.eventId ?? card.evidenceEventIds[0])
555
+ .filter((eventId) => Boolean(eventId))
556
+ .map((eventId) => this.kernel.getEventContext(eventId, { before: 0, after: 0 })?.event)
557
+ .filter((event) => Boolean(event))
558
+ .filter((event) => this.isRawEventInRecallScope(event, query, queryPlan.intent))
559
+ .filter((event) => this.isQuoteSourceEvent(event))
560
+ .filter((event) => this.hasReadableEventText(event));
561
+ return this.dedupeRawEventsByTurnPreferUser(events)
562
+ .slice(0, limit)
563
+ .map((event) => this.toAgentRawRecallItem(event, {
564
+ sourceType: 'raw_ledger',
565
+ whyMatched: 'forensic_quote_atlas_source_locator',
566
+ canAnswerExactQuote: true,
567
+ }));
568
+ }
569
+ catch {
570
+ return [];
571
+ }
572
+ }
573
+ facetGraphItemsForQuery(query, limit) {
574
+ try {
575
+ const atlas = this.kernel.graphExplore(query.query, {
576
+ projectId: query.projectId,
577
+ limit,
578
+ includeEvidence: true,
579
+ evidenceLimit: 2,
580
+ refresh: true,
581
+ staleOk: true,
582
+ });
583
+ const cards = (atlas.cards ?? []).slice(0, limit);
584
+ const items = cards.map((card) => this.toAgentRecallItemFromAtlasCard(card, query)).filter((item) => Boolean(item));
585
+ const relatedButNotSelected = cards.flatMap((card) => card.relatedButNotSelected ?? []).slice(0, 8);
586
+ return { items, cards, relatedButNotSelected, relaxationTrace: atlas.relaxationTrace ?? [] };
587
+ }
588
+ catch {
589
+ return { items: [], cards: [], relatedButNotSelected: [], relaxationTrace: [] };
590
+ }
591
+ }
592
+ toAgentRecallItemFromAtlasCard(card, query) {
593
+ const eventId = card.sourceLocator?.eventId ?? card.evidenceEventIds[0];
594
+ const sourceContext = eventId ? this.toAgentSourceContext(eventId) : undefined;
595
+ const anchorEvent = sourceContext?.event;
596
+ return {
597
+ id: `facet:${card.canonicalId}`,
598
+ text: [card.displayTitle, card.oneLineSummary].filter(Boolean).join(': '),
599
+ projectId: query.projectId,
600
+ topicPath: card.parentTopics[0],
601
+ canonicalId: card.canonicalId,
602
+ displayTitle: card.displayTitle,
603
+ matchedFacets: card.matchedFacets,
604
+ matchedPaths: card.matchedPaths,
605
+ tags: ['facet_graph', card.eventKind, card.issueType].filter((tag) => Boolean(tag)),
606
+ source: 'memory_atlas',
607
+ sourceType: 'raw_ledger',
608
+ sourceAnchor: anchorEvent ? this.toAgentSourceAnchorFromContextEvent(anchorEvent) : eventId ? { eventId } : undefined,
609
+ sourceContext,
610
+ confidence: Math.min(1, 0.7 + card.matchedFacets.length * 0.08),
611
+ whyMatched: card.whyMatched,
612
+ canAnswerExactQuote: Boolean(sourceContext),
613
+ };
614
+ }
615
+ recallForensicAnchor(query, queryPlan, limit) {
537
616
  if (!query.anchorEventId)
538
617
  return [];
539
618
  const context = this.kernel.getEventContext(query.anchorEventId, { before: 4, after: 4 });
@@ -542,8 +621,7 @@ export class KernelAgentMemoryBackend {
542
621
  const candidates = [context.event, ...context.before.slice().reverse(), ...context.after];
543
622
  return candidates
544
623
  .filter((event) => this.isAgentRawEvent(event, query.agentId))
545
- .filter((event) => this.isAllowedSession(event, query))
546
- .filter((event) => this.isAllowedRawEventCollection(event, query.collection))
624
+ .filter((event) => this.isRawEventInRecallScope(event, query, queryPlan.intent))
547
625
  .filter((event) => !this.isOperationalNoiseRawEvent(event))
548
626
  .filter((event) => this.isQuoteSourceEvent(event))
549
627
  .filter((event) => this.hasReadableEventText(event))
@@ -551,7 +629,7 @@ export class KernelAgentMemoryBackend {
551
629
  const anchorDelta = (a.eventId === query.anchorEventId ? 0 : 1) - (b.eventId === query.anchorEventId ? 0 : 1);
552
630
  if (anchorDelta !== 0)
553
631
  return anchorDelta;
554
- return this.quoteEventPriority(a) - this.quoteEventPriority(b);
632
+ return this.quoteEventPriority(a, queryPlan) - this.quoteEventPriority(b, queryPlan);
555
633
  })
556
634
  .slice(0, limit)
557
635
  .map((event) => this.toAgentRawRecallItem(event, {
@@ -584,15 +662,39 @@ export class KernelAgentMemoryBackend {
584
662
  }
585
663
  return out;
586
664
  }
665
+ rawEventsForLocalDateCue(query, limit) {
666
+ const localDate = localDateCue(query.query, query);
667
+ if (!localDate)
668
+ return [];
669
+ const [year, month, day] = localDate.split('-').map(Number);
670
+ const byLocalDate = this.kernel.eventStore.queryEvents(1, 1000, {
671
+ projectId: query.projectId ? [query.projectId] : undefined,
672
+ workspaceId: query.workspaceId ? [query.workspaceId] : undefined,
673
+ }).records.filter((event) => event.localDate === localDate).slice(0, limit);
674
+ if (byLocalDate.length)
675
+ return byLocalDate;
676
+ const startTime = Date.UTC(year, month - 1, day);
677
+ const endTime = Date.UTC(year, month - 1, day + 1);
678
+ const byTime = this.kernel.eventStore.queryEvents(1, Math.max(1, Math.min(limit, 200)), {
679
+ projectId: query.projectId ? [query.projectId] : undefined,
680
+ workspaceId: query.workspaceId ? [query.workspaceId] : undefined,
681
+ startTime,
682
+ endTime,
683
+ }).records;
684
+ if (byTime.length)
685
+ return byTime;
686
+ return this.kernel.eventStore.queryEvents(1, 1000, {
687
+ projectId: query.projectId ? [query.projectId] : undefined,
688
+ workspaceId: query.workspaceId ? [query.workspaceId] : undefined,
689
+ }).records.filter((event) => event.localDate === localDate).slice(0, limit);
690
+ }
587
691
  rawLedgerFallbackItemsForQuery(queryPlan, query, limit) {
588
692
  const searchedEvents = this.searchRawEventsByQueryPlan(queryPlan, query, Math.max(limit * 2, 10));
589
693
  const rawEvents = queryPlan.intent === 'forensic_quote'
590
694
  ? this.dedupeRawEventsByTurnPreferUser(searchedEvents)
591
695
  : this.dedupeRawEventsByTurnPreferCue(searchedEvents, queryPlan);
592
696
  return rawEvents
593
- .filter((event) => this.isAgentRawEvent(event, query.agentId))
594
- .filter((event) => this.isAllowedSession(event, query))
595
- .filter((event) => this.isAllowedRawEventCollection(event, query.collection))
697
+ .filter((event) => this.isRawEventInRecallScope(event, query, queryPlan.intent))
596
698
  .filter((event) => !this.isOperationalNoiseRawEvent(event))
597
699
  .slice(0, limit)
598
700
  .map((event) => this.toAgentRawRecallItem(event, {
@@ -614,11 +716,7 @@ export class KernelAgentMemoryBackend {
614
716
  const event = this.kernel.getEventContext(anchor.eventId, { before: 0, after: 0 })?.event;
615
717
  if (!event)
616
718
  continue;
617
- if (!this.isAgentRawEvent(event, query.agentId))
618
- continue;
619
- if (!this.isAllowedSession(event, query))
620
- continue;
621
- if (!this.isAllowedRawEventCollection(event, query.collection))
719
+ if (!this.isRawEventInRecallScope(event, query, queryPlan.intent))
622
720
  continue;
623
721
  if (this.isOperationalNoiseRawEvent(event))
624
722
  continue;
@@ -687,6 +785,28 @@ export class KernelAgentMemoryBackend {
687
785
  .filter((term) => term.length >= 2 && !/^(hermes|openclaw|cogmem)$/i.test(term));
688
786
  return uniqueNonEmpty(terms);
689
787
  }
788
+ queryHasStructuredCue(queryPlan) {
789
+ return /hermes|openclaw|cogmem|启动|安装|配置|重启|停止|操作|处理|执行|start|launch|install|configure|restart|stop|run/i.test(queryPlan.originalQuery)
790
+ || queryPlan.intent === 'action_history';
791
+ }
792
+ structuredCueTerms(queryPlan) {
793
+ const terms = [
794
+ ...queryPlan.keywords,
795
+ ...queryPlan.semanticCuePhrases.flatMap((phrase) => phrase.split(/\s+/)),
796
+ ]
797
+ .map((term) => term.trim())
798
+ .filter((term) => term.length >= 2)
799
+ .filter((term) => !/^(之前|什么|做过|做了|让你|我的|原话|精确|完整|the|what|did|you|to)$/i.test(term));
800
+ if (/启动|start|launch/i.test(queryPlan.originalQuery))
801
+ terms.push('启动', 'start', 'launch');
802
+ if (/安装|install|setup/i.test(queryPlan.originalQuery))
803
+ terms.push('安装', 'install', 'setup');
804
+ if (/配置|修改|config/i.test(queryPlan.originalQuery))
805
+ terms.push('配置', '修改', 'config');
806
+ if (/操作|处理|执行|run|ran/i.test(queryPlan.originalQuery))
807
+ terms.push('操作', '执行', 'run');
808
+ return uniqueNonEmpty(terms);
809
+ }
690
810
  itemSearchableText(item) {
691
811
  return [
692
812
  item.text,
@@ -698,15 +818,75 @@ export class KernelAgentMemoryBackend {
698
818
  const out = [];
699
819
  const seen = new Set();
700
820
  for (const item of [...primary, ...secondary]) {
701
- if (seen.has(item.id))
821
+ const keys = [item.canonicalId, item.sourceAnchor?.eventId, item.id].filter((value) => Boolean(value));
822
+ if (keys.some((key) => seen.has(key)))
702
823
  continue;
703
- seen.add(item.id);
824
+ for (const key of keys)
825
+ seen.add(key);
704
826
  out.push(item);
705
827
  if (out.length >= limit)
706
828
  break;
707
829
  }
708
830
  return out;
709
831
  }
832
+ mergeHistoricalRecallItems(facetItems, rawItems, graphItems, compiledItems, limit) {
833
+ const merged = this.mergeRecallItems(facetItems, this.mergeRecallItems(rawItems, this.mergeRecallItems(graphItems, compiledItems, limit), limit), limit);
834
+ if (merged.some((item) => item.sourceType === 'imported_summary'))
835
+ return merged;
836
+ const importedSupport = compiledItems.find((item) => item.sourceType === 'imported_summary');
837
+ if (!importedSupport)
838
+ return merged;
839
+ const withoutDuplicate = merged.filter((item) => item.id !== importedSupport.id && item.sourceAnchor?.eventId !== importedSupport.sourceAnchor?.eventId);
840
+ if (withoutDuplicate.length < limit)
841
+ return [...withoutDuplicate, importedSupport];
842
+ return [...withoutDuplicate.slice(0, Math.max(0, limit - 1)), importedSupport];
843
+ }
844
+ compiledItemsForHistoricalQuery(queryPlan, query, limit) {
845
+ const retrievalLimit = Math.max(limit * 4, 24);
846
+ const out = [];
847
+ const seen = new Set();
848
+ for (const searchText of uniqueNonEmpty([queryPlan.primarySearchText, ...queryPlan.searchTexts, queryPlan.originalQuery])) {
849
+ const rawEvidence = this.kernel.navigateMemory(searchText, {
850
+ projectId: query.projectId,
851
+ limit: retrievalLimit,
852
+ startTime: query.startTime,
853
+ endTime: query.endTime,
854
+ }).rawEvidence;
855
+ const items = this.filterAgentEvidence(rawEvidence, query.agentId, query.collection, query.excludeSessionId)
856
+ .map((neuron) => this.toAgentRecallItem(neuron));
857
+ for (const item of items) {
858
+ const key = item.id || item.sourceAnchor?.eventId;
859
+ if (key && seen.has(key))
860
+ continue;
861
+ if (key)
862
+ seen.add(key);
863
+ out.push(item);
864
+ if (out.length >= limit)
865
+ return out;
866
+ }
867
+ }
868
+ return out;
869
+ }
870
+ filterCompiledItemsByQueryCues(items, queryPlan) {
871
+ if (!this.queryHasStructuredCue(queryPlan))
872
+ return items;
873
+ const cues = this.structuredCueTerms(queryPlan);
874
+ if (!cues.length)
875
+ return items;
876
+ return items.filter((item) => {
877
+ const haystack = this.itemSearchableText(item).toLowerCase();
878
+ if (queryPlan.intent === 'action_history') {
879
+ const entityTerms = extractEntityCues(queryPlan.originalQuery).map((entity) => entity.label.toLowerCase());
880
+ const entityMatched = entityTerms.length === 0 || entityTerms.some((term) => haystack.includes(term));
881
+ const queryKinds = inferActionKinds(queryPlan.originalQuery);
882
+ const allowedKinds = queryKinds.length ? queryKinds : ['started', 'installed', 'configured', 'restarted', 'stopped', 'operated', 'implemented'];
883
+ const itemKinds = inferActionKinds(haystack);
884
+ const actionMatched = itemKinds.some((kind) => allowedKinds.includes(kind));
885
+ return entityMatched && actionMatched;
886
+ }
887
+ return cues.some((cue) => haystack.includes(cue.toLowerCase()));
888
+ });
889
+ }
710
890
  dedupeRawEventsByTurnPreferUser(events) {
711
891
  const byTurn = new Map();
712
892
  for (const event of events) {
@@ -727,6 +907,8 @@ export class KernelAgentMemoryBackend {
727
907
  expandRawSearchTexts(queryPlan) {
728
908
  const hostNeutralKeywords = queryPlan.keywords.filter((keyword) => !/^(hermes|openclaw|cogmem)$/i.test(keyword));
729
909
  return uniqueNonEmpty([
910
+ queryPlan.originalQuery,
911
+ queryPlan.keywords.join(' '),
730
912
  ...queryPlan.searchTexts,
731
913
  hostNeutralKeywords.join(' '),
732
914
  ...hostNeutralKeywords.filter((keyword) => keyword.length >= 2),
@@ -873,6 +1055,21 @@ export class KernelAgentMemoryBackend {
873
1055
  return true;
874
1056
  return false;
875
1057
  }
1058
+ isRawEventInRecallScope(event, query, effectiveIntent) {
1059
+ if (query.projectId && event.projectId !== query.projectId)
1060
+ return false;
1061
+ if (query.workspaceId && event.workspaceId !== query.workspaceId)
1062
+ return false;
1063
+ if (query.threadId && event.threadId !== query.threadId)
1064
+ return false;
1065
+ if (!this.isAgentRawEvent(event, query.agentId))
1066
+ return false;
1067
+ if (!this.isAllowedSession(event, query, effectiveIntent))
1068
+ return false;
1069
+ if (!this.isAllowedRawEventCollection(event, query.collection))
1070
+ return false;
1071
+ return true;
1072
+ }
876
1073
  isOperationalNoiseRawEvent(event) {
877
1074
  const payload = event.payload;
878
1075
  const tags = Array.isArray(payload.metadata?.tags) ? payload.metadata.tags : [];
@@ -884,10 +1081,10 @@ export class KernelAgentMemoryBackend {
884
1081
  }
885
1082
  return isOperationalNoiseText(typeof payload.text === 'string' ? payload.text : JSON.stringify(event.payload));
886
1083
  }
887
- isAllowedSession(event, query) {
1084
+ isAllowedSession(event, query, effectiveIntent = query.intent) {
888
1085
  if (query.excludeSessionId && event.sessionId === query.excludeSessionId)
889
1086
  return false;
890
- if (query.sessionId && query.intent && query.intent !== 'memory_recall' && event.sessionId === query.sessionId)
1087
+ if (query.sessionId && effectiveIntent && effectiveIntent !== 'memory_recall' && event.sessionId === query.sessionId)
891
1088
  return false;
892
1089
  return true;
893
1090
  }
@@ -897,12 +1094,13 @@ export class KernelAgentMemoryBackend {
897
1094
  || typeof payload.output === 'string'
898
1095
  || typeof payload.title === 'string';
899
1096
  }
900
- quoteEventPriority(event) {
1097
+ quoteEventPriority(event, queryPlan) {
1098
+ const cuePenalty = queryPlan && this.rawEventCueScore(event, queryPlan) > 0 ? 0 : queryPlan ? 10 : 0;
901
1099
  if (event.role === 'user')
902
- return 0;
1100
+ return cuePenalty;
903
1101
  if (event.role === 'assistant')
904
- return 1;
905
- return 2;
1102
+ return cuePenalty + 1;
1103
+ return cuePenalty + 2;
906
1104
  }
907
1105
  isQuoteSourceEvent(event) {
908
1106
  return event.role === 'user' || (!event.role && event.rawEventType === 'message');
@@ -1317,6 +1515,38 @@ function uniqueNonEmpty(values) {
1317
1515
  function laneAllowed(policy, lane) {
1318
1516
  return !policy || policy.allowedLanes.includes(lane);
1319
1517
  }
1518
+ function localDateCue(query, options = {}) {
1519
+ const currentYear = localYear(options);
1520
+ const iso = query.match(/(20\d{2})[-年\/.](\d{1,2})[-月\/.](\d{1,2})日?/);
1521
+ if (iso)
1522
+ return `${iso[1]}-${padDatePart(Number(iso[2]))}-${padDatePart(Number(iso[3]))}`;
1523
+ const cn = query.match(/(?:(20\d{2})年)?(\d{1,2})月(\d{1,2})(?:号|日)?/);
1524
+ if (cn)
1525
+ return `${cn[1] || currentYear}-${padDatePart(Number(cn[2]))}-${padDatePart(Number(cn[3]))}`;
1526
+ return undefined;
1527
+ }
1528
+ function localYear(options) {
1529
+ const explicit = options.localDateNow?.match(/^(20\d{2})-\d{2}-\d{2}$/u)?.[1];
1530
+ if (explicit)
1531
+ return Number(explicit);
1532
+ const now = options.now ?? Date.now();
1533
+ try {
1534
+ const parts = new Intl.DateTimeFormat('en-CA', {
1535
+ timeZone: options.timeZone || 'Asia/Tokyo',
1536
+ year: 'numeric',
1537
+ month: '2-digit',
1538
+ day: '2-digit',
1539
+ }).formatToParts(new Date(now));
1540
+ const year = parts.find((part) => part.type === 'year')?.value;
1541
+ if (year)
1542
+ return Number(year);
1543
+ }
1544
+ catch { /* fall back below */ }
1545
+ return new Date(now).getUTCFullYear();
1546
+ }
1547
+ function padDatePart(value) {
1548
+ return String(value).padStart(2, '0');
1549
+ }
1320
1550
  function cliArg(value) {
1321
1551
  return /^[A-Za-z0-9._:/=@+-]+$/u.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
1322
1552
  }
@@ -1,4 +1,4 @@
1
- export type AgentRecallIntent = 'memory_recall' | 'previous_session_summary' | 'forensic_quote' | 'historical_discussion';
1
+ export type AgentRecallIntent = 'memory_recall' | 'previous_session_summary' | 'forensic_quote' | 'historical_discussion' | 'action_history';
2
2
  export interface AgentRecallQueryCompileInput {
3
3
  query: string;
4
4
  intent?: AgentRecallIntent;
@@ -1,8 +1,9 @@
1
+ import { extractEntityCues, inferOperationalActionCue } from '../utils/EntityCueExtractor.js';
1
2
  const PROTECTED_PHRASES = [
2
3
  'CogMem Memory Context',
3
4
  'Memory Context',
4
5
  'OpenClaw',
5
- 'Hermes',
6
+ '启动',
6
7
  'cogmem',
7
8
  'Obsidian',
8
9
  '记忆内核',
@@ -25,6 +26,7 @@ const PROTECTED_PHRASES = [
25
26
  '报错',
26
27
  '错误',
27
28
  '工具',
29
+ '操作',
28
30
  ];
29
31
  const QUERY_FILLERS = [
30
32
  '我现在不是问你泛泛解释',
@@ -99,6 +101,9 @@ export function inferAgentRecallIntent(query) {
99
101
  if (/原话|怎么说的|完整对话|上一句|下一句|exact quote|verbatim/.test(text)) {
100
102
  return 'forensic_quote';
101
103
  }
104
+ if (/(让你.{0,20}(对|给|把)?.{0,20}(做过|做了|启动|安装|配置|修改|重启|停止|操作|处理|执行)|对.{0,30}(做过什么|做了什么|哪些操作)|what did (i ask you to do|you do) to|operations? on)/i.test(text)) {
105
+ return 'action_history';
106
+ }
102
107
  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)) {
103
108
  return 'historical_discussion';
104
109
  }
@@ -202,6 +207,13 @@ function buildSemanticCuePhrases(keywords, query, anchorText) {
202
207
  out.push('产品コード');
203
208
  out.push('数量');
204
209
  }
210
+ const action = inferOperationalActionCue(text);
211
+ for (const entity of extractEntityCues(text, 4)) {
212
+ out.push(entity.label);
213
+ out.push(`${entity.label} 操作`);
214
+ if (action)
215
+ out.push(`${action.label} ${entity.label}`);
216
+ }
205
217
  return uniqueNonEmpty(out);
206
218
  }
207
219
  function extractTemporalHints(query) {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { serializeUntrustedMemory } from './UntrustedMemorySerializer.js';
2
3
  export function createMemoryUsageReceipt(input) {
3
4
  const createdAt = input.createdAt ?? Date.now();
4
5
  const turnId = input.turnId || `${input.sessionId}:${createdAt}`;
@@ -29,14 +30,14 @@ export function createMemoryUsageReceipt(input) {
29
30
  }
30
31
  export function formatMemoryUsageBridge(receipt, maxChars = 1200) {
31
32
  const lines = [
32
- `<COGMEM_TURN_BRIDGE turn_id="${escapeAttribute(receipt.turnId)}" source="cogmem" compact="true" ttl_turns="${receipt.ttlTurns}" compile_allowed="false">`,
33
+ `<COGMEM_TURN_BRIDGE turn_id="${serializeUntrustedMemory(receipt.turnId, 180)}" source="cogmem" compact="true" ttl_turns="${receipt.ttlTurns}" compile_allowed="false">`,
33
34
  'Previous assistant answer used Cogmem memory.',
34
35
  '',
35
36
  'Used memory themes:',
36
37
  ...listLines(receipt.usedThemes),
37
38
  '',
38
39
  'Working conclusion produced in that turn:',
39
- `- ${receipt.workingConclusion || 'No compact conclusion recorded.'}`,
40
+ `- ${serializeUntrustedMemory(receipt.workingConclusion || 'No compact conclusion recorded.', 260)}`,
40
41
  '',
41
42
  'Source anchors:',
42
43
  ...listLines(receipt.sourceAnchors.map(formatSourceAnchor)),
@@ -96,19 +97,16 @@ function firstSentence(text, limit) {
96
97
  return sentence.length > limit ? `${sentence.slice(0, limit)}...` : sentence;
97
98
  }
98
99
  function listLines(values) {
99
- return values.length > 0 ? values.map((value) => `- ${value}`) : ['- none'];
100
+ return values.length > 0 ? values.map((value) => `- ${serializeUntrustedMemory(value, 260)}`) : ['- none'];
100
101
  }
101
102
  function formatSourceAnchor(anchor) {
102
103
  return [
103
- anchor.memoryId ? `memory:${anchor.memoryId}` : '',
104
- anchor.eventId ? `event:${anchor.eventId}` : '',
105
- anchor.sessionId ? `session:${anchor.sessionId}` : '',
106
- anchor.role ? `role:${anchor.role}` : '',
104
+ anchor.memoryId ? `memory:${serializeUntrustedMemory(anchor.memoryId, 160)}` : '',
105
+ anchor.eventId ? `event:${serializeUntrustedMemory(anchor.eventId, 160)}` : '',
106
+ anchor.sessionId ? `session:${serializeUntrustedMemory(anchor.sessionId, 160)}` : '',
107
+ anchor.role ? `role:${serializeUntrustedMemory(anchor.role, 80)}` : '',
107
108
  ].filter(Boolean).join('; ');
108
109
  }
109
- function escapeAttribute(value) {
110
- return String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
111
- }
112
110
  function clampBlock(text, closingTag, maxChars) {
113
111
  if (text.length <= maxChars)
114
112
  return text;
@@ -1,3 +1,4 @@
1
+ import { serializeUntrustedMemory } from './UntrustedMemorySerializer.js';
1
2
  export function updateSessionWorkingState(previous, input) {
2
3
  const maxChars = Math.max(240, Math.min(4000, Math.floor(input.maxChars ?? previous?.maxChars ?? 1800)));
3
4
  const topic = inferTopic(input.userText) || previous?.currentTopic;
@@ -19,7 +20,7 @@ export function formatSessionWorkingState(state) {
19
20
  const lines = [
20
21
  `<COGMEM_SESSION_STATE scope="current_session" compact="true" persistence="session_only" compile_allowed="false">`,
21
22
  'Current working topic:',
22
- `- ${state.currentTopic || 'unspecified'}`,
23
+ `- ${serializeUntrustedMemory(state.currentTopic || 'unspecified', 180)}`,
23
24
  '',
24
25
  'Current design direction:',
25
26
  ...listLines(state.designDirection),
@@ -72,7 +73,7 @@ function compactSentence(text, limit) {
72
73
  return sentence.length > limit ? `${sentence.slice(0, limit)}...` : sentence;
73
74
  }
74
75
  function listLines(values) {
75
- return values.length > 0 ? values.map((value) => `- ${value}`) : ['- none'];
76
+ return values.length > 0 ? values.map((value) => `- ${serializeUntrustedMemory(value, 260)}`) : ['- none'];
76
77
  }
77
78
  function appendBounded(existing, additions, limit) {
78
79
  const out = [];
@@ -0,0 +1,2 @@
1
+ export declare function serializeUntrustedMemory(input: unknown, limit?: number): string;
2
+ //# sourceMappingURL=UntrustedMemorySerializer.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { stripCogmemRecallBlocks } from './ContextHygiene.js';
2
+ export function serializeUntrustedMemory(input, limit = 500) {
3
+ const clean = stripCogmemRecallBlocks(String(input ?? '')).text
4
+ .replace(/<\/?COGMEM_[A-Z0-9_:-]+[^>]*>/gi, '')
5
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, ' ')
6
+ .replace(/\s+/g, ' ')
7
+ .trim()
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#39;');
13
+ return clean.slice(0, Math.max(0, limit));
14
+ }
@@ -27,6 +27,8 @@ export class ActionFrameExtractor {
27
27
  if (!event.projectId || (event.role !== 'user' && event.role !== 'tool'))
28
28
  continue;
29
29
  const text = eventTextForMemory(event);
30
+ if (!text.trim())
31
+ continue;
30
32
  const markers = actionMarkers(text);
31
33
  if (!markers.length)
32
34
  continue;