Enhance Vectorize All process with error handling, retries and minor improvements (#5479) * fix (vectors): Fixed Vectorize All progress report and ETA issues * fix (vectors): Added strip reasoning block function for extras/WebLLM summaries * feat(vectors): Retry failed summaries with configurable attempts * feat(vectors): Skip summarization for short messages * feat(vectors): Skip failed messages during Vectorize All instead of aborting all Prevents the "Vectorize All" process from stopping on single-message errors. Failed items are now skipped and reported at the end of the session rather than aborting the entire sync. Summarization: Implements per-message retries; failures use the original text as a fallback or mark for skipping. Vector Insertion: Differentiates fatal configuration errors (abort) from transient batch failures (skip and notify). * Resolved: 'account_id_missing' is missing * Resolved: Refactored out summarizeSkipOnFailure() functionality into summarize() via options parameter * Fix eslint and type checks * feat(vectors): add types to maps and sets, improve summarize function options --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

e5d4ff5fae3c7568512ddd67f1114a5f21c4376c

TanJeeSchuan <89920999+TanJeeSchuan@users.noreply.github.com>

Signed
2 files changed, +151 -39Ignore whitespace
public/scripts/extensions/vectors/index.js+141 -39
@@ -44,6 +44,7 @@ import { oai_settings } from '../../openai.js';
4444 * @property {string} text - The hashed message text
4545 * @property {number} hash - The hash used as the vector key
4646 * @property {number} index - The index of the message in the chat
47+ * @property {boolean} [summaryFailed] - Whether summarization failed for this message (used internally to skip messages that fail summarization)
4748 */
4849
4950const MODULE_NAME = 'vectors';
@@ -77,6 +78,8 @@ const settings = {
7778 summarize_sent: false,
7879 summary_source: 'main',
7980 summary_prompt: 'Ignore previous instructions. Summarize the most important parts of the message. Limit yourself to 250 words or less. Your response should include nothing but the summary.',
81+ summary_retries: 2,
82+ summary_threshold: 200,
8083 force_chunk_delimiter: '',
8184
8285 // For chats
@@ -118,7 +121,21 @@ const settings = {
118121
119122const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
120123const webllmProvider = new WebLlmVectorProvider();
124+/**
125+ * Cache for storing summaries of messages by their hash.
126+ * @type {Map<number, string>}
127+ */
121128const cachedSummaries = new Map();
129+/**
130+ * Hashes skipped this Vectorize All session (summary or embed failure). Cleared on next Vectorize All click.
131+ * @type {Set<number>}
132+ */
133+const skippedHashes = new Set();
134+/**
135+ * Error causes treated as fatal — abort Vectorize All rather than skip.
136+ * @type {Set<string>}
137+ */
138+const FATAL_CAUSES = new Set(['account_id_missing', 'api_key_missing', 'api_url_missing', 'api_model_missing', 'extras_module_missing', 'webllm_not_supported', 'summary_endpoint_invalid']);
122139const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
123140
124141/**
@@ -199,10 +216,12 @@ async function onVectorizeAllClick() {
199216 // Clear all cached summaries to ensure that new ones are created
200217 // upon request of a full vectorise
201218 cachedSummaries.clear();
219+ skippedHashes.clear();
202220
203221 const batchSize = getBatchSize();
204222 const elapsedLog = [];
205223 let finished = false;
224+ let initialPending = null; // total items pending at the start of this run — set on first sync return
206225 $('#vectorize_progress').show();
207226 $('#vectorize_progress_percent').text('0');
208227 $('#vectorize_progress_eta').text('...');
@@ -216,16 +235,27 @@ async function onVectorizeAllClick() {
216235 const startTime = Date.now();
217236 const remaining = await synchronizeChat(batchSize);
218237 const elapsed = Date.now() - startTime;
238+
239+ if (remaining === null) {
240+ // synchronizeChat already surfaced a toast; bail out of the loop.
241+ throw new Error('Vectorization aborted');
242+ }
243+
219244 elapsedLog.push(elapsed);
220245 finished = remaining <= 0;
221246
222- const total = getContext().chat.length;
247+ if (initialPending === null) {
223- const processed = total - remaining;
248+ initialPending = Math.max(0, remaining + batchSize);
224- const processedPercent = Math.round((processed / total) * 100); // percentage of the work done
249+ }
250+ const pending = Math.max(0, remaining);
251+ const processed = Math.max(0, initialPending - pending);
252+ const processedPercent = initialPending > 0
253+ ? Math.min(100, Math.round((processed / initialPending) * 100))
254+ : 100;
225255 const lastElapsed = elapsedLog.slice(-5); // last 5 elapsed times
226256 const averageElapsed = lastElapsed.reduce((a, b) => a + b, 0) / lastElapsed.length; // average time needed to process one item
227257 const pace = averageElapsed / batchSize; // time needed to process one item
228258 const remainingTime = Math.round(pace * remainingpending / 1000);
229259
230260 $('#vectorize_progress_percent').text(processedPercent);
231261 $('#vectorize_progress_eta').text(remainingTime);
@@ -234,6 +264,9 @@ async function onVectorizeAllClick() {
234264 throw new Error('Chat changed');
235265 }
236266 }
267+ if (skippedHashes.size > 0) {
268+ toastr.warning(`${skippedHashes.size} message(s) skipped due to errors. Click Vectorize All again to retry.`, 'Vectorization partial');
269+ }
237270 } catch (error) {
238271 console.error('Vectors: Failed to vectorize all', error);
239272 } finally {
@@ -304,7 +337,7 @@ async function summarizeExtra(element) {
304337
305338 if (apiResult.ok) {
306339 const data = await apiResult.json();
307340 element.text = removeReasoningFromString(data.summary);
308341 }
309342 } catch (error) {
310343 console.log(error);
@@ -336,45 +369,70 @@ async function summarizeWebLLM(element) {
336369 }
337370
338371 const messages = [{ role: 'system', content: settings.summary_prompt }, { role: 'user', content: element.text }];
339372 element.text = removeReasoningFromString(await generateWebLlmChatPrompt(messages));
340373
341374 return true;
342375}
343376
344377/**
345378 * SummarizesRuns messagesone usingsummarization attempt for a single element via the chosen methodendpoint.
346379 * @param {HashedMessage[]} hashedMessages Array of hashed messageselement
380+ * @param {string} endpoint
381+ * @returns {Promise<boolean>} Whether the attempt succeeded.
382+ */
383+async function summarizeOne(element, endpoint) {
384+ switch (endpoint) {
385+ case 'main':
386+ return await summarizeMain(element);
387+ case 'extras':
388+ return await summarizeExtra(element);
389+ case 'webllm':
390+ return await summarizeWebLLM(element);
391+ default:
392+ throw new Error(`Unsupported summary endpoint: ${endpoint}`, { cause: 'summary_endpoint_invalid' });
393+ }
394+}
395+
396+/**
397+ * Summarizes messages using the chosen method. Every returned element has been
398+ * summarized (via live call or cache). Throws if any element fails after
399+ * `settings.summary_retries` attempts.
400+ * @param {HashedMessage[]} hashedMessages Array of hashed messages (mutated in place)
347401 * @param {string} endpoint Type of endpoint to use
402+ * @param {Object} [options] Options for summarization behavior
403+ * @param {boolean} [options.skipOnFailure=false] If true, tags failed elements with `summaryFailed = true` instead of throwing
348404 * @returns {Promise<HashedMessage[]>} Summarized messages
349405 */
350406async function summarize(hashedMessages, endpoint = 'main', { skipOnFailure = false } = {}) {
407+ const maxAttempts = Math.max(1, Number(settings.summary_retries) || 1);
351408 for (const element of hashedMessages) {
352409 const cachedSummary = cachedSummaries.get(element.hash);
353410 if (!cachedSummary) {
354411 let successelement.text = truecachedSummary;
355- switch (endpoint) {
412+ continue;
356- case 'main':
413+ }
357- success = await summarizeMain(element);
414+
358- break;
415+ let success = false;
359- case 'extras':
416+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
360- success = await summarizeExtra(element);
417+ try {
361- break;
418+ success = await summarizeOne(element, endpoint);
362- case 'webllm':
419+ if (success) break;
363- success = await summarizeWebLLM(element);
420+ } catch (error) {
364- break;
421+ if (FATAL_CAUSES.has(error?.cause)) throw error;
365- default:
422+ console.warn(`Vectors: summary attempt ${attempt}/${maxAttempts} threw for hash ${element.hash}`, error);
366- console.error('Unsupported endpoint', endpoint);
367- success = false;
368- break;
369423 }
370- if (success) {
424+ console.warn(`Vectors: summary attempt ${attempt}/${maxAttempts} failed for hash ${element.hash}`);
371- cachedSummaries.set(element.hash, element.text);
425+ }
372- } else {
426+ if (!success) {
373- break;
427+ if (skipOnFailure) {
428+ console.warn(`Vectors: summarization exhausted ${maxAttempts} attempt(s) for hash ${element.hash} — marking for skip`);
429+ element.summaryFailed = true;
430+ continue;
374431 }
375- } else {
432+
376- element.text = cachedSummary;
433+ throw new Error(`Summarization failed after ${maxAttempts} attempt(s)`, { cause: 'summary_failed' });
377434 }
435+ cachedSummaries.set(element.hash, element.text);
378436 }
379437 return hashedMessages;
380438}
@@ -401,21 +459,43 @@ async function synchronizeChat(batchSize = 5) {
401459 return -1;
402460 }
403461
462+ /** @type {HashedMessage[]} */
404463 const hashedMessages = context.chat.filter(x => settings.keep_hidden || !x.is_system).map(x => ({ text: String(substituteParams(x.mes)), hash: getStringHash(substituteParams(x.mes)), index: context.chat.indexOf(x) }));
405464 const hashesInCollection = await getSavedHashes(chatId);
406465
407- let newVectorItems = hashedMessages.filter(x => !hashesInCollection.includes(x.hash));
466+ const newVectorItems = hashedMessages
467+ .filter(x => !hashesInCollection.includes(x.hash))
468+ .filter(x => !skippedHashes.has(x.hash));
408469 const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x));
409470
471+ let batch = newVectorItems.slice(0, batchSize);
472+
410473 if (settings.summarize) {
411474 newVectorItemsconst =minLength await= summarizeMath.max(newVectorItems0, Number(settings.summary_sourcesummary_threshold) || 0);
475+ const toSummarize = minLength > 0 ? batch.filter(x => x.text.length >= minLength) : batch;
476+ if (toSummarize.length > 0) {
477+ await summarize(toSummarize, settings.summary_source, { skipOnFailure: true });
478+ const failed = toSummarize.filter(x => x.summaryFailed);
479+ if (failed.length > 0) {
480+ for (const item of failed) skippedHashes.add(item.hash);
481+ batch = batch.filter(x => !x.summaryFailed);
482+ }
483+ }
412484 }
413485
414486 if (newVectorItemsbatch.length > 0) {
415487 const chunkedBatch = splitByChunks(newVectorItems.slice(0, batchSize)batch);
416488
417489 console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSizebatch.length}...`);
418- await insertVectorItems(chatId, chunkedBatch);
490+ try {
491+ await insertVectorItems(chatId, chunkedBatch);
492+ } catch (insertError) {
493+ if (FATAL_CAUSES.has(insertError?.cause)) {
494+ throw insertError;
495+ }
496+ console.warn('Vectors: insert failed for batch — marking for skip', insertError);
497+ for (const item of batch) skippedHashes.add(item.hash);
498+ }
419499 }
420500
421501 if (deletedHashes.length > 0) {
@@ -444,6 +524,10 @@ async function synchronizeChat(batchSize = 5) {
444524 return 'WebLLM extension is not installed or the model is not set.';
445525 case 'account_id_missing':
446526 return 'Workers AI account ID is required. Save it in the "API Connections" panel.';
527+ case 'summary_endpoint_invalid':
528+ return 'Summarization endpoint is not supported.';
529+ case 'summary_failed':
530+ return 'Summarization failed after the configured number of retries.';
447531 default:
448532 return 'Check server console for more details';
449533 }
@@ -453,7 +537,7 @@ async function synchronizeChat(batchSize = 5) {
453537
454538 const message = getErrorMessage(error.cause);
455539 toastr.error(message, 'Vectorization failed', { preventDuplicates: true });
456540 return -1null;
457541 } finally {
458542 syncBlocked = false;
459543 }
@@ -827,7 +911,11 @@ async function getQueryText(chat, initiator) {
827911 .slice(0, settings.query);
828912
829913 if (initiator === 'chat' && settings.enabled_chats && settings.summarize && settings.summarize_sent) {
830914 hashedMessagesconst =minLength await= summarizeMath.max(hashedMessages0, Number(settings.summary_sourcesummary_threshold) || 0);
915+ const toSummarize = minLength > 0 ? hashedMessages.filter(x => x.text.length >= minLength) : hashedMessages;
916+ if (toSummarize.length > 0) {
917+ await summarize(toSummarize, settings.summary_source, { skipOnFailure: true });
918+ }
831919 }
832920
833921 const queryText = hashedMessages.map(x => x.text).join('\n');
@@ -1830,6 +1918,20 @@ export async function init() {
18301918 saveSettingsDebounced();
18311919 });
18321920
1921+ $('#vectors_summary_retries').val(settings.summary_retries).on('input', () => {
1922+ const parsed = Number($('#vectors_summary_retries').val());
1923+ settings.summary_retries = Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : 1;
1924+ Object.assign(extension_settings.vectors, settings);
1925+ saveSettingsDebounced();
1926+ });
1927+
1928+ $('#vectors_summary_threshold').val(settings.summary_threshold).on('input', () => {
1929+ const parsed = Number($('#vectors_summary_threshold').val());
1930+ settings.summary_threshold = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
1931+ Object.assign(extension_settings.vectors, settings);
1932+ saveSettingsDebounced();
1933+ });
1934+
18331935 $('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => {
18341936 settings.message_chunk_size = Number($('#vectors_message_chunk_size').val());
18351937 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -493,6 +493,16 @@
493493 <label for="vectors_summary_prompt" title="Summary Prompt:">Summary Prompt:</label>
494494 <small data-i18n="Only used when Main API or WebLLM Extension is selected.">Only used when Main API or WebLLM Extension is selected.</small>
495495 <textarea id="vectors_summary_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation."></textarea>
496+
497+ <label for="vectors_summary_retries" title="Number of attempts per message before aborting vectorization.">
498+ <span data-i18n="Summarization retries per message">Summarization retries per message</span>
499+ </label>
500+ <input id="vectors_summary_retries" type="number" class="text_pole widthUnset" min="1" max="10" step="1" />
501+
502+ <label for="vectors_summary_threshold" title="Messages shorter than this (in characters) are embedded as-is without summarization. Set to 0 to always summarize.">
503+ <span data-i18n="Summarization min length (chars)">Summarization min length (chars)</span>
504+ </label>
505+ <input id="vectors_summary_threshold" type="number" class="text_pole widthUnset" min="0" step="1" />
496506 </div>
497507 </div>
498508 <small data-i18n="Old messages are vectorized gradually as you chat. To process all previous messages, click the button below.">