Merge pull request #2468 from SillyTavern/wi-scan-state Fix min activations for non-recursable entries

81f65203540ffda9b70b61732d2978475acff4b6

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
1 files changed, +78 -22Showing whitespace changes
public/scripts/world-info.js+78 -22
@@ -50,6 +50,28 @@ const world_info_logic = {
5050 AND_ALL: 3,
5151};
5252
53+/**
54+ * @enum {number} Possible states of the WI evaluation
55+ */
56+const scan_state = {
57+ /**
58+ * The scan will be stopped.
59+ */
60+ NONE: 0,
61+ /**
62+ * Initial state.
63+ */
64+ INITIAL: 1,
65+ /**
66+ * The scan is triggered by a recursion step.
67+ */
68+ RECURSION: 2,
69+ /**
70+ * The scan is triggered by a min activations depth skew.
71+ */
72+ MIN_ACTIVATIONS: 2,
73+};
74+
5375const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry');
5476
5577let world_info = {};
@@ -136,6 +158,11 @@ class WorldInfoBuffer {
136158 #recurseBuffer = [];
137159
138160 /**
161+ * @type {string[]} Array of strings added by prompt injections that are valid for the current scan
162+ */
163+ #injectBuffer = [];
164+
165+ /**
139166 * @type {number} The skew of the global scan depth. Used in "min activations"
140167 */
141168 #skew = 0;
@@ -184,9 +211,10 @@ class WorldInfoBuffer {
184211 /**
185212 * Gets all messages up to the given depth + recursion buffer.
186213 * @param {WIScanEntry} entry The entry that triggered the scan
214+ * @param {number} scanState The state of the scan
187215 * @returns {string} A slice of buffer until the given depth (inclusive)
188216 */
189217 get(entry, scanState) {
190218 let depth = entry.scanDepth ?? this.getDepth();
191219 if (depth <= this.#startDepth) {
192220 return '';
@@ -204,7 +232,12 @@ class WorldInfoBuffer {
204232
205233 let result = this.#depthBuffer.slice(this.#startDepth, depth).join('\n');
206234
207235 if (this.#recurseBufferinjectBuffer.length > 0) {
236+ result += '\n' + this.#injectBuffer.join('\n');
237+ }
238+
239+ // Min activations should not include the recursion buffer
240+ if (this.#recurseBuffer.length > 0 && scanState !== scan_state.MIN_ACTIVATIONS) {
208241 result += '\n' + this.#recurseBuffer.join('\n');
209242 }
210243
@@ -259,6 +292,14 @@ class WorldInfoBuffer {
259292 }
260293
261294 /**
295+ * Adds an injection to the buffer.
296+ * @param {string} message The injection to add
297+ */
298+ addInject(message) {
299+ this.#injectBuffer.push(message);
300+ }
301+
302+ /**
262303 * Increments skew and sets startDepth to previous depth.
263304 */
264305 advanceScanPosition() {
@@ -293,10 +334,11 @@ class WorldInfoBuffer {
293334 /**
294335 * Gets the match score for the given entry.
295336 * @param {WIScanEntry} entry Entry to check
337+ * @param {number} scanState The state of the scan
296338 * @returns {number} The number of key activations for the given entry
297339 */
298340 getScore(entry, scanState) {
299341 const bufferState = this.get(entry, scanState);
300342 let numberOfPrimaryKeys = 0;
301343 let numberOfSecondaryKeys = 0;
302344 let primaryScore = 0;
@@ -3503,12 +3545,12 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
35033545 if (context.extensionPrompts[key]?.scan) {
35043546 const prompt = getExtensionPromptByName(key);
35053547 if (prompt) {
35063548 buffer.addRecurseaddInject(prompt);
35073549 }
35083550 }
35093551 }
35103552
35113553 let needsToScanscanState = truescan_state.INITIAL;
35123554 let token_budget_overflowed = false;
35133555 let count = 0;
35143556 let allActivatedEntries = new Set();
@@ -3532,8 +3574,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
35323574 return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
35333575 }
35343576
35353577 while (needsToScanscanState) {
35363578 // Track how many times the loop has run. May be useful for debugging.
3579+ // eslint-disable-next-line no-unused-vars
35373580 count++;
35383581
35393582 let activatedNow = new Set();
@@ -3587,7 +3630,18 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
35873630 continue;
35883631 }
35893632
35903633 if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion) || (count == 1 && entry.delayUntilRecursion)) {
3634+ continue;
3635+ }
3636+
3637+ // Only use checks for recursion flags if the scan step was activated by recursion
3638+ if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion) {
3639+ console.debug(`WI entry ${entry.uid} suppressed by delay until recursion`, entry);
3640+ continue;
3641+ }
3642+
3643+ if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion) {
3644+ console.debug(`WI entry ${entry.uid} suppressed by exclude recursion`, entry);
35913645 continue;
35923646 }
35933647
@@ -3602,7 +3656,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
36023656
36033657 primary: for (let key of entry.key) {
36043658 const substituted = substituteParams(key);
36053659 const textToScan = buffer.get(entry, scanState);
36063660
36073661 if (substituted && buffer.matchKeys(textToScan, substituted.trim(), entry)) {
36083662 console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`);
@@ -3665,14 +3719,14 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
36653719 }
36663720 }
36673721
36683722 needsToScanscanState = world_info_recursive && activatedNow.size > 0 ? scan_state.RECURSION : scan_state.NONE;
36693723 const newEntries = [...activatedNow]
36703724 .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
36713725 let newContent = '';
36723726 const textToScanTokens = await getTokenCountAsync(allActivatedText);
36733727 const probabilityChecksBefore = failedProbabilityChecks.size;
36743728
36753729 filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState);
36763730
36773731 console.debug('-- PROBABILITY CHECKS BEGIN --');
36783732 for (const entry of newEntries) {
@@ -3697,7 +3751,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
36973751 console.log('Alerting');
36983752 toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
36993753 }
37003754 needsToScanscanState = falsescan_state.NONE;
37013755 token_budget_overflowed = true;
37023756 break;
37033757 }
@@ -3710,15 +3764,15 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
37103764
37113765 if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) {
37123766 console.debug('WI probability checks failed for all activated entries, stopping');
37133767 needsToScanscanState = falsescan_state.NONE;
37143768 }
37153769
37163770 if (newEntries.length === 0) {
37173771 console.debug('No new entries activated, stopping');
37183772 needsToScanscanState = falsescan_state.NONE;
37193773 }
37203774
37213775 if (needsToScanscanState) {
37223776 const text = newEntries
37233777 .filter(x => !failedProbabilityChecks.has(x))
37243778 .filter(x => !x.preventRecursion)
@@ -3728,7 +3782,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
37283782 }
37293783
37303784 // world_info_min_activations
37313785 if (!needsToScanscanState && !token_budget_overflowed) {
37323786 if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) {
37333787 let over_max = (
37343788 world_info_min_activations_depth_max > 0 &&
@@ -3736,7 +3790,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
37363790 ) || (buffer.getDepth() > chat.length);
37373791
37383792 if (!over_max) {
37393793 needsToScanscanState = truescan_state.MIN_ACTIVATIONS; // loop
37403794 buffer.advanceScanPosition();
37413795 }
37423796 }
@@ -3824,8 +3878,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
38243878 * @param {Record<string, WIScanEntry[]>} groups The groups to filter
38253879 * @param {WorldInfoBuffer} buffer The buffer to use for scoring
38263880 * @param {(entry: WIScanEntry) => void} removeEntry The function to remove an entry
3881+ * @param {number} scanState The current scan state
38273882 */
38283883function filterGroupsByScoring(groups, buffer, removeEntry, scanState) {
38293884 for (const [key, group] of Object.entries(groups)) {
38303885 // Group scoring is disabled both globally and for the group entries
38313886 if (!world_info_use_group_scoring && !group.some(x => x.useGroupScoring)) {
@@ -3833,7 +3888,7 @@ function filterGroupsByScoring(groups, buffer, removeEntry) {
38333888 continue;
38343889 }
38353890
38363891 const scores = group.map(entry => buffer.getScore(entry, scanState));
38373892 const maxScore = Math.max(...scores);
38383893 console.debug(`Group '${key}' max score: ${maxScore}`);
38393894 //console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] })));
@@ -3861,8 +3916,9 @@ function filterGroupsByScoring(groups, buffer, removeEntry) {
38613916 * @param {object[]} newEntries Entries activated on current recursion level
38623917 * @param {Set<object>} allActivatedEntries Set of all activated entries
38633918 * @param {WorldInfoBuffer} buffer The buffer to use for scanning
3919+ * @param {number} scanState The current scan state
38643920 */
38653921function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState) {
38663922 console.debug('-- INCLUSION GROUP CHECKS BEGIN --');
38673923 const grouped = newEntries.filter(x => x.group).reduce((acc, item) => {
38683924 item.group.split(/,\s*/).filter(x => x).forEach(group => {
@@ -3891,7 +3947,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer) {
38913947 }
38923948 }
38933949
38943950 filterGroupsByScoring(grouped, buffer, removeEntry, scanState);
38953951
38963952 for (const [key, group] of Object.entries(grouped)) {
38973953 console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group);