Fix tracker injection context ordering and placeholder timing

This commit is contained in:
CristianAUnisa
2026-04-25 19:39:07 +02:00
parent ed9c12e969
commit 45c074499c
3 changed files with 94 additions and 72 deletions
+44 -7
View File
@@ -556,6 +556,38 @@ export function getSwipeData(message, swipeId) {
return null; return null;
} }
/**
* Resolve active swipe index for a message.
* Falls back to message.swipe_id, but prefers exact match against current
* message text when available to avoid stale swipe_id during event timing races.
*
* @param {Object} message - Assistant message object
* @returns {number} Active swipe index
*/
function resolveActiveSwipeId(message) {
const fallbackSwipeId = Number(message?.swipe_id ?? 0);
const swipes = Array.isArray(message?.swipes) ? message.swipes : null;
if (!swipes || swipes.length === 0) {
return Math.max(0, fallbackSwipeId);
}
const currentText = typeof message?.mes === 'string' ? message.mes : '';
if (currentText) {
for (let i = swipes.length - 1; i >= 0; i--) {
if (typeof swipes[i] === 'string' && swipes[i] === currentText) {
return i;
}
}
}
if (fallbackSwipeId < 0) {
return 0;
}
return Math.min(fallbackSwipeId, swipes.length - 1);
}
/** /**
* Commits tracker data from the assistant message immediately before currentMessageIndex. * Commits tracker data from the assistant message immediately before currentMessageIndex.
* Walks backward through the chat skipping the current message, user messages, and system * Walks backward through the chat skipping the current message, user messages, and system
@@ -574,25 +606,30 @@ export function commitTrackerDataFromPriorMessage(currentMessageIndex) {
return; return;
} }
// console.log('[RPG Companion] commitTrackerDataFromPriorMessage called with index', currentMessageIndex, '| chat.length =', chat.length);
for (let i = currentMessageIndex - 1; i >= 0; i--) { for (let i = currentMessageIndex - 1; i >= 0; i--) {
const message = chat[i]; const message = chat[i];
if (message.is_user || message.is_system) continue; if (message.is_user || message.is_system) continue;
// Found the prior assistant message — commit its active swipe data // Found the prior assistant message — commit its active swipe data
const swipeId = message.swipe_id || 0; const swipeId = resolveActiveSwipeId(message);
const swipeData = getSwipeData(message, swipeId); const swipeData = getSwipeData(message, swipeId);
// console.log('[RPG Companion] Committing from chat[' + i + '] swipe', swipeId, '| has swipe data:', !!swipeData);
committedTrackerData.userStats = swipeData?.userStats || null; if (!swipeData) {
committedTrackerData.infoBox = swipeData?.infoBox || null; // Keep searching backward for a valid state if this assistant message has no data
const rawCharacterThoughts = swipeData?.characterThoughts; continue;
}
committedTrackerData.userStats = swipeData.userStats || null;
committedTrackerData.infoBox = swipeData.infoBox || null;
const rawCharacterThoughts = swipeData.characterThoughts;
committedTrackerData.characterThoughts = committedTrackerData.characterThoughts =
rawCharacterThoughts == null rawCharacterThoughts == null
? null ? null
: (typeof rawCharacterThoughts === 'string' : (typeof rawCharacterThoughts === 'string'
? rawCharacterThoughts ? rawCharacterThoughts
: JSON.stringify(rawCharacterThoughts)); : JSON.stringify(rawCharacterThoughts));
return; return;
} }
@@ -631,7 +668,7 @@ export function inheritSwipeDataFromPriorMessage(message, messageIndex) {
const msg = chat[i]; const msg = chat[i];
if (msg.is_user || msg.is_system) continue; if (msg.is_user || msg.is_system) continue;
const swipeId = msg.swipe_id || 0; const swipeId = resolveActiveSwipeId(msg);
const swipeData = getSwipeData(msg, swipeId); const swipeData = getSwipeData(msg, swipeId);
if (!swipeData) continue; // No data on this assistant message; keep searching further back if (!swipeData) continue; // No data on this assistant message; keep searching further back
+9 -62
View File
@@ -38,8 +38,9 @@ let currentSuppressionState = false;
// Type imports // Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
// Track last chat length we committed at to prevent duplicate commits from streaming // Track the latest user message we committed for to prevent duplicate commits
let lastCommittedChatLength = -1; // when GENERATION_STARTED can fire multiple times for the same turn.
let lastCommittedUserMessageSignature = null;
// Store context map for prompt injection (used by event handlers) // Store context map for prompt injection (used by event handlers)
let pendingContextMap = new Map(); let pendingContextMap = new Map();
@@ -607,66 +608,12 @@ export async function onGenerationStarted(type, data, dryRun) {
// Ensure checkpoint is applied before generation // Ensure checkpoint is applied before generation
await restoreCheckpointOnLoad(); await restoreCheckpointOnLoad();
const currentChatLength = chat ? chat.length : 0; // If this is a new generation (not a swipe and not the tracker update pass),
// commit the tracker data from the last assistant message (N-1 rule).
// For TOGETHER mode: Commit when user sends message (before first generation) // Passing chat.length ensures we start searching backwards from the end of the chat,
if (extensionSettings.generationMode === 'together') { // correctly finding the latest valid assistant state regardless of where the user message is.
// By the time onGenerationStarted fires, ST has already added the placeholder AI message if (!lastActionWasSwipe && !isGenerating) {
// So we check the second-to-last message to see if user just sent a message commitTrackerDataFromPriorMessage(chat ? chat.length : 0);
const secondToLastMessage = chat && chat.length > 1 ? chat[chat.length - 2] : null;
const isUserMessage = secondToLastMessage && secondToLastMessage.is_user;
// Commit if:
// 1. Second-to-last message is from USER (user just sent message)
// 2. Not a swipe (lastActionWasSwipe = false)
// 3. Haven't already committed for this chat length (prevent streaming duplicates)
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
if (shouldCommit) {
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing from N-1 assistant message');
// console.log('[RPG Companion] Chat length:', currentChatLength, 'Last committed:', lastCommittedChatLength);
// Commit from the prior assistant message's swipe store (N-1 rule).
// currentChatLength - 1 is the new AI placeholder; the function walks backward
// past it and the user message to find the previous AI message's tracker state.
commitTrackerDataFromPriorMessage(currentChatLength - 1);
// Track chat length to prevent duplicate commits from streaming
lastCommittedChatLength = currentChatLength;
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
// userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
// characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
// });
} else if (lastActionWasSwipe) {
// console.log('[RPG Companion] ⏭️ Skipping commit: swipe (using previous committed data)');
} else if (!isUserMessage) {
// console.log('[RPG Companion] ⏭️ Skipping commit: second-to-last message is not user message (likely swipe or continuation)');
}
// console.log('[RPG Companion] 📦 TOGETHER MODE: Injecting committed tracker data into prompt');
// console.log('[RPG Companion] committedTrackerData =', {
// userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
// characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
// });
}
// For SEPARATE and EXTERNAL modes: Check if we need to commit extension data
// BUT: Only do this for the MAIN generation, not the tracker update generation
// If isGenerating is true, this is the tracker update generation (second call), so skip flag logic
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
if (!lastActionWasSwipe) {
// User sent a new message - commit from the prior assistant message's swipe store
// (N-1 rule) rather than lastGeneratedData, which may reflect a sibling swipe's
// outcome and would poison the context for the new generation.
// currentChatLength - 1 is the new AI placeholder; search starts before it.
commitTrackerDataFromPriorMessage(currentChatLength - 1);
}
// If lastActionWasSwipe, context was already committed by commitTrackerDataFromPriorMessage
// in onMessageSwiped before generation started.
} }
// Use the committed tracker data as source for generation // Use the committed tracker data as source for generation
+41 -3
View File
@@ -231,6 +231,38 @@ function getCurrentSwipeText(message) {
return typeof message?.mes === 'string' ? message.mes : ''; return typeof message?.mes === 'string' ? message.mes : '';
} }
/**
* Resolves the currently active swipe index for a message.
* Some ST flows can briefly expose a stale message.swipe_id during swipe transitions,
* so we also match against message.mes in the swipes array when possible.
*
* @param {Object} message - Assistant message object
* @returns {number} Active swipe index
*/
function resolveActiveSwipeId(message) {
const fallbackSwipeId = Number(message?.swipe_id ?? 0);
const swipes = Array.isArray(message?.swipes) ? message.swipes : null;
if (!swipes || swipes.length === 0) {
return Math.max(0, fallbackSwipeId);
}
const currentText = typeof message?.mes === 'string' ? message.mes : '';
if (currentText) {
for (let i = swipes.length - 1; i >= 0; i--) {
if (typeof swipes[i] === 'string' && swipes[i] === currentText) {
return i;
}
}
}
if (fallbackSwipeId < 0) {
return 0;
}
return Math.min(fallbackSwipeId, swipes.length - 1);
}
function repairLatestTrackerStateFromCurrentSwipeContent(chatMessages = getContext()?.chat || []) { function repairLatestTrackerStateFromCurrentSwipeContent(chatMessages = getContext()?.chat || []) {
for (let i = chatMessages.length - 1; i >= 0; i--) { for (let i = chatMessages.length - 1; i >= 0; i--) {
const message = chatMessages[i]; const message = chatMessages[i];
@@ -393,6 +425,7 @@ export function onMessageSent() {
const chat = context.chat; const chat = context.chat;
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null; const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
if (lastMessage && lastMessage.mes === '...') { if (lastMessage && lastMessage.mes === '...') {
// console.log('[RPG Companion] 🟢 Ignoring onMessageSent: streaming placeholder message'); // console.log('[RPG Companion] 🟢 Ignoring onMessageSent: streaming placeholder message');
return; return;
@@ -405,6 +438,9 @@ export function onMessageSent() {
// This allows auto-update to distinguish between new generations and loading chat history // This allows auto-update to distinguish between new generations and loading chat history
setIsAwaitingNewMessage(true); setIsAwaitingNewMessage(true);
// Note: FAB spinning is NOT shown for together mode since no extra API request is made // Note: FAB spinning is NOT shown for together mode since no extra API request is made
// The RPG data comes embedded in the main response // The RPG data comes embedded in the main response
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called // FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
@@ -430,6 +466,7 @@ export async function onMessageReceived(data) {
// Commit happens in onMessageSent (when user sends message, before generation) // Commit happens in onMessageSent (when user sends message, before generation)
const lastMessage = chat[chat.length - 1]; const lastMessage = chat[chat.length - 1];
if (lastMessage && !lastMessage.is_user) { if (lastMessage && !lastMessage.is_user) {
const rawSwipeId = Number(lastMessage.swipe_id ?? 0);
const responseText = lastMessage.mes; const responseText = lastMessage.mes;
const parsedData = parseResponse(responseText); const parsedData = parseResponse(responseText);
@@ -471,7 +508,8 @@ export async function onMessageReceived(data) {
lastMessage.extra.rpg_companion_swipes = {}; lastMessage.extra.rpg_companion_swipes = {};
} }
const currentSwipeId = lastMessage.swipe_id || 0; const currentSwipeId = resolveActiveSwipeId(lastMessage);
setMessageSwipeTrackerData(lastMessage, currentSwipeId, { setMessageSwipeTrackerData(lastMessage, currentSwipeId, {
userStats: parsedData.userStats, userStats: parsedData.userStats,
infoBox: parsedData.infoBox, infoBox: parsedData.infoBox,
@@ -668,7 +706,7 @@ export function onMessageSwiped(messageIndex) {
return; return;
} }
const currentSwipeId = message.swipe_id || 0; const currentSwipeId = resolveActiveSwipeId(message);
const swipeCount = Array.isArray(message.swipes) ? message.swipes.length : 0; const swipeCount = Array.isArray(message.swipes) ? message.swipes.length : 0;
// Only set flag to true if this swipe will trigger a NEW generation // Only set flag to true if this swipe will trigger a NEW generation
@@ -677,7 +715,7 @@ export function onMessageSwiped(messageIndex) {
message.swipes[currentSwipeId] !== undefined && message.swipes[currentSwipeId] !== undefined &&
message.swipes[currentSwipeId] !== null && message.swipes[currentSwipeId] !== null &&
message.swipes[currentSwipeId].length > 0; message.swipes[currentSwipeId].length > 0;
const swipeData = getCurrentSwipeTrackerData(message); const swipeData = getSwipeData(message, currentSwipeId);
const isPendingNewSwipe = currentSwipeId >= swipeCount; const isPendingNewSwipe = currentSwipeId >= swipeCount;
if (!isExistingSwipe) { if (!isExistingSwipe) {