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
@@ -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 {
@@ -505,33 +509,67 @@ export class KernelAgentMemoryBackend {
505
509
  const compiledItems = allowsCompiled
506
510
  ? this.compiledItemsForHistoricalQuery(queryPlan, query, limit)
507
511
  : [];
508
- const items = this.mergeHistoricalRecallItems(facetItems, rawItems, graphItems, compiledItems, limit);
512
+ const relevantCompiledItems = this.filterCompiledItemsByQueryCues(compiledItems, queryPlan);
513
+ const items = this.mergeHistoricalRecallItems(facetItems, rawItems, graphItems, relevantCompiledItems, limit);
509
514
  const selectedLane = facetItems.length > 0
510
515
  ? 'facet_graph_raw_ledger'
511
516
  : rawItems.length > 0
512
517
  ? 'raw_ledger'
513
518
  : graphItems.length > 0
514
519
  ? 'graph'
515
- : compiledItems.length > 0
520
+ : relevantCompiledItems.length > 0
516
521
  ? 'compiled'
517
522
  : 'none';
518
523
  return {
519
- recallMode: facetItems.length > 0 || 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',
520
527
  items,
521
- fallbackUsed: true,
528
+ fallbackUsed: facetItems.length === 0,
522
529
  queryPlan,
523
530
  atlasCards: facetResult.cards,
524
531
  relatedButNotSelected: facetResult.relatedButNotSelected,
525
532
  relaxationTrace: facetResult.relaxationTrace,
526
- decisionTrace: recallDecisionTrace(selectedLane, 'historical_discussion', {
533
+ decisionTrace: recallDecisionTrace(selectedLane, queryPlan.intent === 'action_history' ? 'action_history' : 'historical_discussion', {
527
534
  graph: graphItems.length + facetItems.length,
528
- navigation: compiledItems.length,
529
- scopedNavigation: compiledItems.length,
535
+ navigation: relevantCompiledItems.length,
536
+ scopedNavigation: relevantCompiledItems.length,
530
537
  brainFallback: 0,
531
538
  rawLedger: rawItems.length + facetItems.length,
532
539
  }, items.length),
533
540
  };
534
541
  }
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
+ }
535
573
  facetGraphItemsForQuery(query, limit) {
536
574
  try {
537
575
  const atlas = this.kernel.graphExplore(query.query, {
@@ -574,7 +612,7 @@ export class KernelAgentMemoryBackend {
574
612
  canAnswerExactQuote: Boolean(sourceContext),
575
613
  };
576
614
  }
577
- recallForensicAnchor(query, limit) {
615
+ recallForensicAnchor(query, queryPlan, limit) {
578
616
  if (!query.anchorEventId)
579
617
  return [];
580
618
  const context = this.kernel.getEventContext(query.anchorEventId, { before: 4, after: 4 });
@@ -583,8 +621,7 @@ export class KernelAgentMemoryBackend {
583
621
  const candidates = [context.event, ...context.before.slice().reverse(), ...context.after];
584
622
  return candidates
585
623
  .filter((event) => this.isAgentRawEvent(event, query.agentId))
586
- .filter((event) => this.isAllowedSession(event, query))
587
- .filter((event) => this.isAllowedRawEventCollection(event, query.collection))
624
+ .filter((event) => this.isRawEventInRecallScope(event, query, queryPlan.intent))
588
625
  .filter((event) => !this.isOperationalNoiseRawEvent(event))
589
626
  .filter((event) => this.isQuoteSourceEvent(event))
590
627
  .filter((event) => this.hasReadableEventText(event))
@@ -592,7 +629,7 @@ export class KernelAgentMemoryBackend {
592
629
  const anchorDelta = (a.eventId === query.anchorEventId ? 0 : 1) - (b.eventId === query.anchorEventId ? 0 : 1);
593
630
  if (anchorDelta !== 0)
594
631
  return anchorDelta;
595
- return this.quoteEventPriority(a) - this.quoteEventPriority(b);
632
+ return this.quoteEventPriority(a, queryPlan) - this.quoteEventPriority(b, queryPlan);
596
633
  })
597
634
  .slice(0, limit)
598
635
  .map((event) => this.toAgentRawRecallItem(event, {
@@ -625,15 +662,39 @@ export class KernelAgentMemoryBackend {
625
662
  }
626
663
  return out;
627
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
+ }
628
691
  rawLedgerFallbackItemsForQuery(queryPlan, query, limit) {
629
692
  const searchedEvents = this.searchRawEventsByQueryPlan(queryPlan, query, Math.max(limit * 2, 10));
630
693
  const rawEvents = queryPlan.intent === 'forensic_quote'
631
694
  ? this.dedupeRawEventsByTurnPreferUser(searchedEvents)
632
695
  : this.dedupeRawEventsByTurnPreferCue(searchedEvents, queryPlan);
633
696
  return rawEvents
634
- .filter((event) => this.isAgentRawEvent(event, query.agentId))
635
- .filter((event) => this.isAllowedSession(event, query))
636
- .filter((event) => this.isAllowedRawEventCollection(event, query.collection))
697
+ .filter((event) => this.isRawEventInRecallScope(event, query, queryPlan.intent))
637
698
  .filter((event) => !this.isOperationalNoiseRawEvent(event))
638
699
  .slice(0, limit)
639
700
  .map((event) => this.toAgentRawRecallItem(event, {
@@ -655,11 +716,7 @@ export class KernelAgentMemoryBackend {
655
716
  const event = this.kernel.getEventContext(anchor.eventId, { before: 0, after: 0 })?.event;
656
717
  if (!event)
657
718
  continue;
658
- if (!this.isAgentRawEvent(event, query.agentId))
659
- continue;
660
- if (!this.isAllowedSession(event, query))
661
- continue;
662
- if (!this.isAllowedRawEventCollection(event, query.collection))
719
+ if (!this.isRawEventInRecallScope(event, query, queryPlan.intent))
663
720
  continue;
664
721
  if (this.isOperationalNoiseRawEvent(event))
665
722
  continue;
@@ -728,6 +785,28 @@ export class KernelAgentMemoryBackend {
728
785
  .filter((term) => term.length >= 2 && !/^(hermes|openclaw|cogmem)$/i.test(term));
729
786
  return uniqueNonEmpty(terms);
730
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
+ }
731
810
  itemSearchableText(item) {
732
811
  return [
733
812
  item.text,
@@ -788,6 +867,26 @@ export class KernelAgentMemoryBackend {
788
867
  }
789
868
  return out;
790
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
+ }
791
890
  dedupeRawEventsByTurnPreferUser(events) {
792
891
  const byTurn = new Map();
793
892
  for (const event of events) {
@@ -808,6 +907,8 @@ export class KernelAgentMemoryBackend {
808
907
  expandRawSearchTexts(queryPlan) {
809
908
  const hostNeutralKeywords = queryPlan.keywords.filter((keyword) => !/^(hermes|openclaw|cogmem)$/i.test(keyword));
810
909
  return uniqueNonEmpty([
910
+ queryPlan.originalQuery,
911
+ queryPlan.keywords.join(' '),
811
912
  ...queryPlan.searchTexts,
812
913
  hostNeutralKeywords.join(' '),
813
914
  ...hostNeutralKeywords.filter((keyword) => keyword.length >= 2),
@@ -954,6 +1055,21 @@ export class KernelAgentMemoryBackend {
954
1055
  return true;
955
1056
  return false;
956
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
+ }
957
1073
  isOperationalNoiseRawEvent(event) {
958
1074
  const payload = event.payload;
959
1075
  const tags = Array.isArray(payload.metadata?.tags) ? payload.metadata.tags : [];
@@ -965,10 +1081,10 @@ export class KernelAgentMemoryBackend {
965
1081
  }
966
1082
  return isOperationalNoiseText(typeof payload.text === 'string' ? payload.text : JSON.stringify(event.payload));
967
1083
  }
968
- isAllowedSession(event, query) {
1084
+ isAllowedSession(event, query, effectiveIntent = query.intent) {
969
1085
  if (query.excludeSessionId && event.sessionId === query.excludeSessionId)
970
1086
  return false;
971
- 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)
972
1088
  return false;
973
1089
  return true;
974
1090
  }
@@ -978,12 +1094,13 @@ export class KernelAgentMemoryBackend {
978
1094
  || typeof payload.output === 'string'
979
1095
  || typeof payload.title === 'string';
980
1096
  }
981
- quoteEventPriority(event) {
1097
+ quoteEventPriority(event, queryPlan) {
1098
+ const cuePenalty = queryPlan && this.rawEventCueScore(event, queryPlan) > 0 ? 0 : queryPlan ? 10 : 0;
982
1099
  if (event.role === 'user')
983
- return 0;
1100
+ return cuePenalty;
984
1101
  if (event.role === 'assistant')
985
- return 1;
986
- return 2;
1102
+ return cuePenalty + 1;
1103
+ return cuePenalty + 2;
987
1104
  }
988
1105
  isQuoteSourceEvent(event) {
989
1106
  return event.role === 'user' || (!event.role && event.rawEventType === 'message');
@@ -1398,6 +1515,38 @@ function uniqueNonEmpty(values) {
1398
1515
  function laneAllowed(policy, lane) {
1399
1516
  return !policy || policy.allowedLanes.includes(lane);
1400
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
+ }
1401
1550
  function cliArg(value) {
1402
1551
  return /^[A-Za-z0-9._:/=@+-]+$/u.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
1403
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;
@@ -1,4 +1,5 @@
1
1
  import { eventTextForMemory } from '../episode/CogmemBlockStripper.js';
2
+ import { extractEntityCues, inferOperationalActionCue } from '../utils/EntityCueExtractor.js';
2
3
  const TITLE_RULES = [
3
4
  {
4
5
  id: 'memory_context_blackbox',
@@ -54,10 +55,12 @@ export class EpisodeTitleGenerator {
54
55
  const assistantText = normalizeText(assistantEvents.map(eventTextForMemory).join('\n'));
55
56
  const summaryText = normalizeText([input.summary, input.topicPath, input.episodeType].filter(Boolean).join('\n'));
56
57
  const preferredText = [userText, summaryText, assistantText].filter(Boolean).join('\n');
58
+ const operationTitle = inferOperationTitle(preferredText);
57
59
  const matchedRule = TITLE_RULES.find((rule) => rule.test(preferredText));
58
60
  const localDate = inferLocalDate(input.events, input.startedAt);
59
61
  const sourceEventIds = sourceIdsFor(userEvents.length > 0 ? userEvents : input.events);
60
62
  if (matchedRule) {
63
+ const matchedSourceEventIds = sourceIdsForMatching(userEvents.length > 0 ? userEvents : input.events, matchedRule.test) || sourceEventIds;
61
64
  return {
62
65
  displayTitle: boundTitle(matchedRule.displayTitle),
63
66
  oneLineSummary: matchedRule.oneLineSummary,
@@ -68,7 +71,7 @@ export class EpisodeTitleGenerator {
68
71
  localDate,
69
72
  confidence: matchedRule.confidence,
70
73
  reviewNeeded: matchedRule.confidence < 0.65,
71
- sourceEventIds,
74
+ sourceEventIds: matchedSourceEventIds,
72
75
  generatorTrace: {
73
76
  usedUserText: userText.length > 0,
74
77
  usedAssistantText: userText.length === 0 && assistantText.length > 0,
@@ -77,6 +80,27 @@ export class EpisodeTitleGenerator {
77
80
  },
78
81
  };
79
82
  }
83
+ if (operationTitle) {
84
+ const matchedSourceEventIds = sourceIdsForMatching(userEvents.length > 0 ? userEvents : input.events, (text) => Boolean(inferOperationTitle(text))) || sourceEventIds;
85
+ return {
86
+ displayTitle: boundTitle(operationTitle.displayTitle),
87
+ oneLineSummary: operationTitle.oneLineSummary,
88
+ topicHints: operationTitle.topicHints,
89
+ issueHints: [],
90
+ eventKind: 'operation',
91
+ userIntent: 'action_history',
92
+ localDate,
93
+ confidence: 0.82,
94
+ reviewNeeded: false,
95
+ sourceEventIds: matchedSourceEventIds,
96
+ generatorTrace: {
97
+ usedUserText: userText.length > 0,
98
+ usedAssistantText: userText.length === 0 && assistantText.length > 0,
99
+ fallback: false,
100
+ matchedRules: ['generic_entity_operation'],
101
+ },
102
+ };
103
+ }
80
104
  const fallbackTitle = fallbackDisplayTitle({ userText, summaryText, topicPath: input.topicPath, episodeType: input.episodeType });
81
105
  return {
82
106
  displayTitle: fallbackTitle,
@@ -100,9 +124,24 @@ export class EpisodeTitleGenerator {
100
124
  function normalizeText(text) {
101
125
  return text.replace(/\s+/g, ' ').trim();
102
126
  }
127
+ function inferOperationTitle(text) {
128
+ const action = inferOperationalActionCue(text);
129
+ const entity = extractEntityCues(text, 1)[0];
130
+ if (!action || !entity)
131
+ return undefined;
132
+ return {
133
+ displayTitle: `${entity.label} ${action.label}`,
134
+ oneLineSummary: `用户要求对 ${entity.label} 执行${action.verb}相关操作。`,
135
+ topicHints: [entity.id],
136
+ };
137
+ }
103
138
  function sourceIdsFor(events) {
104
139
  return events.map((event) => event.eventId).filter(Boolean).slice(0, 20);
105
140
  }
141
+ function sourceIdsForMatching(events, test) {
142
+ const matched = events.filter((event) => test(eventTextForMemory(event)));
143
+ return matched.length ? sourceIdsFor(matched) : undefined;
144
+ }
106
145
  function inferLocalDate(events, startedAt) {
107
146
  for (const event of events) {
108
147
  const candidate = event.localDate;
@@ -140,8 +179,8 @@ function inferTopicHints(text) {
140
179
  hints.add('memory');
141
180
  if (/openclaw/i.test(text))
142
181
  hints.add('openclaw');
143
- if (/hermes/i.test(text))
144
- hints.add('hermes');
182
+ for (const entity of extractEntityCues(text, 4))
183
+ hints.add(entity.id);
145
184
  return Array.from(hints);
146
185
  }
147
186
  function normalizeKind(kind) {
@@ -24,6 +24,8 @@ export interface FacetQueryPlan {
24
24
  export interface FacetQueryPlannerOptions {
25
25
  projectId: string;
26
26
  now?: number;
27
+ localDateNow?: string;
28
+ timeZone?: string;
27
29
  }
28
30
  export declare class FacetQueryPlanner {
29
31
  plan(query: string, options: FacetQueryPlannerOptions): FacetQueryPlan;