Skip to content

[BUG] Eager greeting resolution in getCharacterCardFields fires {{setvar}} from all greetings, breaking per-greeting variables #5857

Description

@burnerfuckmyass

Environment

🐧 Linux

System

NixOS, Firefox

Version

1.17.0 staging

Desktop Information

No response

Describe the problem

Macros with side effects (e.g. {{setvar::x::...}}) placed in greetings are executed for every greeting on every generation, not just the selected one. This makes it impossible to use per-greeting variable definitions.

Steps to reproduce:

  1. Create a character whose first message contains {{setvar::route::A}} and an alternate greeting containing {{setvar::route::B}}.
  2. Start a chat and swipe to the alternate greeting (route should now be B).
  3. Send a message so a generation runs, then check {{getvar::route}}.

Expected: route is B, the value set by the greeting actually in use.

Actual: route is A. Every generation re-runs the setvars from first_mes (and all alternate greetings), clobbering the value from the selected greeting.

Root cause: getCharacterCardFields() in public/script.js eagerly resolves every field returned by getCharacterCardFieldsLazy(), including firstMessage and alternateGreetings. Resolving those runs baseChatReplace over first_mes and every alternate greeting, which fires their macro side effects. Since getCharacterCardFields() is called on each generation, the side effects of unselected greetings run repeatedly, overwriting chat variables set by the active greeting.

Additional info

Here's what my clanker vibeslopped to fix it.

diff --git a/public/script.js b/public/script.js
index 2620fe858..f06c3a2e0 100644
--- a/public/script.js
+++ b/public/script.js
@@ -3416,8 +3416,8 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) {
 export function getCharacterCardFields({ chid = undefined } = {}) {
     const lazy = getCharacterCardFieldsLazy({ chid });
 
-    // Resolve all lazy fields into a plain object
-    return {
+    // Resolve the non-greeting fields eagerly into a plain object.
+    const fields = {
         system: lazy.system,
         mesExamples: lazy.mesExamples,
         description: lazy.description,
@@ -3428,9 +3428,26 @@ export function getCharacterCardFields({ chid = undefined } = {}) {
         version: lazy.version,
         charDepthPrompt: lazy.charDepthPrompt,
         creatorNotes: lazy.creatorNotes,
-        firstMessage: lazy.firstMessage,
-        alternateGreetings: lazy.alternateGreetings,
     };
+
+    // Keep the greeting fields lazy. Resolving them runs baseChatReplace over
+    // first_mes and *every* alternate greeting, so eagerly resolving here would
+    // fire the {{setvar::}} side-effects of unselected greetings on every call
+    // (getCharacterCardFields runs on each generation), clobbering chat
+    // variables set by the active greeting. They are only resolved on explicit
+    // access, e.g. via the {{greeting}} macro.
+    Object.defineProperty(fields, 'firstMessage', {
+        get: () => lazy.firstMessage,
+        enumerable: true,
+        configurable: true,
+    });
+    Object.defineProperty(fields, 'alternateGreetings', {
+        get: () => lazy.alternateGreetings,
+        enumerable: true,
+        configurable: true,
+    });
+
+    return /** @type {CharacterCardFields} */ (fields);
 }
 
 /**

Note this patch is a quick, sloppy fix. The getters still resolve (and fire side effects) if a caller spreads or serializes the object. A more thorough fix would be to audit the getCharacterCardFields call sites and switch the ones that don't need resolved greetings to getCharacterCardFieldsLazy, so greeting macros only run when a greeting is actually rendered.

Please tick the boxes

  • I have explained the issue clearly, and I included all relevant info
  • I have checked that this issue hasn't already been raised
  • I have checked the docs important
  • I confirm that my issue is not related to third-party content, unofficial extension or patch. If in doubt, check with a new user account and with extensions disabled

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛 Bug[ISSUE] Ticket describing something that isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions