Refactor history injection to modify prompts instead of chat messages

This prevents any risk of injected context being accidentally saved to the chat.
Instead of modifying chat[].mes directly, we now:
1. Build a context map during GENERATION_STARTED
2. Inject into the prompt string (GENERATE_AFTER_COMBINE_PROMPTS) for text completion
3. Inject into the message array (CHAT_COMPLETION_PROMPT_READY) for chat completion

The original chat messages are never modified.
This commit is contained in:
tomt610
2026-01-10 19:10:33 +00:00
parent db2bed16a7
commit db97f012b0
+139 -48
View File
@@ -37,9 +37,8 @@ let currentSuppressionState = false;
// Track last chat length we committed at to prevent duplicate commits from streaming // Track last chat length we committed at to prevent duplicate commits from streaming
let lastCommittedChatLength = -1; let lastCommittedChatLength = -1;
// Store original message content for restoration after generation // Store context map for prompt injection (used by event handlers)
// Map of message index -> original mes content let pendingContextMap = new Map();
let originalMessageContent = new Map();
/** /**
* Builds a map of historical context data from ST chat messages with rpg_companion_swipes data. * Builds a map of historical context data from ST chat messages with rpg_companion_swipes data.
@@ -162,81 +161,171 @@ function buildHistoricalContextMap() {
} }
/** /**
* Injects historical context into chat messages by modifying them in-place. * Prepares historical context for injection into prompts.
* Stores original content for restoration after generation. * This builds the context map and stores it for use by prompt event handlers.
* This approach works for ALL API types (text completion and chat completion). * Does NOT modify the original chat messages.
*/ */
function injectHistoricalContextIntoChat() { function prepareHistoricalContextInjection() {
const historyPersistence = extensionSettings.historyPersistence; const historyPersistence = extensionSettings.historyPersistence;
if (!historyPersistence || !historyPersistence.enabled) { if (!historyPersistence || !historyPersistence.enabled) {
// console.log('[RPG Companion] History persistence not enabled, skipping injection'); pendingContextMap = new Map();
return; return;
} }
if (currentSuppressionState || !extensionSettings.enabled) { if (currentSuppressionState || !extensionSettings.enabled) {
// console.log('[RPG Companion] Skipping history injection: suppressed or disabled'); pendingContextMap = new Map();
return; return;
} }
const context = getContext(); const context = getContext();
const chat = context.chat; const chat = context.chat;
if (!chat || chat.length < 2) { if (!chat || chat.length < 2) {
// console.log('[RPG Companion] Chat too short, skipping history injection'); pendingContextMap = new Map();
return; return;
} }
// Build the context map // Build and store the context map for use by prompt handlers
const contextMap = buildHistoricalContextMap(); pendingContextMap = buildHistoricalContextMap();
if (contextMap.size === 0) { }
// console.log('[RPG Companion] No historical context to inject');
return; /**
* Injects historical context into a text completion prompt string.
* Searches for message content in the prompt and appends context after matches.
*
* @param {string} prompt - The text completion prompt
* @returns {string} - The modified prompt with injected context
*/
function injectContextIntoTextPrompt(prompt) {
if (pendingContextMap.size === 0) {
return prompt;
} }
// console.log(`[RPG Companion] Injecting historical context into ${contextMap.size} messages`); const context = getContext();
const chat = context.chat;
// Clear any previous stored content let modifiedPrompt = prompt;
originalMessageContent.clear();
let injectedCount = 0; let injectedCount = 0;
for (const [msgIdx, ctxContent] of contextMap) {
// Process each message that needs context injection
for (const [msgIdx, ctxContent] of pendingContextMap) {
const message = chat[msgIdx]; const message = chat[msgIdx];
if (!message || typeof message.mes !== 'string') { if (!message || typeof message.mes !== 'string') {
continue; continue;
} }
// Store original content for restoration // Find the message content in the prompt
originalMessageContent.set(msgIdx, message.mes); // Use a portion of the message to find it (last 100 chars should be unique enough)
const searchContent = message.mes.length > 100
? message.mes.slice(-100)
: message.mes;
// Modify the message in-place const searchIndex = modifiedPrompt.lastIndexOf(searchContent);
message.mes = message.mes + ctxContent; if (searchIndex === -1) {
injectedCount++; // Message not found in prompt (might be truncated)
// console.log(`[RPG Companion] Injected context into message ${msgIdx}`); continue;
} }
// console.log(`[RPG Companion] Successfully injected historical context into ${injectedCount} messages`); // Find the end of this message content in the prompt
const insertPosition = searchIndex + searchContent.length;
// Insert the context after the message
modifiedPrompt = modifiedPrompt.slice(0, insertPosition) + ctxContent + modifiedPrompt.slice(insertPosition);
injectedCount++;
}
if (injectedCount > 0) {
console.log(`[RPG Companion] Injected historical context into ${injectedCount} positions in text prompt`);
}
return modifiedPrompt;
} }
/** /**
* Restores original message content after generation completes. * Injects historical context into a chat completion message array.
* This ensures the injected context doesn't persist in the actual chat data. * Modifies the content of messages in the array directly.
*
* @param {Array} chatMessages - The chat completion message array
* @returns {Array} - The modified message array with injected context
*/ */
function restoreOriginalMessageContent() { function injectContextIntoChatPrompt(chatMessages) {
if (originalMessageContent.size === 0) { if (pendingContextMap.size === 0 || !Array.isArray(chatMessages)) {
return; return chatMessages;
} }
const context = getContext(); const context = getContext();
const chat = context.chat; const chat = context.chat;
let injectedCount = 0;
// console.log(`[RPG Companion] Restoring ${originalMessageContent.size} messages to original content`); // Process each message that needs context injection
for (const [msgIdx, ctxContent] of pendingContextMap) {
const originalMessage = chat[msgIdx];
if (!originalMessage || typeof originalMessage.mes !== 'string') {
continue;
}
for (const [msgIdx, originalContent] of originalMessageContent) { // Find this message in the chat completion array by matching content
if (chat[msgIdx]) { // Use a portion of the message to find it
chat[msgIdx].mes = originalContent; const searchContent = originalMessage.mes.length > 100
? originalMessage.mes.slice(-100)
: originalMessage.mes;
for (const promptMsg of chatMessages) {
if (promptMsg.content && typeof promptMsg.content === 'string' &&
promptMsg.content.includes(searchContent)) {
// Found the message - append context
promptMsg.content = promptMsg.content + ctxContent;
injectedCount++;
break;
}
} }
} }
originalMessageContent.clear(); if (injectedCount > 0) {
console.log(`[RPG Companion] Injected historical context into ${injectedCount} messages in chat prompt`);
}
return chatMessages;
}
/**
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
* Injects historical context into the prompt string.
*
* @param {Object} eventData - Event data with prompt property
*/
function onGenerateAfterCombinePrompts(eventData) {
if (!eventData || typeof eventData.prompt !== 'string') {
return;
}
if (eventData.dryRun) {
return;
}
eventData.prompt = injectContextIntoTextPrompt(eventData.prompt);
// Clear the pending context after injection
pendingContextMap = new Map();
}
/**
* Event handler for CHAT_COMPLETION_PROMPT_READY.
* Injects historical context into the chat message array.
*
* @param {Object} eventData - Event data with chat property
*/
function onChatCompletionPromptReady(eventData) {
if (!eventData || !Array.isArray(eventData.chat)) {
return;
}
if (eventData.dryRun) {
return;
}
eventData.chat = injectContextIntoChatPrompt(eventData.chat);
// Clear the pending context after injection
pendingContextMap = new Map();
} }
/** /**
@@ -565,22 +654,24 @@ Ensure these details naturally reflect and influence the narrative. Character be
// Set suppression state for the historical context injection // Set suppression state for the historical context injection
currentSuppressionState = shouldSuppress; currentSuppressionState = shouldSuppress;
// Inject historical context directly into chat messages // Prepare historical context for injection into prompts
// This temporarily modifies messages and will be restored after generation // This builds the context map but does NOT modify original chat messages
injectHistoricalContextIntoChat(); prepareHistoricalContextInjection();
// Register a one-time listener to restore messages after prompt is built // Register one-time listeners to inject context into the actual prompt
// Using .once() so it auto-removes after firing // These modify only the prompt sent to the API, not the stored chat data
eventSource.once(event_types.GENERATE_AFTER_COMBINE_PROMPTS, () => { if (pendingContextMap.size > 0) {
restoreOriginalMessageContent(); eventSource.once(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
}); eventSource.once(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
}
} }
/** /**
* Called when generation ends to restore original message content. * Called when generation ends to clean up any pending context.
* This should be called from the GENERATION_ENDED event handler. * This should be called from the GENERATION_ENDED event handler.
*/ */
export function onGenerationEndedCleanup() { export function onGenerationEndedCleanup() {
restoreOriginalMessageContent(); // Clear any pending context that wasn't used (e.g., if generation was cancelled)
pendingContextMap = new Map();
} }