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.
- package/CHANGELOG.md +12 -0
- package/MEMORY_ATLAS.md +16 -6
- package/README.md +16 -11
- package/RELEASE_CHECKLIST.md +4 -4
- package/dist/agent/AgentMemoryBackend.d.ts +11 -2
- package/dist/agent/AgentMemoryBackend.js +185 -36
- package/dist/agent/AgentRecallQueryCompiler.d.ts +1 -1
- package/dist/agent/AgentRecallQueryCompiler.js +13 -1
- package/dist/agent/MemoryUsageReceipt.js +8 -10
- package/dist/agent/SessionWorkingState.js +3 -2
- package/dist/agent/UntrustedMemorySerializer.d.ts +2 -0
- package/dist/agent/UntrustedMemorySerializer.js +14 -0
- package/dist/atlas/ActionFrameExtractor.js +2 -0
- package/dist/atlas/EpisodeTitleGenerator.js +42 -3
- package/dist/atlas/FacetQueryPlanner.d.ts +2 -0
- package/dist/atlas/FacetQueryPlanner.js +57 -16
- package/dist/atlas/GraphCurator.d.ts +3 -0
- package/dist/atlas/GraphCurator.js +118 -14
- package/dist/atlas/MemoryAtlasIndexer.d.ts +12 -0
- package/dist/atlas/MemoryAtlasIndexer.js +36 -0
- package/dist/atlas/MemoryAtlasQueryCompiler.js +10 -7
- package/dist/atlas/MemoryAtlasService.js +66 -22
- package/dist/bin/memory.js +14 -4
- package/dist/factory.d.ts +18 -0
- package/dist/factory.js +41 -2
- package/dist/host/openclaw/AutoMemoryPluginInstaller.js +71 -44
- package/dist/mcp/server.js +1 -1
- package/dist/recall/RecallExplanation.d.ts +1 -1
- package/dist/store/MemoryAtlasStore.d.ts +3 -1
- package/dist/store/MemoryAtlasStore.js +149 -25
- package/dist/utils/ActionKindRegistry.d.ts +10 -0
- package/dist/utils/ActionKindRegistry.js +18 -0
- package/dist/utils/EntityCueExtractor.d.ts +13 -0
- package/dist/utils/EntityCueExtractor.js +81 -0
- package/examples/hermes-backend/AGENTS.md +1 -1
- package/examples/hermes-backend/README.md +1 -1
- package/examples/hermes-backend/SKILL.md +10 -3
- package/examples/hermes-backend/references/operations.md +9 -8
- package/examples/openclaw-backend/AGENTS.md +1 -1
- package/examples/openclaw-backend/README.md +2 -2
- package/examples/openclaw-backend/SKILL.md +24 -7
- package/examples/openclaw-backend/references/operations.md +26 -10
- 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 (
|
|
253
|
+
if (queryPlan.intent === 'previous_session_summary') {
|
|
252
254
|
return this.recallPreviousSession(query, queryPlan);
|
|
253
255
|
}
|
|
254
|
-
if (
|
|
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.
|
|
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
|
-
:
|
|
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.
|
|
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
|
|
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
|
-
:
|
|
520
|
+
: relevantCompiledItems.length > 0
|
|
516
521
|
? 'compiled'
|
|
517
522
|
: 'none';
|
|
518
523
|
return {
|
|
519
|
-
recallMode: facetItems.length > 0
|
|
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:
|
|
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:
|
|
529
|
-
scopedNavigation:
|
|
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.
|
|
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.
|
|
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.
|
|
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 &&
|
|
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
|
|
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
|
-
'
|
|
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="${
|
|
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, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
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,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, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
144
|
-
hints.add(
|
|
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;
|