@yeaft/webchat-agent 1.0.406 → 1.0.407
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/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/dream/triage.js +4 -3
- package/yeaft/engine.js +6 -4
- package/yeaft/memory/ams.js +5 -4
- package/yeaft/memory/keywords.js +0 -1
- package/yeaft/memory/preflow.js +81 -3
- package/yeaft/memory/prompt-cleanup.js +75 -4
- package/yeaft/memory/segment-store.js +2 -2
- package/yeaft/sessions/pre-flow.js +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.407"}
|
package/package.json
CHANGED
package/yeaft/dream/triage.js
CHANGED
|
@@ -145,6 +145,7 @@ export function buildPass2Prompt(ctx) {
|
|
|
145
145
|
* Run soft classification for one segment of one group's diff.
|
|
146
146
|
*
|
|
147
147
|
* @param {{
|
|
148
|
+
* root?: string,
|
|
148
149
|
* sessionId: string,
|
|
149
150
|
* messages: Array<object>,
|
|
150
151
|
* topicSummaries: Array<{ path: string, summary: string }>,
|
|
@@ -152,7 +153,7 @@ export function buildPass2Prompt(ctx) {
|
|
|
152
153
|
* }} args
|
|
153
154
|
* @returns {Promise<Array<{ kind: 'update'|'create', scope: string }>>}
|
|
154
155
|
*/
|
|
155
|
-
export async function classifySoft({ sessionId, messages, topicSummaries, llm, language }) {
|
|
156
|
+
export async function classifySoft({ root, sessionId, messages, topicSummaries, llm, language }) {
|
|
156
157
|
if (!llm) throw new Error('triage.classifySoft: llm callable required');
|
|
157
158
|
const pass1Prompt = buildPass1Prompt({ sessionId, messages, topicSummaries, language });
|
|
158
159
|
const pass1Raw = await llm({ pass: 'triage-pass1', prompt: pass1Prompt, system: triageSystem(language) });
|
|
@@ -183,8 +184,8 @@ export async function classifySoft({ sessionId, messages, topicSummaries, llm, l
|
|
|
183
184
|
const segs = path.split('/').filter(Boolean);
|
|
184
185
|
if (!sessionId || sessionId === '_no-session') continue;
|
|
185
186
|
if (!isValidTopic({ kind: 'session-topic', sessionId, path: segs })) continue;
|
|
186
|
-
const redirected =
|
|
187
|
-
? resolveTopicRedirect(
|
|
187
|
+
const redirected = root
|
|
188
|
+
? resolveTopicRedirect(root, sessionId, segs.join('/'))
|
|
188
189
|
: segs.join('/');
|
|
189
190
|
const scope = `sessions/${sessionId}/topic/${redirected}`;
|
|
190
191
|
if (pass2.decision === 'match') {
|
package/yeaft/engine.js
CHANGED
|
@@ -1486,7 +1486,7 @@ export class Engine {
|
|
|
1486
1486
|
* without injection.
|
|
1487
1487
|
*
|
|
1488
1488
|
* @param {string} prompt
|
|
1489
|
-
* @param {{ sessionId?: string, vpId?: string, extraScopes?: string[] }} [ctx]
|
|
1489
|
+
* @param {{ sessionId?: string, vpId?: string, extraScopes?: string[], strictScopes?: string[] }} [ctx]
|
|
1490
1490
|
* @returns {Promise<{ profile: string, entries: object[], formatted: string }|null>}
|
|
1491
1491
|
*/
|
|
1492
1492
|
async #recallMemory(prompt, ctx = {}) {
|
|
@@ -1499,6 +1499,7 @@ export class Engine {
|
|
|
1499
1499
|
chatId: ctx.chatId || this.#chatId,
|
|
1500
1500
|
vpId: ctx.vpId,
|
|
1501
1501
|
extraScopes: ctx.extraScopes,
|
|
1502
|
+
strictScopes: ctx.strictScopes,
|
|
1502
1503
|
pickLimit: resolveMemoryRecallLimit(this.#config),
|
|
1503
1504
|
uniqueScopes: true,
|
|
1504
1505
|
canonicalOnly: true,
|
|
@@ -2300,11 +2301,8 @@ export class Engine {
|
|
|
2300
2301
|
const projectScopesForMemory = Array.isArray(projectSessionIds)
|
|
2301
2302
|
? projectSessionIds.flatMap(id => [
|
|
2302
2303
|
`sessions/${id}`,
|
|
2303
|
-
`sessions/${id}/user`,
|
|
2304
2304
|
`session/${id}`,
|
|
2305
|
-
`session/${id}/user`,
|
|
2306
2305
|
`group/${id}`,
|
|
2307
|
-
`group/${id}/user`,
|
|
2308
2306
|
])
|
|
2309
2307
|
: [];
|
|
2310
2308
|
const recallResult = await this.#recallMemory(prompt, {
|
|
@@ -2313,6 +2311,10 @@ export class Engine {
|
|
|
2313
2311
|
? vpPersona.vpId
|
|
2314
2312
|
: (typeof senderVpId === 'string' ? senderVpId : undefined),
|
|
2315
2313
|
extraScopes: [...topicScopesForMemory, ...projectScopesForMemory],
|
|
2314
|
+
// Global user memory and Project siblings are much broader than the
|
|
2315
|
+
// active Session. A single generic OR-FTS hit must not pull an entire
|
|
2316
|
+
// historical content.md into the provider prompt.
|
|
2317
|
+
strictScopes: ['user', ...projectScopesForMemory],
|
|
2316
2318
|
});
|
|
2317
2319
|
recallEntryCount = recallResult && Array.isArray(recallResult.entries)
|
|
2318
2320
|
? recallResult.entries.length
|
package/yeaft/memory/ams.js
CHANGED
|
@@ -18,6 +18,7 @@ import { approxTokens } from './budget.js';
|
|
|
18
18
|
import {
|
|
19
19
|
cleanMemoryPromptText,
|
|
20
20
|
filterMemoryPromptTextForPrompt,
|
|
21
|
+
filterRelatedSessionPromptText,
|
|
21
22
|
isDuplicateMemoryText,
|
|
22
23
|
rememberMemoryText,
|
|
23
24
|
} from './prompt-cleanup.js';
|
|
@@ -172,11 +173,11 @@ export class ActiveMemorySet {
|
|
|
172
173
|
const { picked: resPicked, cost: resCost } = pickMemoryItems({
|
|
173
174
|
items: [...this._resident.entries()].map(([scope, entry]) => ({
|
|
174
175
|
scope,
|
|
175
|
-
// Related-Session
|
|
176
|
-
//
|
|
177
|
-
//
|
|
176
|
+
// Related-Session content is broad historical context. Even after FTS
|
|
177
|
+
// selects its scope, keep only chunks supported by the current user turn;
|
|
178
|
+
// otherwise one weak term can drag an entire old PR/session into prompt.
|
|
178
179
|
summary: entry.category === 'experience'
|
|
179
|
-
?
|
|
180
|
+
? filterRelatedSessionPromptText(entry.summary, userMsg)
|
|
180
181
|
: filterMemoryPromptTextForPrompt(entry.summary, userMsg),
|
|
181
182
|
...(entry.category ? { category: entry.category } : {}),
|
|
182
183
|
})),
|
package/yeaft/memory/keywords.js
CHANGED
package/yeaft/memory/preflow.js
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
import { extractKeywords } from './keywords.js';
|
|
19
19
|
import { approxTokens } from './budget.js';
|
|
20
20
|
import { isVpForeign } from './store.js';
|
|
21
|
+
import { isTransientMemoryText, promptRelevanceTokens } from './prompt-cleanup.js';
|
|
21
22
|
|
|
22
23
|
export const DEFAULT_PICK_LIMIT = 8;
|
|
24
|
+
const STRICT_SCOPE_MIN_QUERY_TERMS = 2;
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* @typedef {object} PreflowOptions
|
|
@@ -32,6 +34,7 @@ export const DEFAULT_PICK_LIMIT = 8;
|
|
|
32
34
|
* @property {number} [pickLimit] max picked segments (default 8)
|
|
33
35
|
* @property {boolean} [uniqueScopes] pick at most one best hit per scope
|
|
34
36
|
* @property {boolean} [canonicalOnly] search canonical content records only
|
|
37
|
+
* @property {string[]} [strictScopes] scopes that require multiple distinct query-term matches
|
|
35
38
|
*/
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -42,6 +45,7 @@ export const DEFAULT_PICK_LIMIT = 8;
|
|
|
42
45
|
* @property {import('./segment.js').Segment[]} picked
|
|
43
46
|
* @property {number} pickedTokens
|
|
44
47
|
* @property {number} droppedCount
|
|
48
|
+
* @property {number} droppedByRelevance
|
|
45
49
|
*/
|
|
46
50
|
|
|
47
51
|
/**
|
|
@@ -61,12 +65,13 @@ export function runPreflow(index, opts) {
|
|
|
61
65
|
? opts.budgetTokens : Infinity;
|
|
62
66
|
const pickLimit = Number.isFinite(opts.pickLimit) && opts.pickLimit > 0
|
|
63
67
|
? Math.floor(opts.pickLimit) : DEFAULT_PICK_LIMIT;
|
|
68
|
+
const strictScopes = new Set(Array.isArray(opts.strictScopes) ? opts.strictScopes : []);
|
|
64
69
|
|
|
65
70
|
const keywords = extractKeywords(userMsg);
|
|
66
71
|
if (keywords.length === 0) {
|
|
67
72
|
return {
|
|
68
73
|
keywords: [], ftsQuery: '', hits: [],
|
|
69
|
-
picked: [], pickedTokens: 0, droppedCount: 0,
|
|
74
|
+
picked: [], pickedTokens: 0, droppedCount: 0, droppedByRelevance: 0,
|
|
70
75
|
};
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -75,7 +80,7 @@ export function runPreflow(index, opts) {
|
|
|
75
80
|
if (scopeFilter.length === 0) {
|
|
76
81
|
return {
|
|
77
82
|
keywords, ftsQuery, hits: [],
|
|
78
|
-
picked: [], pickedTokens: 0, droppedCount: 0,
|
|
83
|
+
picked: [], pickedTokens: 0, droppedCount: 0, droppedByRelevance: 0,
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -91,7 +96,13 @@ export function runPreflow(index, opts) {
|
|
|
91
96
|
const pickedScopes = new Set();
|
|
92
97
|
let cost = 0;
|
|
93
98
|
let dropped = 0;
|
|
99
|
+
let droppedByRelevance = 0;
|
|
94
100
|
for (const h of reranked) {
|
|
101
|
+
if (strictScopes.has(h.scope) && !passesStrictScopeGate(h, userMsg)) {
|
|
102
|
+
dropped += 1;
|
|
103
|
+
droppedByRelevance += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
95
106
|
if (opts.uniqueScopes && pickedScopes.has(h.scope)) {
|
|
96
107
|
dropped += 1;
|
|
97
108
|
continue;
|
|
@@ -108,10 +119,77 @@ export function runPreflow(index, opts) {
|
|
|
108
119
|
|
|
109
120
|
return {
|
|
110
121
|
keywords, ftsQuery, hits: reranked,
|
|
111
|
-
picked, pickedTokens: cost, droppedCount: dropped,
|
|
122
|
+
picked, pickedTokens: cost, droppedCount: dropped, droppedByRelevance,
|
|
112
123
|
};
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
function passesStrictScopeGate(hit, userMsg) {
|
|
127
|
+
const queryTerms = promptRelevanceTokens(userMsg);
|
|
128
|
+
if (matchedStrictQueryTermCount(hit, queryTerms) >= STRICT_SCOPE_MIN_QUERY_TERMS) return true;
|
|
129
|
+
|
|
130
|
+
// Broad scopes still need a narrow path for exact entity lookups. The
|
|
131
|
+
// canonical record's tags are derived from its entire body, so an exact tag is
|
|
132
|
+
// not strong evidence. Require the whole one-term query to equal an authored
|
|
133
|
+
// Markdown heading instead; generic sentences and body-only hits stay gated.
|
|
134
|
+
return isSingleDiscriminativeQuery(userMsg, queryTerms) && hasExactCanonicalHeading(hit, userMsg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isSingleDiscriminativeQuery(userMsg, queryTerms) {
|
|
138
|
+
if (queryTerms.size !== 1 || isTransientMemoryText(userMsg)) return false;
|
|
139
|
+
const words = String(userMsg || '')
|
|
140
|
+
.normalize('NFKC')
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.match(/[\p{L}\p{N}_]+/gu) || [];
|
|
143
|
+
return words.length === 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function matchedStrictQueryTermCount(hit, queryTerms) {
|
|
147
|
+
const haystack = promptRelevanceTokens(
|
|
148
|
+
`${hit?.body || ''}\n${Array.isArray(hit?.tags) ? hit.tags.join(' ') : ''}`,
|
|
149
|
+
);
|
|
150
|
+
let matched = 0;
|
|
151
|
+
for (const term of queryTerms) {
|
|
152
|
+
if (haystack.has(term)) matched += 1;
|
|
153
|
+
}
|
|
154
|
+
return matched;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hasExactCanonicalHeading(hit, userMsg) {
|
|
158
|
+
const query = normalizeStrongEvidenceText(userMsg);
|
|
159
|
+
if (!query) return false;
|
|
160
|
+
|
|
161
|
+
let fence = null;
|
|
162
|
+
for (const line of String(hit?.body || '').split(/\r?\n/)) {
|
|
163
|
+
if (fence) {
|
|
164
|
+
const closing = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
|
|
165
|
+
if (closing && closing[1][0] === fence.marker && closing[1].length >= fence.length) {
|
|
166
|
+
fence = null;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const opening = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
172
|
+
if (opening && !(opening[1][0] === '`' && opening[2].includes('`'))) {
|
|
173
|
+
fence = { marker: opening[1][0], length: opening[1].length };
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (/^(?: {4}| {0,3}\t)/.test(line)) continue;
|
|
177
|
+
|
|
178
|
+
const match = /^ {0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/.exec(line);
|
|
179
|
+
if (match && normalizeStrongEvidenceText(match[1]) === query) return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeStrongEvidenceText(text) {
|
|
185
|
+
return String(text || '')
|
|
186
|
+
.normalize('NFKC')
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
.replace(/[`*_~[\]()<>{}.,,。::;;!!??"'“”‘’]/g, ' ')
|
|
189
|
+
.replace(/\s+/g, ' ')
|
|
190
|
+
.trim();
|
|
191
|
+
}
|
|
192
|
+
|
|
115
193
|
/**
|
|
116
194
|
* Compose an FTS5 MATCH query from keywords. Each keyword is OR'd with
|
|
117
195
|
* a prefix wildcard so morphological variants match. We escape any
|
|
@@ -20,9 +20,15 @@ const COMMON_INTENT_TOKENS = new Set([
|
|
|
20
20
|
'merge', 'merged', 'review', 'reviewed', 'release', 'tag', 'tags',
|
|
21
21
|
'pr', 'pull', 'request', 'issue', 'fix', 'feat', 'test', 'tests',
|
|
22
22
|
'dream', 'memory', 'session', 'topic', 'status', 'blocker', 'blocked',
|
|
23
|
-
'
|
|
24
|
-
'合并', '评审', '发布', '标签', '记忆', '主题', '阻塞', '正在',
|
|
23
|
+
'yeaft',
|
|
25
24
|
]);
|
|
25
|
+
const COMMON_CJK_INTENT_PHRASES = [
|
|
26
|
+
'当前', '状态', '任务', '工作项', '工作', '待办', '下一步', '完成', '已完成',
|
|
27
|
+
'合并', '评审', '发布', '标签', '记忆', '主题', '阻塞', '正在',
|
|
28
|
+
'内容', '用户', '要求', '需求', '设计', '决策', '搜索', '发现', '需要', '应该',
|
|
29
|
+
'添加', '无关', '不相干', '完全', '明细', '看看', '还有', '这个',
|
|
30
|
+
'比如', '每个', '如果',
|
|
31
|
+
].sort((a, b) => b.length - a.length);
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Remove Dream scheduler metadata blocks from memory text.
|
|
@@ -126,6 +132,43 @@ export function filterMemoryPromptTextForPrompt(text, userText) {
|
|
|
126
132
|
return joinMemoryPromptChunks(kept).trim();
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Project sibling content is a broad historical source, not resident state for
|
|
137
|
+
* the active Session. Keep only chunks with concrete lexical support from the
|
|
138
|
+
* current user turn; if none match, the sibling contributes no prompt block.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} text
|
|
141
|
+
* @param {string} userText
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
export function filterRelatedSessionPromptText(text, userText) {
|
|
145
|
+
const cleaned = cleanMemoryPromptText(text);
|
|
146
|
+
if (!cleaned || !userText) return '';
|
|
147
|
+
const kept = [];
|
|
148
|
+
let governingHeading = '';
|
|
149
|
+
let emittedHeading = '';
|
|
150
|
+
for (const chunk of splitMemoryPromptChunks(cleaned)) {
|
|
151
|
+
if (/^#{1,6}\s+\S/.test(chunk)) {
|
|
152
|
+
governingHeading = chunk;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const headingRelevant = governingHeading && isMemoryPromptRelevant(governingHeading, userText);
|
|
156
|
+
const chunkRelevant = isMemoryPromptRelevant(chunk, userText);
|
|
157
|
+
const headingCarriesSpecificTerm = headingRelevant && hasSpecificPromptTerms(governingHeading);
|
|
158
|
+
if (!chunkRelevant && (
|
|
159
|
+
!headingCarriesSpecificTerm
|
|
160
|
+
|| !hasConcretePromptTerms(chunk)
|
|
161
|
+
|| hasOperationalMemoryMarker(chunk)
|
|
162
|
+
)) continue;
|
|
163
|
+
if (governingHeading && emittedHeading !== governingHeading) {
|
|
164
|
+
kept.push(governingHeading);
|
|
165
|
+
emittedHeading = governingHeading;
|
|
166
|
+
}
|
|
167
|
+
kept.push(chunk);
|
|
168
|
+
}
|
|
169
|
+
return joinMemoryPromptChunks(kept).trim();
|
|
170
|
+
}
|
|
171
|
+
|
|
129
172
|
function splitMemoryPromptChunks(text) {
|
|
130
173
|
const chunks = [];
|
|
131
174
|
for (const block of String(text || '').split(/\n{2,}/)) {
|
|
@@ -177,13 +220,41 @@ export function promptRelevanceTokens(text) {
|
|
|
177
220
|
if (!COMMON_INTENT_TOKENS.has(token)) out.add(token);
|
|
178
221
|
}
|
|
179
222
|
for (const match of cleaned.matchAll(CJK_RUN_RE)) {
|
|
180
|
-
for (const
|
|
181
|
-
|
|
223
|
+
for (const run of removeCommonCjkIntentPhrases(match[0])) {
|
|
224
|
+
for (const token of cjkBigrams(run)) out.add(token);
|
|
182
225
|
}
|
|
183
226
|
}
|
|
184
227
|
return out;
|
|
185
228
|
}
|
|
186
229
|
|
|
230
|
+
function hasConcretePromptTerms(text) {
|
|
231
|
+
return promptRelevanceTokens(text).size > 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hasOperationalMemoryMarker(text) {
|
|
235
|
+
return /\b(work\s*item|todo|next\s+step|blocker|blocked|pr\s*#?\d+|pull\s+request|review|merge\s+commit|release\s+tag|tag\s+v\d|v\d+\.\d+\.\d+)\b|(?:工作项|当前(?:状态|任务|工作)|正在|待办|下一步|阻塞|评审|合并|发布|已推|已合并|已完成)/i.test(String(text || ''));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function hasSpecificPromptTerms(text) {
|
|
239
|
+
const cleaned = cleanMemoryPromptText(text).toLowerCase();
|
|
240
|
+
for (const match of cleaned.matchAll(ASCII_WORD_RE)) {
|
|
241
|
+
const token = match[0];
|
|
242
|
+
if (!COMMON_INTENT_TOKENS.has(token) && token !== 'yeaft') return true;
|
|
243
|
+
}
|
|
244
|
+
for (const match of cleaned.matchAll(CJK_RUN_RE)) {
|
|
245
|
+
const run = match[0];
|
|
246
|
+
if (run.length === 2 && !COMMON_CJK_INTENT_PHRASES.includes(run)) return true;
|
|
247
|
+
if (removeCommonCjkIntentPhrases(run).some(part => part.length >= 2)) return true;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function removeCommonCjkIntentPhrases(text) {
|
|
253
|
+
let cleaned = text;
|
|
254
|
+
for (const phrase of COMMON_CJK_INTENT_PHRASES) cleaned = cleaned.split(phrase).join(' ');
|
|
255
|
+
return cleaned.match(CJK_RUN_RE) || [];
|
|
256
|
+
}
|
|
257
|
+
|
|
187
258
|
function cjkBigrams(text) {
|
|
188
259
|
const out = [];
|
|
189
260
|
for (let i = 0; i < text.length - 1; i += 1) out.push(text.slice(i, i + 2));
|
|
@@ -72,8 +72,8 @@ export function writeScope(memoryRoot, scope, segments) {
|
|
|
72
72
|
export function readCanonicalContentRecord(memoryRoot, scope) {
|
|
73
73
|
const path = join(memoryRoot, scope, 'content.md');
|
|
74
74
|
if (!existsSync(path)) return null;
|
|
75
|
-
const body = readFileSync(path, 'utf8')
|
|
76
|
-
if (!body) return null;
|
|
75
|
+
const body = readFileSync(path, 'utf8');
|
|
76
|
+
if (!body.trim()) return null;
|
|
77
77
|
const stat = statSync(path);
|
|
78
78
|
const timestamp = stat.mtime.toISOString();
|
|
79
79
|
const digest = createHash('sha256').update(scope).digest('hex').slice(0, 12);
|
|
@@ -262,6 +262,7 @@ export function formatPickedForInjection(picked) {
|
|
|
262
262
|
* @property {number} [pickLimit] Max picked segments (default 8)
|
|
263
263
|
* @property {boolean} [uniqueScopes] Pick only the best hit per scope
|
|
264
264
|
* @property {boolean} [canonicalOnly] Search canonical content records only
|
|
265
|
+
* @property {string[]} [strictScopes] Additional scopes that require multiple distinct query-term matches
|
|
265
266
|
* @property {boolean} [fallbackOnEmpty] Include bounded recent scoped segments when FTS has no hits
|
|
266
267
|
* @property {number} [fallbackPerScope] Max fallback segments per scope
|
|
267
268
|
*/
|
|
@@ -359,6 +360,7 @@ export function runMemoryPreflow(index, opts) {
|
|
|
359
360
|
pickLimit: opts.pickLimit,
|
|
360
361
|
uniqueScopes: opts.uniqueScopes === true,
|
|
361
362
|
canonicalOnly: opts.canonicalOnly === true,
|
|
363
|
+
strictScopes: opts.strictScopes,
|
|
362
364
|
});
|
|
363
365
|
|
|
364
366
|
let fallbackUsed = false;
|
|
@@ -396,6 +398,7 @@ export function runMemoryPreflow(index, opts) {
|
|
|
396
398
|
ftsQuery: result.ftsQuery,
|
|
397
399
|
pickedTokens: result.pickedTokens,
|
|
398
400
|
droppedCount: result.droppedCount,
|
|
401
|
+
droppedByRelevance: result.droppedByRelevance || 0,
|
|
399
402
|
hitCount: (result.hits || []).length,
|
|
400
403
|
fallbackUsed,
|
|
401
404
|
},
|