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