mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 14:20:35 +08:00
fix(recall): prefer user-floor cache on reroll
This commit is contained in:
@@ -442,6 +442,10 @@ export function onMessageEditedController(runtime, messageId, meta = null) {
|
|||||||
runtime.refreshPersistedRecallMessageUi?.();
|
runtime.refreshPersistedRecallMessageUi?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const parsedMessageId = Number(messageId);
|
||||||
|
if (Number.isFinite(parsedMessageId)) {
|
||||||
|
runtime.removeMessageRecallRecord?.(Math.floor(parsedMessageId));
|
||||||
|
}
|
||||||
runtime.invalidateRecallAfterHistoryMutation("消息已编辑");
|
runtime.invalidateRecallAfterHistoryMutation("消息已编辑");
|
||||||
runtime.scheduleHistoryMutationRecheck("message-edited", messageId, meta);
|
runtime.scheduleHistoryMutationRecheck("message-edited", messageId, meta);
|
||||||
runtime.refreshPersistedRecallMessageUi?.();
|
runtime.refreshPersistedRecallMessageUi?.();
|
||||||
|
|||||||
1
index.js
1
index.js
@@ -22469,6 +22469,7 @@ function onMessageEdited(messageId, meta = null) {
|
|||||||
{
|
{
|
||||||
invalidateRecallAfterHistoryMutation,
|
invalidateRecallAfterHistoryMutation,
|
||||||
isMvuExtraAnalysisGuardActive,
|
isMvuExtraAnalysisGuardActive,
|
||||||
|
removeMessageRecallRecord,
|
||||||
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
||||||
scheduleHistoryMutationRecheck,
|
scheduleHistoryMutationRecheck,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ function buildPersistedRecallReuseResult(record = {}) {
|
|||||||
|
|
||||||
function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
|
function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
|
||||||
const generationType = String(recallInput?.generationType || "normal").trim() || "normal";
|
const generationType = String(recallInput?.generationType || "normal").trim() || "normal";
|
||||||
if (generationType === "normal") return null;
|
|
||||||
|
|
||||||
const targetUserMessageIndex = Number.isFinite(recallInput?.targetUserMessageIndex)
|
const targetUserMessageIndex = Number.isFinite(recallInput?.targetUserMessageIndex)
|
||||||
? Math.floor(Number(recallInput.targetUserMessageIndex))
|
? Math.floor(Number(recallInput.targetUserMessageIndex))
|
||||||
@@ -119,6 +118,16 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
|
|||||||
const currentRecallInputText = normalizeText(recallInput?.userMessage || "");
|
const currentRecallInputText = normalizeText(recallInput?.userMessage || "");
|
||||||
const recordRecallInput = normalizeText(record?.recallInput || "");
|
const recordRecallInput = normalizeText(record?.recallInput || "");
|
||||||
const boundUserFloorText = normalizeText(record?.boundUserFloorText || "");
|
const boundUserFloorText = normalizeText(record?.boundUserFloorText || "");
|
||||||
|
const recallSource = String(recallInput?.source || "").trim();
|
||||||
|
const activeInputSources = new Set([
|
||||||
|
"send-intent",
|
||||||
|
"generation-started-send-intent",
|
||||||
|
"generation-started-textarea",
|
||||||
|
"host-generation-lifecycle",
|
||||||
|
"textarea-live",
|
||||||
|
"planner-handoff",
|
||||||
|
]);
|
||||||
|
const isActiveInputSource = activeInputSources.has(recallSource);
|
||||||
|
|
||||||
const matchesBoundUserFloor = Boolean(
|
const matchesBoundUserFloor = Boolean(
|
||||||
currentUserFloorText &&
|
currentUserFloorText &&
|
||||||
@@ -135,10 +144,38 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
|
|||||||
boundUserFloorText &&
|
boundUserFloorText &&
|
||||||
currentUserFloorText === boundUserFloorText,
|
currentUserFloorText === boundUserFloorText,
|
||||||
);
|
);
|
||||||
|
const boundUserFloorMismatch = Boolean(
|
||||||
|
boundUserFloorText && currentUserFloorText !== boundUserFloorText,
|
||||||
|
);
|
||||||
|
if (boundUserFloorMismatch) return null;
|
||||||
|
|
||||||
if (record.authoritativeInputUsed) {
|
const matchesPersistedRecord = record.authoritativeInputUsed
|
||||||
if (!matchesBoundUserFloor) return null;
|
? matchesBoundUserFloor
|
||||||
} else if (!matchesRecallInput && !matchesCurrentUserFloor) {
|
: matchesRecallInput || matchesCurrentUserFloor;
|
||||||
|
const canReuseUnboundTargetRecord = Boolean(
|
||||||
|
currentUserFloorText &&
|
||||||
|
!boundUserFloorText &&
|
||||||
|
!isActiveInputSource &&
|
||||||
|
String(record?.injectionText || "").trim(),
|
||||||
|
);
|
||||||
|
const userFloorSources = new Set([
|
||||||
|
"chat-last-user",
|
||||||
|
"chat-latest-user",
|
||||||
|
"chat-tail-user",
|
||||||
|
"message-sent",
|
||||||
|
"persisted-user-floor",
|
||||||
|
]);
|
||||||
|
const canTrustUserFloorRecord = Boolean(
|
||||||
|
!isActiveInputSource &&
|
||||||
|
!boundUserFloorText &&
|
||||||
|
(generationType !== "normal" || userFloorSources.has(recallSource)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!matchesPersistedRecord &&
|
||||||
|
!canReuseUnboundTargetRecord &&
|
||||||
|
!canTrustUserFloorRecord
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
onMessageEditedController,
|
||||||
onMessageUpdatedController,
|
onMessageUpdatedController,
|
||||||
registerCoreEventHooksController,
|
registerCoreEventHooksController,
|
||||||
} from "../host/event-binding.js";
|
} from "../host/event-binding.js";
|
||||||
@@ -38,6 +39,38 @@ import {
|
|||||||
assert.equal(ignored?.detail?.reason, "lightweight-refresh-only");
|
assert.equal(ignored?.detail?.reason, "lightweight-refresh-only");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let removedMessageIndex = null;
|
||||||
|
let invalidated = 0;
|
||||||
|
let rechecked = 0;
|
||||||
|
let refreshed = 0;
|
||||||
|
|
||||||
|
onMessageEditedController(
|
||||||
|
{
|
||||||
|
isMvuExtraAnalysisGuardActive: () => false,
|
||||||
|
removeMessageRecallRecord(messageIndex) {
|
||||||
|
removedMessageIndex = messageIndex;
|
||||||
|
},
|
||||||
|
invalidateRecallAfterHistoryMutation() {
|
||||||
|
invalidated += 1;
|
||||||
|
},
|
||||||
|
scheduleHistoryMutationRecheck() {
|
||||||
|
rechecked += 1;
|
||||||
|
},
|
||||||
|
refreshPersistedRecallMessageUi() {
|
||||||
|
refreshed += 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
9,
|
||||||
|
{ source: "unit-test" },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(removedMessageIndex, 9);
|
||||||
|
assert.equal(invalidated, 1);
|
||||||
|
assert.equal(rechecked, 1);
|
||||||
|
assert.equal(refreshed, 1);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const bindings = [];
|
const bindings = [];
|
||||||
const runtime = {
|
const runtime = {
|
||||||
|
|||||||
@@ -432,8 +432,210 @@ assert.equal(
|
|||||||
|
|
||||||
console.log(" ✓ runRecallController reuses persisted record on regenerate");
|
console.log(" ✓ runRecallController reuses persisted record on regenerate");
|
||||||
|
|
||||||
|
const normalTypedReuseChat = [
|
||||||
|
{ is_user: true, mes: "重 Roll 但宿主仍标 normal" },
|
||||||
|
{ is_user: false, mes: "上一条回复。", is_system: false },
|
||||||
|
];
|
||||||
|
const normalTypedReuseRecord = buildPersistedRecallRecord({
|
||||||
|
injectionText: "注入:重 Roll 但宿主仍标 normal",
|
||||||
|
selectedNodeIds: ["node-normal-type"],
|
||||||
|
recallInput: "旧捕获文本",
|
||||||
|
recallSource: "chat-last-user",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
tokenEstimate: 5,
|
||||||
|
manuallyEdited: false,
|
||||||
|
boundUserFloorText: "重 Roll 但宿主仍标 normal",
|
||||||
|
});
|
||||||
|
writePersistedRecallToUserMessage(normalTypedReuseChat, 0, normalTypedReuseRecord);
|
||||||
|
|
||||||
|
let normalTypedRetrieveCalled = false;
|
||||||
|
const normalTypedReuseRuntime = {
|
||||||
|
...rerollRuntime,
|
||||||
|
getContext: () => ({ chat: normalTypedReuseChat, chatId: "chat-normal-typed-reroll" }),
|
||||||
|
retrieve: async () => {
|
||||||
|
normalTypedRetrieveCalled = true;
|
||||||
|
return {
|
||||||
|
injectionText: "不应出现的新召回",
|
||||||
|
selectedNodeIds: ["node-new"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalTypedReuseResult = await runRecallController(normalTypedReuseRuntime, {
|
||||||
|
overrideUserMessage: "重 Roll 但宿主仍标 normal",
|
||||||
|
generationType: "normal",
|
||||||
|
targetUserMessageIndex: 0,
|
||||||
|
overrideSource: "chat-last-user",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
deliveryMode: "immediate",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
normalTypedReuseResult.reason,
|
||||||
|
"persisted-user-floor-reused",
|
||||||
|
"normal-typed reroll should still reuse target user-floor recall",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
normalTypedRetrieveCalled,
|
||||||
|
false,
|
||||||
|
"normal-typed reroll should not call retrieve when target user floor has recall",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(" ✓ runRecallController reuses persisted record when host reports reroll as normal");
|
||||||
|
|
||||||
|
const legacyUnboundReuseChat = [
|
||||||
|
{ is_user: true, mes: "旧记录没有绑定楼层" },
|
||||||
|
{ is_user: false, mes: "上一条回复。", is_system: false },
|
||||||
|
];
|
||||||
|
const legacyUnboundRecord = buildPersistedRecallRecord({
|
||||||
|
injectionText: "注入:旧记录没有绑定楼层",
|
||||||
|
selectedNodeIds: ["node-legacy-unbound"],
|
||||||
|
recallInput: "历史旧输入",
|
||||||
|
recallSource: "chat-last-user",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
tokenEstimate: 5,
|
||||||
|
manuallyEdited: false,
|
||||||
|
});
|
||||||
|
writePersistedRecallToUserMessage(legacyUnboundReuseChat, 0, legacyUnboundRecord);
|
||||||
|
|
||||||
|
let legacyUnboundRetrieveCalled = false;
|
||||||
|
const legacyUnboundRuntime = {
|
||||||
|
...rerollRuntime,
|
||||||
|
getContext: () => ({ chat: legacyUnboundReuseChat, chatId: "chat-legacy-unbound" }),
|
||||||
|
retrieve: async () => {
|
||||||
|
legacyUnboundRetrieveCalled = true;
|
||||||
|
return {
|
||||||
|
injectionText: "不应出现的旧记录新召回",
|
||||||
|
selectedNodeIds: ["node-new"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyUnboundResult = await runRecallController(legacyUnboundRuntime, {
|
||||||
|
overrideUserMessage: "旧记录没有绑定楼层",
|
||||||
|
generationType: "normal",
|
||||||
|
targetUserMessageIndex: 0,
|
||||||
|
overrideSource: "chat-last-user",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
deliveryMode: "immediate",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
legacyUnboundResult.reason,
|
||||||
|
"persisted-user-floor-reused",
|
||||||
|
"legacy unbound user-floor recall should be reused for normal-typed history generation",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
legacyUnboundRetrieveCalled,
|
||||||
|
false,
|
||||||
|
"legacy unbound user-floor recall should not call retrieve",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(" ✓ runRecallController reuses legacy unbound user-floor recall");
|
||||||
|
|
||||||
|
const activeInputUnboundChat = [
|
||||||
|
{ is_user: true, mes: "主动新输入不应复用旧召回" },
|
||||||
|
{ is_user: false, mes: "上一条回复。", is_system: false },
|
||||||
|
];
|
||||||
|
const activeInputUnboundRecord = buildPersistedRecallRecord({
|
||||||
|
injectionText: "旧注入:主动新输入不应复用旧召回",
|
||||||
|
selectedNodeIds: ["node-active-old"],
|
||||||
|
recallInput: "旧输入",
|
||||||
|
recallSource: "send-intent",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
tokenEstimate: 5,
|
||||||
|
manuallyEdited: false,
|
||||||
|
});
|
||||||
|
writePersistedRecallToUserMessage(activeInputUnboundChat, 0, activeInputUnboundRecord);
|
||||||
|
|
||||||
|
let activeInputRetrieveCalled = false;
|
||||||
|
const activeInputRuntime = {
|
||||||
|
...rerollRuntime,
|
||||||
|
getContext: () => ({ chat: activeInputUnboundChat, chatId: "chat-active-input" }),
|
||||||
|
retrieve: async () => {
|
||||||
|
activeInputRetrieveCalled = true;
|
||||||
|
return {
|
||||||
|
injectionText: "新召回:主动新输入不应复用旧召回",
|
||||||
|
selectedNodeIds: ["node-active-new"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeInputResult = await runRecallController(activeInputRuntime, {
|
||||||
|
overrideUserMessage: "主动新输入不应复用旧召回",
|
||||||
|
generationType: "normal",
|
||||||
|
targetUserMessageIndex: 0,
|
||||||
|
overrideSource: "send-intent",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
deliveryMode: "immediate",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
activeInputRetrieveCalled,
|
||||||
|
true,
|
||||||
|
"active send-intent input should not reuse an unbound stale record",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
activeInputResult.injectionText,
|
||||||
|
"新召回:主动新输入不应复用旧召回",
|
||||||
|
"active send-intent input should use the fresh recall result",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(" ✓ runRecallController does not reuse unbound record for active input");
|
||||||
|
|
||||||
|
const mismatchedBoundChat = [
|
||||||
|
{ is_user: true, mes: "已经编辑过的新楼层" },
|
||||||
|
{ is_user: false, mes: "上一条回复。", is_system: false },
|
||||||
|
];
|
||||||
|
const mismatchedBoundRecord = buildPersistedRecallRecord({
|
||||||
|
injectionText: "旧注入:已经编辑过的新楼层",
|
||||||
|
selectedNodeIds: ["node-mismatch-old"],
|
||||||
|
recallInput: "已经编辑过的新楼层",
|
||||||
|
recallSource: "chat-last-user",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
tokenEstimate: 5,
|
||||||
|
manuallyEdited: false,
|
||||||
|
boundUserFloorText: "编辑前的旧楼层",
|
||||||
|
});
|
||||||
|
writePersistedRecallToUserMessage(mismatchedBoundChat, 0, mismatchedBoundRecord);
|
||||||
|
|
||||||
|
let mismatchedBoundRetrieveCalled = false;
|
||||||
|
const mismatchedBoundRuntime = {
|
||||||
|
...rerollRuntime,
|
||||||
|
getContext: () => ({ chat: mismatchedBoundChat, chatId: "chat-bound-mismatch" }),
|
||||||
|
retrieve: async () => {
|
||||||
|
mismatchedBoundRetrieveCalled = true;
|
||||||
|
return {
|
||||||
|
injectionText: "新召回:已经编辑过的新楼层",
|
||||||
|
selectedNodeIds: ["node-mismatch-new"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mismatchedBoundResult = await runRecallController(mismatchedBoundRuntime, {
|
||||||
|
overrideUserMessage: "已经编辑过的新楼层",
|
||||||
|
generationType: "normal",
|
||||||
|
targetUserMessageIndex: 0,
|
||||||
|
overrideSource: "chat-last-user",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
deliveryMode: "immediate",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
mismatchedBoundRetrieveCalled,
|
||||||
|
true,
|
||||||
|
"bound user-floor mismatch should force a fresh recall",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
mismatchedBoundResult.injectionText,
|
||||||
|
"新召回:已经编辑过的新楼层",
|
||||||
|
"bound user-floor mismatch should not reuse stale persisted recall",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(" ✓ runRecallController does not reuse record when bound user floor mismatches");
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// 4. runRecallController: regenerate with empty recallInput does NOT reuse
|
// 4. runRecallController: regenerate with empty recallInput reuses user-floor record
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const noReuseChat = [
|
const noReuseChat = [
|
||||||
@@ -494,11 +696,16 @@ const noReuseResult = await runRecallController(noReuseRuntime, {
|
|||||||
assert.equal(noReuseResult.status, "completed", "no-reuse should complete");
|
assert.equal(noReuseResult.status, "completed", "no-reuse should complete");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
noReuseRetrieveCalled,
|
noReuseRetrieveCalled,
|
||||||
true,
|
false,
|
||||||
"retrieve() SHOULD be called when persisted record has empty recallInput",
|
"retrieve() should NOT be called when target user floor has a persisted recall record",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
noReuseResult.reason,
|
||||||
|
"persisted-user-floor-reused",
|
||||||
|
"empty recallInput legacy records should still be reused from the user floor",
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(" ✓ runRecallController does NOT reuse record with empty recallInput");
|
console.log(" ✓ runRecallController reuses user-floor record with empty recallInput");
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
console.log("recall-reroll-reuse tests passed");
|
console.log("recall-reroll-reuse tests passed");
|
||||||
|
|||||||
Reference in New Issue
Block a user