From d96a1998902a6a88b570851f604036e396dfee5a Mon Sep 17 00:00:00 2001 From: Daryl Date: Sat, 21 Feb 2026 22:21:18 -0400 Subject: [PATCH] Implement separate generation ID to ensure that messages deleted during separate tracker generation do not attempt to apply the received data to a now non-existent message --- src/core/state.js | 26 ++++++++++++++++++++++++++ src/systems/generation/apiClient.js | 13 +++++++++++-- src/systems/integration/sillytavern.js | 15 +++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/core/state.js b/src/core/state.js index 693072a..e90367e 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -381,6 +381,32 @@ export let isPlotProgression = false; */ export let isAwaitingNewMessage = false; +/** + * Monotonically-increasing counter used to detect stale separate-mode tracker + * generation results. Incremented each time a new automated generation is + * triggered or a message deletion occurs so any in-flight (or pending) call + * from a previous generation can recognise that its result is no longer valid. + */ +let separateGenerationId = 0; + +/** + * Returns the current separate generation ID. + * @returns {number} + */ +export function getSeparateGenerationId() { + return separateGenerationId; +} + +/** + * Increments and returns the new separate generation ID. + * Call this when starting a new generation or when a deletion + * invalidates any pending/in-flight generation. + * @returns {number} The new ID + */ +export function incrementSeparateGenerationId() { + return ++separateGenerationId; +} + /** * Temporary storage for pending dice roll (not saved until user clicks "Save Roll") */ diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 5f48715..d22b762 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -18,7 +18,8 @@ import { lastActionWasSwipe, setIsGenerating, setLastActionWasSwipe, - $musicPlayerContainer + $musicPlayerContainer, + getSeparateGenerationId } from '../../core/state.js'; import { saveChatData, mirrorToSwipeInfo } from '../../core/persistence.js'; import { @@ -218,7 +219,7 @@ export async function switchToPreset(presetName) { * @param {Function} renderThoughts - UI function to render character thoughts * @param {Function} renderInventory - UI function to render inventory */ -export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory) { +export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, generationId = null) { if (isGenerating) { // console.log('[RPG Companion] Already generating, skipping...'); return; @@ -262,6 +263,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough }); } + // If a generationId was provided and the counter has since been incremented + // (by a deletion or a newer generation), discard this result entirely. + // The finally block still runs to restore button state. + if (generationId !== null && getSeparateGenerationId() !== generationId) { + // console.log('[RPG Companion] ⚠️ Separate generation result discarded — superseded (genId', generationId, '!= current', getSeparateGenerationId(), ')'); + return; + } + if (response) { // console.log('[RPG Companion] Raw AI response:', response); const parsedData = parseResponse(response); diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index d40ddda..b627236 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -20,7 +20,9 @@ import { setIsAwaitingNewMessage, updateLastGeneratedData, updateCommittedTrackerData, - $musicPlayerContainer + $musicPlayerContainer, + getSeparateGenerationId, + incrementSeparateGenerationId } from '../../core/state.js'; import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData, commitTrackerDataFromPriorMessage, mirrorToSwipeInfo } from '../../core/persistence.js'; import { i18n } from '../../core/i18n.js'; @@ -266,8 +268,13 @@ export async function onMessageReceived(data) { // Trigger auto-update if enabled (for both separate and external modes) // Only trigger if this is a newly generated message, not loading chat history if (extensionSettings.autoUpdate && isAwaitingNewMessage) { + // Capture the current generation ID before the async gap so that any + // message deletion (or a newer generation) that increments the counter + // while the 500ms timer or the API call is in-flight will cause + // updateRPGData to discard its result rather than stomping the UI. + const genId = incrementSeparateGenerationId(); setTimeout(async () => { - await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, genId); // Update FAB widgets and strip widgets after separate/external mode update completes setFabLoadingState(false); updateFabWidgets(); @@ -439,6 +446,10 @@ export function onMessageDeleted() { // console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted'); + // Invalidate any pending or in-flight separate-mode generation so + // its result is not applied to the (now-changed) chat tail. + incrementSeparateGenerationId(); + const currentChat = getContext().chat; // Walk backward to find the new last assistant message.