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

This commit is contained in:
Daryl
2026-02-21 22:21:18 -04:00
parent 4b816dd1fd
commit d96a199890
3 changed files with 50 additions and 4 deletions
+26
View File
@@ -381,6 +381,32 @@ export let isPlotProgression = false;
*/ */
export let isAwaitingNewMessage = 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") * Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
*/ */
+11 -2
View File
@@ -18,7 +18,8 @@ import {
lastActionWasSwipe, lastActionWasSwipe,
setIsGenerating, setIsGenerating,
setLastActionWasSwipe, setLastActionWasSwipe,
$musicPlayerContainer $musicPlayerContainer,
getSeparateGenerationId
} from '../../core/state.js'; } from '../../core/state.js';
import { saveChatData, mirrorToSwipeInfo } from '../../core/persistence.js'; import { saveChatData, mirrorToSwipeInfo } from '../../core/persistence.js';
import { import {
@@ -218,7 +219,7 @@ export async function switchToPreset(presetName) {
* @param {Function} renderThoughts - UI function to render character thoughts * @param {Function} renderThoughts - UI function to render character thoughts
* @param {Function} renderInventory - UI function to render inventory * @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) { if (isGenerating) {
// console.log('[RPG Companion] Already generating, skipping...'); // console.log('[RPG Companion] Already generating, skipping...');
return; 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) { if (response) {
// console.log('[RPG Companion] Raw AI response:', response); // console.log('[RPG Companion] Raw AI response:', response);
const parsedData = parseResponse(response); const parsedData = parseResponse(response);
+13 -2
View File
@@ -20,7 +20,9 @@ import {
setIsAwaitingNewMessage, setIsAwaitingNewMessage,
updateLastGeneratedData, updateLastGeneratedData,
updateCommittedTrackerData, updateCommittedTrackerData,
$musicPlayerContainer $musicPlayerContainer,
getSeparateGenerationId,
incrementSeparateGenerationId
} from '../../core/state.js'; } from '../../core/state.js';
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData, commitTrackerDataFromPriorMessage, mirrorToSwipeInfo } from '../../core/persistence.js'; import { saveChatData, loadChatData, autoSwitchPresetForEntity, getSwipeData, commitTrackerDataFromPriorMessage, mirrorToSwipeInfo } from '../../core/persistence.js';
import { i18n } from '../../core/i18n.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) // Trigger auto-update if enabled (for both separate and external modes)
// Only trigger if this is a newly generated message, not loading chat history // Only trigger if this is a newly generated message, not loading chat history
if (extensionSettings.autoUpdate && isAwaitingNewMessage) { 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 () => { 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 // Update FAB widgets and strip widgets after separate/external mode update completes
setFabLoadingState(false); setFabLoadingState(false);
updateFabWidgets(); updateFabWidgets();
@@ -439,6 +446,10 @@ export function onMessageDeleted() {
// console.log('[RPG Companion] 🗑️ EVENT: 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; const currentChat = getContext().chat;
// Walk backward to find the new last assistant message. // Walk backward to find the new last assistant message.