phase2-4 recall prompt-flow hardening

This commit is contained in:
Youzini-afk
2026-04-11 18:51:50 +08:00
parent 3a10dbb9ba
commit 0cb95c4f2b
14 changed files with 1069 additions and 143 deletions

View File

@@ -1,11 +1,27 @@
import { register, registerHooks } from "node:module";
const DEFAULT_REGEX_ENGINE_HOOK_ENTRIES = Object.freeze([
{
specifiers: ["../../../../regex/engine.js"],
url: toDataModuleUrl([
"export const regex_placement = { USER_INPUT: 1, AI_OUTPUT: 2, SLASH_COMMAND: 3, WORLD_INFO: 5, REASONING: 6 };",
"export function getRegexedString(...args) {",
" const fn = globalThis.__taskRegexTestCoreGetRegexedString;",
" return typeof fn === 'function' ? fn(...args) : String(args?.[0] ?? '');",
"}",
].join("\n")),
},
]);
export function toDataModuleUrl(source = "") {
return `data:text/javascript,${encodeURIComponent(String(source || ""))}`;
}
export function installResolveHooks(entries = []) {
const normalizedEntries = (Array.isArray(entries) ? entries : [])
const normalizedEntries = [
...(Array.isArray(entries) ? entries : []),
...DEFAULT_REGEX_ENGINE_HOOK_ENTRIES,
]
.map((entry) => ({
specifiers: Array.isArray(entry?.specifiers)
? entry.specifiers.map((value) => String(value || "")).filter(Boolean)

View File

@@ -4244,6 +4244,65 @@ async function testGenerationRecallDeferredRewriteMutatesFinalMesSendPayload() {
);
}
async function testGenerationRecallDeferredRewriteMutatesFinalMesSendAuthoritativeUserInput() {
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
harness.extension_settings[MODULE_NAME] = {
recallUseAuthoritativeGenerationInput: true,
};
harness.chat = [{ is_user: true, mes: "楼层稳定输入" }];
harness.pendingRecallSendIntent = {
text: "发送前真实输入",
hash: "hash-deferred-authoritative-rewrite",
at: Date.now(),
source: "dom-intent",
};
harness.result.pendingRecallSendIntent = harness.pendingRecallSendIntent;
await harness.result.onGenerationAfterCommands("normal", {}, false);
const promptData = {
finalMesSend: [
{
injected: false,
message: "楼层稳定输入",
extensionPrompts: [],
},
],
};
const resolution = await harness.result.onBeforeCombinePrompts(promptData);
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATION_AFTER_COMMANDS",
);
const transaction = [...harness.result.generationRecallTransactions.values()][0];
assert.ok(transaction);
assert.equal(transaction.frozenRecallOptions.authoritativeInputUsed, true);
assert.equal(transaction.frozenRecallOptions.boundUserFloorText, "楼层稳定输入");
assert.equal(
harness.runRecallCalls[0].authoritativeInputUsed,
true,
);
assert.equal(harness.runRecallCalls[0].boundUserFloorText, "楼层稳定输入");
assert.equal(promptData.finalMesSend[0].message, "发送前真实输入");
assert.equal(resolution.applicationMode, "rewrite");
assert.equal(resolution.authoritativeInputUsed, true);
assert.equal(resolution.boundUserFloorText, "楼层稳定输入");
assert.equal(resolution.inputRewrite.applied, true);
assert.equal(resolution.inputRewrite.changed, true);
assert.equal(resolution.inputRewrite.field, "finalMesSend[0].message");
assert.match(
promptData.finalMesSend[0].extensionPrompts.join("\n"),
/注入:发送前真实输入/,
);
assert.equal(
harness.recordedInjectionSnapshots.at(-1)?.inputRewrite?.applied,
true,
);
}
async function testGenerationRecallSendIntentBeatsChatTailAndStaysObservable() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "旧的 chat tail" }];
@@ -4480,8 +4539,11 @@ async function testPersistentRecallDataLayerLifecycleAndCompatibility() {
hookName: "GENERATION_AFTER_COMMANDS",
tokenEstimate: 24,
manuallyEdited: false,
authoritativeInputUsed: true,
boundUserFloorText: "稳定楼层输入",
nowIso: "2026-01-01T00:00:00.000Z",
});
assert.equal(writePersistedRecallToUserMessage(chat, 2, record), true);
const loaded = readPersistedRecallFromUserMessage(chat, 2);
@@ -4489,6 +4551,8 @@ async function testPersistentRecallDataLayerLifecycleAndCompatibility() {
assert.equal(loaded.injectionText, "fresh-memory");
assert.equal(loaded.generationCount, 0);
assert.equal(loaded.manuallyEdited, false);
assert.equal(loaded.authoritativeInputUsed, true);
assert.equal(loaded.boundUserFloorText, "稳定楼层输入");
chat[2].mes = "u2 edited";
assert.equal(
@@ -4517,14 +4581,19 @@ async function testPersistentRecallDataLayerLifecycleAndCompatibility() {
hookName: "MESSAGE_RECALL_BADGE_RERUN",
tokenEstimate: 30,
manuallyEdited: false,
authoritativeInputUsed: false,
boundUserFloorText: "",
nowIso: "2026-01-01T00:00:02.000Z",
},
readPersistedRecallFromUserMessage(chat, 2),
);
assert.equal(writePersistedRecallToUserMessage(chat, 2, overwrite), true);
const overwritten = readPersistedRecallFromUserMessage(chat, 2);
assert.equal(overwritten?.manuallyEdited, false);
assert.equal(overwritten?.injectionText, "system-rerecall");
assert.equal(overwritten?.authoritativeInputUsed, false);
assert.equal(overwritten?.boundUserFloorText, "");
assert.equal(removePersistedRecallFromUserMessage(chat, 2), true);
assert.equal(readPersistedRecallFromUserMessage(chat, 2), null);
@@ -4601,17 +4670,39 @@ async function testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor(
status: "completed",
didRecall: true,
injectionText: "fresh-memory",
authoritativeInputUsed: true,
boundUserFloorText: "稳定楼层输入",
},
transaction: {
frozenRecallOptions: {
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "当前输入",
lockedSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
},
},
});
assert.equal(resolution.source, "fresh");
assert.equal(resolution.targetUserMessageIndex, 0);
assert.equal(resolution.authoritativeInputUsed, true);
assert.equal(resolution.boundUserFloorText, "稳定楼层输入");
assert.equal(
harness.chat[0]?.extra?.bme_recall?.injectionText,
"fresh-memory",
);
assert.equal(
JSON.stringify(harness.chat[0]?.extra?.bme_recall?.selectedNodeIds || []),
JSON.stringify([]),
);
assert.equal(harness.chat[0]?.extra?.bme_recall?.authoritativeInputUsed, true);
assert.equal(
harness.chat[0]?.extra?.bme_recall?.boundUserFloorText,
"稳定楼层输入",
);
assert.equal(harness.metadataSaveCalls > 0, true);
}
{
@@ -4640,6 +4731,8 @@ async function testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor(
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "尾部 user 仍可匹配",
lockedSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
},
},
});
@@ -4674,6 +4767,8 @@ async function testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor(
generationType: "normal",
targetUserMessageIndex: null,
overrideUserMessage: "发送前捕获的原始文本",
lockedSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
},
},
});
@@ -4703,6 +4798,8 @@ async function testGenerationRecallFinalInjectionBackfillsPersistedRecord() {
didRecall: true,
injectionText: "fresh-memory",
selectedNodeIds: ["node-a", "node-b"],
authoritativeInputUsed: true,
boundUserFloorText: "稳定楼层输入",
},
transaction: {
frozenRecallOptions: {
@@ -4721,9 +4818,15 @@ async function testGenerationRecallFinalInjectionBackfillsPersistedRecord() {
harness.chat[0]?.extra?.bme_recall?.injectionText,
"fresh-memory",
);
assert.deepEqual(
harness.chat[0]?.extra?.bme_recall?.selectedNodeIds,
["node-a", "node-b"],
assert.equal(
JSON.stringify(harness.chat[0]?.extra?.bme_recall?.selectedNodeIds || []),
JSON.stringify(["node-a", "node-b"]),
);
assert.equal(harness.chat[0]?.extra?.bme_recall?.authoritativeInputUsed, true);
assert.equal(
harness.chat[0]?.extra?.bme_recall?.boundUserFloorText,
"稳定楼层输入",
);
assert.equal(harness.metadataSaveCalls > 0, true);
}
@@ -4744,9 +4847,9 @@ async function testGenerationRecallImmediateAfterCommandsBackfillsPersistedRecor
harness.chat[0]?.extra?.bme_recall?.injectionText,
"注入:即时模式补写目标",
);
assert.deepEqual(
harness.chat[0]?.extra?.bme_recall?.selectedNodeIds,
["node-test-1"],
assert.equal(
JSON.stringify(harness.chat[0]?.extra?.bme_recall?.selectedNodeIds || []),
JSON.stringify(["node-test-1"]),
);
assert.equal(harness.metadataSaveCalls > 0, true);
}
@@ -6079,6 +6182,7 @@ await testAutoExtractionDefersWhenHistoryRecoveryBusy();
await testRemoveNodeHandlesCyclicChildGraph();
await testGenerationRecallAppliesFinalInjectionOncePerTransaction();
await testGenerationRecallDeferredRewriteMutatesFinalMesSendPayload();
await testGenerationRecallDeferredRewriteMutatesFinalMesSendAuthoritativeUserInput();
await testPersistentRecallDataLayerLifecycleAndCompatibility();
await testPersistentRecallSourceResolutionAndTargetRouting();
await testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor();

View File

@@ -327,9 +327,42 @@ try {
systemOnlyPromptBuild,
"fallback <updatevariable>hidden</updatevariable> text",
);
assert.equal(systemOnlyPayload.userPrompt, "fallback text");
assert.equal(systemOnlyPayload.fallbackUserPromptSource, "fallback-user-prompt");
const additionalUserOnlyPayload = buildTaskLlmPayload(
{
debug: {
taskType: "recall",
},
systemPrompt: "",
executionMessages: [],
privateTaskMessages: [
{
role: "user",
content: "来自 additionalMessages 的结构化用户块",
source: "profile-block",
},
],
},
"unused fallback user prompt",
);
assert.equal(additionalUserOnlyPayload.userPrompt, "");
assert.equal(
systemOnlyPayload.userPrompt,
"fallback <updatevariable>hidden</updatevariable> text",
additionalUserOnlyPayload.fallbackUserPromptSource,
"additional-messages",
);
assert.deepEqual(
additionalUserOnlyPayload.additionalMessages.map((message) => ({
role: message.role,
content: message.content,
})),
[
{
role: "user",
content: "来自 additionalMessages 的结构化用户块",
},
],
);
const rawWorldInfoEntries = [
@@ -465,6 +498,10 @@ try {
assert.equal(payload.systemPrompt, "");
assert.match(JSON.stringify(payload.promptMessages), /FINAL_BAD/);
assert.doesNotMatch(JSON.stringify(payload.promptMessages), /FINAL_GOOD/);
assert.equal(
payload.promptMessages.some((message) => String(message?.regexSourceType || "").trim()),
true,
);
const result = await llm.callLLMForJSON({
systemPrompt: payload.systemPrompt,
userPrompt: payload.userPrompt,
@@ -492,6 +529,22 @@ try {
assert.ok(runtimePromptBuild);
assert.ok(runtimeLlmRequest);
assert.match(JSON.stringify(runtimeLlmRequest.messages), /FINAL_GOOD/);
assert.equal(
runtimeLlmRequest.messages.some((message) =>
String(message?.regexSourceType || "").trim(),
),
true,
);
assert.equal(
runtimeLlmRequest.transportMessages.some((message) =>
Object.prototype.hasOwnProperty.call(message || {}, "regexSourceType"),
),
false,
);
assert.doesNotMatch(
JSON.stringify(capturedBodies[0].messages),
/regexSourceType|sourceKey|blockId|contentOrigin|speaker/i,
);
assert.equal(runtimeLlmRequest.requestCleaning?.applied, true);
assert.equal(
runtimeLlmRequest.requestCleaning?.stages?.length > 0,
@@ -516,7 +569,7 @@ try {
/status_current_variable|updatevariable|StatusPlaceHolderImpl|stat_data|display_data|delta_data|get_message_variable/i,
);
assert.deepEqual(
runtimeLlmRequest.messages,
runtimeLlmRequest.transportMessages,
runtimeLlmRequest.requestBody.messages,
);
assert.equal(

View File

@@ -199,6 +199,29 @@ async function testHostSnapshotCanRemainAuthoritativeQueryWhenFlagEnabled() {
assert.equal(transaction.frozenRecallOptions.includeSyntheticUserMessage, true);
}
async function testGenerationAfterCommandsWritesBackAuthoritativePromptWhenPreserved() {
const harness = await createGenerationRecallHarness();
harness.extension_settings[MODULE_NAME] = {
recallUseAuthoritativeGenerationInput: true,
};
harness.chat = [{ is_user: true, mes: "旧的 chat tail" }];
harness.pendingRecallSendIntent = {
text: "发送前权威输入",
hash: "hash-phase4-writeback",
at: Date.now(),
source: "dom-intent",
};
const params = {
prompt: "旧 prompt",
user_input: "旧 user_input",
};
await harness.result.onGenerationAfterCommands("normal", params, false);
assert.equal(params.prompt, "发送前权威输入");
assert.equal(params.user_input, "发送前权威输入");
}
function testResolveRecallInputControllerAppendsSyntheticAuthoritativeUserMessage() {
const runtime = {
normalizeRecallInputText(value = "") {
@@ -240,6 +263,7 @@ await testSendIntentCanRemainAuthoritativeQueryWhenFlagEnabled();
await testPlannerHandoffCanRemainAuthoritativeQueryWhenFlagEnabled();
await testAuthoritativeSendIntentStaysFrozenAcrossHooksWhenFlagEnabled();
await testHostSnapshotCanRemainAuthoritativeQueryWhenFlagEnabled();
await testGenerationAfterCommandsWritesBackAuthoritativePromptWhenPreserved();
testResolveRecallInputControllerAppendsSyntheticAuthoritativeUserMessage();
console.log("recall-authoritative-generation-input tests passed");

View File

@@ -12,6 +12,16 @@ const extensionsShimSource = [
const extensionsShimUrl = `data:text/javascript,${encodeURIComponent(
extensionsShimSource,
)}`;
const regexEngineShimSource = [
"export const regex_placement = { USER_INPUT: 1, AI_OUTPUT: 2, SLASH_COMMAND: 3, WORLD_INFO: 5, REASONING: 6 };",
"export function getRegexedString(...args) {",
" const fn = globalThis.__taskRegexTestCoreGetRegexedString;",
" return typeof fn === 'function' ? fn(...args) : String(args?.[0] ?? '');",
"}",
].join("\n");
const regexEngineShimUrl = `data:text/javascript,${encodeURIComponent(
regexEngineShimSource,
)}`;
installResolveHooks([
{
@@ -22,6 +32,10 @@ installResolveHooks([
],
url: extensionsShimUrl,
},
{
specifiers: ["../../../../regex/engine.js"],
url: regexEngineShimUrl,
},
]);
const originalSillyTavern = globalThis.SillyTavern;
@@ -29,6 +43,7 @@ const originalGetTavernRegexes = globalThis.getTavernRegexes;
const originalIsCharacterTavernRegexesEnabled =
globalThis.isCharacterTavernRegexesEnabled;
const originalExtensionSettings = globalThis.__taskRegexTestExtensionSettings;
const originalCoreGetRegexedString = globalThis.__taskRegexTestCoreGetRegexedString;
const PLACEMENT = Object.freeze({
USER_INPUT: 1,
@@ -146,6 +161,14 @@ function setTestContext({
};
}
function setCoreRegexedStringHandler(handler = null) {
if (typeof handler === "function") {
globalThis.__taskRegexTestCoreGetRegexedString = handler;
return;
}
delete globalThis.__taskRegexTestCoreGetRegexedString;
}
try {
const { initializeHostAdapter } = await import("../host/adapter/index.js");
const { applyHostRegexReuse, applyTaskRegex, inspectTaskRegexReuse } = await import(
@@ -157,6 +180,8 @@ try {
normalizeTaskProfile,
normalizeTaskRegexStages,
} = await import("../prompting/prompt-profiles.js");
const initializeFallbackHostAdapter = () =>
initializeHostAdapter({ disableCoreRegexBridge: true });
const normalizedLegacyStages = normalizeTaskRegexStages({
finalPrompt: true,
@@ -245,6 +270,48 @@ try {
true,
);
setTestContext({
extensionSettings: {
regex: [],
preset_allowed_regex: {},
character_allowed_regex: [],
},
});
const coreFormatterCalls = [];
setCoreRegexedStringHandler((text, placement, options) => {
coreFormatterCalls.push({ text, placement, options });
return String(text || "").replace(/Alpha/g, "CORE");
});
initializeHostAdapter({});
const coreBridgeDebug = { entries: [] };
const coreBridgeOutput = applyHostRegexReuse(
buildSettings(),
"extract",
"Alpha Beta",
{
sourceType: "user_input",
role: "user",
debugCollector: coreBridgeDebug,
},
);
assert.equal(coreBridgeOutput.text, "CORE Beta");
assert.deepEqual(coreFormatterCalls, [
{
text: "Alpha Beta",
placement: 1,
options: {
isPrompt: true,
isMarkdown: false,
},
},
]);
assert.equal(coreBridgeDebug.entries[0].executionMode, "host-real");
assert.equal(
inspectTaskRegexReuse(buildSettings(), "extract").host.bridgeTier,
"core-real",
);
setCoreRegexedStringHandler(null);
globalThis.getTavernRegexes = () => {
throw new Error("legacy global getter should not be used in regex tests");
};
@@ -333,11 +400,15 @@ try {
},
},
]);
assert.equal(fullBridgeDebug.entries[0].executionMode, "host-real");
assert.equal(fullBridgeDebug.entries[0].executionMode, "host-helper");
assert.deepEqual(
fullBridgeDebug.entries[0].appliedRules.map((item) => item.id),
["__host_formatter__"],
);
assert.equal(
inspectTaskRegexReuse(fullBridgeSettings, "extract").host.bridgeTier,
"helper-bridge",
);
assert.equal(
applyTaskRegex(
fullBridgeSettings,
@@ -383,7 +454,7 @@ try {
},
],
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const fallbackDebug = { entries: [] };
const fallbackOutput = applyHostRegexReuse(
@@ -412,7 +483,7 @@ try {
character_allowed_regex: [],
},
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const depthMissResult = applyHostRegexReuse(
buildSettings({
sources: {
@@ -476,7 +547,7 @@ try {
},
],
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const fallbackInspect = inspectTaskRegexReuse(buildSettings(), "extract");
assert.equal(fallbackInspect.activeRuleCount, 3);
assert.deepEqual(
@@ -525,7 +596,7 @@ try {
},
],
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const disallowedOutput = applyHostRegexReuse(
buildSettings(),
@@ -587,7 +658,7 @@ try {
character_allowed_regex: [],
},
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const userReuseResult = applyHostRegexReuse(
tavernSemanticsSettings,
@@ -641,7 +712,7 @@ try {
character_allowed_regex: [],
},
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const markdownFinalDebug = { entries: [] };
const markdownFallbackResult = applyHostRegexReuse(
markdownOnlyFinalPromptSettings,
@@ -675,7 +746,7 @@ try {
character_allowed_regex: [],
},
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const beautifyFinalInspect = inspectTaskRegexReuse(
beautifyFinalPromptSettings,
"extract",
@@ -759,7 +830,7 @@ try {
character_allowed_regex: [],
},
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const destinationDebug = { entries: [] };
const destinationReuseResult = applyHostRegexReuse(
destinationBeautifySettings,
@@ -817,7 +888,7 @@ try {
character_allowed_regex: [],
},
});
initializeHostAdapter({});
initializeFallbackHostAdapter();
const mixedReuseResult = applyHostRegexReuse(
tavernSemanticsSettings,
"extract",
@@ -889,6 +960,12 @@ try {
globalThis.__taskRegexTestExtensionSettings = originalExtensionSettings;
}
if (originalCoreGetRegexedString === undefined) {
delete globalThis.__taskRegexTestCoreGetRegexedString;
} else {
globalThis.__taskRegexTestCoreGetRegexedString = originalCoreGetRegexedString;
}
try {
const { initializeHostAdapter } = await import("../host/adapter/index.js");
initializeHostAdapter({});