diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 995060a..a37f506 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -1379,7 +1379,6 @@ async function handleChatChanged() { const newLength = Array.isArray(chat) ? chat.length : 0; await rollbackSummaryIfNeeded(); - invalidateLexicalIndex(); initButtonsForAll(); const store = getSummaryStore(); diff --git a/modules/story-summary/vector/retrieval/diffusion.js b/modules/story-summary/vector/retrieval/diffusion.js index 848d14a..4bf0d86 100644 --- a/modules/story-summary/vector/retrieval/diffusion.js +++ b/modules/story-summary/vector/retrieval/diffusion.js @@ -53,6 +53,9 @@ const CONFIG = { where: 0.15, // location exact match — binary how: 0.30, // action-term co-occurrence — Jaccard }, + WHERE_MAX_GROUP_SIZE: 16, // skip location-only pair expansion for over-common places + WHERE_FREQ_DAMP_PIVOT: 6, // location freq <= pivot keeps full WHERE score + WHERE_FREQ_DAMP_MIN: 0.20, // lower bound for damped WHERE contribution // Post-verification (Cosine Gate) COSINE_GATE: 0.45, // min cosine(queryVector, stateVector) @@ -275,11 +278,24 @@ function buildGraph(allAtoms, excludeEntities = new Set()) { const features = extractAllFeatures(allAtoms, excludeEntities); const { entityIndex, locationIndex } = buildInvertedIndices(features); + const locationFreq = new Map(); + for (const [loc, indices] of locationIndex.entries()) { + locationFreq.set(loc, indices.length); + } // Candidate pairs: share ≥1 entity or same location const pairSet = new Set(); collectPairsFromIndex(entityIndex, pairSet, N); - collectPairsFromIndex(locationIndex, pairSet, N); + let skippedLocationGroups = 0; + for (const [loc, indices] of locationIndex.entries()) { + if (!loc) continue; + if (indices.length > CONFIG.WHERE_MAX_GROUP_SIZE) { + skippedLocationGroups++; + continue; + } + const oneLocMap = new Map([[loc, indices]]); + collectPairsFromIndex(oneLocMap, pairSet, N); + } // Compute three-channel edge weights for all candidates const neighbors = Array.from({ length: N }, () => []); @@ -294,7 +310,15 @@ function buildGraph(allAtoms, excludeEntities = new Set()) { const fj = features[j]; const wWhat = overlapCoefficient(fi.interactionPairs, fj.interactionPairs); - const wWhere = (fi.location && fi.location === fj.location) ? 1.0 : 0.0; + let wWhere = 0.0; + if (fi.location && fi.location === fj.location) { + const freq = locationFreq.get(fi.location) || 1; + const damp = Math.max( + CONFIG.WHERE_FREQ_DAMP_MIN, + Math.min(1, CONFIG.WHERE_FREQ_DAMP_PIVOT / Math.max(1, freq)) + ); + wWhere = damp; + } const wHow = jaccard(fi.actionTerms, fj.actionTerms); const weight = @@ -318,6 +342,7 @@ function buildGraph(allAtoms, excludeEntities = new Set()) { xbLog.info(MODULE_ID, `Graph: ${N} nodes, ${edgeCount} edges ` + `(what=${channelStats.what} where=${channelStats.where} how=${channelStats.how}) ` + + `(whereSkippedGroups=${skippedLocationGroups}) ` + `(${buildTime}ms)` ); diff --git a/modules/story-summary/vector/retrieval/metrics.js b/modules/story-summary/vector/retrieval/metrics.js index c75a794..63d84c6 100644 --- a/modules/story-summary/vector/retrieval/metrics.js +++ b/modules/story-summary/vector/retrieval/metrics.js @@ -49,6 +49,7 @@ export function createMetrics() { chunkHits: 0, eventHits: 0, searchTime: 0, + indexReadyTime: 0, eventFilteredByDense: 0, floorFilteredByDense: 0, }, @@ -255,6 +256,9 @@ export function formatMetricsLog(metrics) { lines.push(`├─ chunk_hits: ${m.lexical.chunkHits}`); lines.push(`├─ event_hits: ${m.lexical.eventHits}`); lines.push(`├─ search_time: ${m.lexical.searchTime}ms`); + if (m.lexical.indexReadyTime > 0) { + lines.push(`├─ index_ready_time: ${m.lexical.indexReadyTime}ms`); + } if (m.lexical.eventFilteredByDense > 0) { lines.push(`├─ event_filtered_by_dense: ${m.lexical.eventFilteredByDense}`); } @@ -411,7 +415,8 @@ export function formatMetricsLog(metrics) { lines.push(`├─ query_build: ${m.query.buildTime}ms`); lines.push(`├─ query_refine: ${m.query.refineTime}ms`); lines.push(`├─ anchor_search: ${m.timing.anchorSearch}ms`); - lines.push(`├─ lexical_search: ${m.lexical.searchTime}ms`); + const lexicalTotal = (m.lexical.searchTime || 0) + (m.lexical.indexReadyTime || 0); + lines.push(`├─ lexical_search: ${lexicalTotal}ms (query=${m.lexical.searchTime || 0}ms, index_ready=${m.lexical.indexReadyTime || 0}ms)`); lines.push(`├─ fusion: ${m.fusion.time}ms`); lines.push(`├─ constraint_filter: ${m.timing.constraintFilter}ms`); lines.push(`├─ event_retrieval: ${m.timing.eventRetrieval}ms`); diff --git a/modules/story-summary/vector/retrieval/recall.js b/modules/story-summary/vector/retrieval/recall.js index 75382f0..28794eb 100644 --- a/modules/story-summary/vector/retrieval/recall.js +++ b/modules/story-summary/vector/retrieval/recall.js @@ -1091,8 +1091,11 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) { eventIds: [], chunkScores: [], searchTime: 0, }; + let indexReadyTime = 0; try { + const T_Index_Ready = performance.now(); const index = await getLexicalIndex(); + indexReadyTime = Math.round(performance.now() - T_Index_Ready); if (index) { lexicalResult = searchLexicalIndex(index, bundle.lexicalTerms); } @@ -1106,7 +1109,8 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) { metrics.lexical.atomHits = lexicalResult.atomIds.length; metrics.lexical.chunkHits = lexicalResult.chunkIds.length; metrics.lexical.eventHits = lexicalResult.eventIds.length; - metrics.lexical.searchTime = lexTime; + metrics.lexical.searchTime = lexicalResult.searchTime || 0; + metrics.lexical.indexReadyTime = indexReadyTime; metrics.lexical.terms = bundle.lexicalTerms.slice(0, 10); } @@ -1162,7 +1166,7 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) { } xbLog.info(MODULE_ID, - `Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length} mergedEvents=+${lexicalEventCount} filteredByDense=${lexicalEventFilteredByDense} floorFiltered=${metrics.lexical.floorFilteredByDense || 0} (${lexTime}ms)` + `Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length} mergedEvents=+${lexicalEventCount} filteredByDense=${lexicalEventFilteredByDense} floorFiltered=${metrics.lexical.floorFilteredByDense || 0} (indexReady=${indexReadyTime}ms search=${lexicalResult.searchTime || 0}ms total=${lexTime}ms)` ); // ═══════════════════════════════════════════════════════════════════ @@ -1292,7 +1296,7 @@ export async function recallMemory(allEvents, vectorConfig, options = {}) { console.log(`R1 weights: [${r1Weights.map(w => w.toFixed(2)).join(', ')}]`); console.log(`Focus: [${bundle.focusEntities.join(', ')}]`); console.log(`Round 2 Anchors: ${anchorHits.length} hits → ${anchorFloors_dense.size} floors`); - console.log(`Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length} evtMerged=+${lexicalEventCount} evtFiltered=${lexicalEventFilteredByDense} floorFiltered=${metrics.lexical.floorFilteredByDense || 0}`); + console.log(`Lexical: chunks=${lexicalResult.chunkIds.length} events=${lexicalResult.eventIds.length} evtMerged=+${lexicalEventCount} evtFiltered=${lexicalEventFilteredByDense} floorFiltered=${metrics.lexical.floorFilteredByDense || 0} (idx=${indexReadyTime}ms search=${lexicalResult.searchTime || 0}ms total=${lexTime}ms)`); console.log(`Fusion (floor, weighted): dense=${metrics.fusion.denseFloors} lex=${metrics.fusion.lexFloors} → cap=${metrics.fusion.afterCap} (${metrics.fusion.time}ms)`); console.log(`Floor Rerank: ${metrics.evidence.beforeRerank || 0} → ${metrics.evidence.floorsSelected || 0} floors → L0=${metrics.evidence.l0Collected || 0} (${metrics.evidence.rerankTime || 0}ms)`); console.log(`L1: ${metrics.evidence.l1Pulled || 0} pulled → ${metrics.evidence.l1Attached || 0} attached (${metrics.evidence.l1CosineTime || 0}ms)`);