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?.();
|
||||
return;
|
||||
}
|
||||
const parsedMessageId = Number(messageId);
|
||||
if (Number.isFinite(parsedMessageId)) {
|
||||
runtime.removeMessageRecallRecord?.(Math.floor(parsedMessageId));
|
||||
}
|
||||
runtime.invalidateRecallAfterHistoryMutation("消息已编辑");
|
||||
runtime.scheduleHistoryMutationRecheck("message-edited", messageId, meta);
|
||||
runtime.refreshPersistedRecallMessageUi?.();
|
||||
|
||||
1
index.js
1
index.js
@@ -22469,6 +22469,7 @@ function onMessageEdited(messageId, meta = null) {
|
||||
{
|
||||
invalidateRecallAfterHistoryMutation,
|
||||
isMvuExtraAnalysisGuardActive,
|
||||
removeMessageRecallRecord,
|
||||
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
||||
scheduleHistoryMutationRecheck,
|
||||
},
|
||||
|
||||
@@ -93,7 +93,6 @@ function buildPersistedRecallReuseResult(record = {}) {
|
||||
|
||||
function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
|
||||
const generationType = String(recallInput?.generationType || "normal").trim() || "normal";
|
||||
if (generationType === "normal") return null;
|
||||
|
||||
const targetUserMessageIndex = Number.isFinite(recallInput?.targetUserMessageIndex)
|
||||
? Math.floor(Number(recallInput.targetUserMessageIndex))
|
||||
@@ -119,6 +118,16 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
|
||||
const currentRecallInputText = normalizeText(recallInput?.userMessage || "");
|
||||
const recordRecallInput = normalizeText(record?.recallInput || "");
|
||||
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(
|
||||
currentUserFloorText &&
|
||||
@@ -135,10 +144,38 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
|
||||
boundUserFloorText &&
|
||||
currentUserFloorText === boundUserFloorText,
|
||||
);
|
||||
const boundUserFloorMismatch = Boolean(
|
||||
boundUserFloorText && currentUserFloorText !== boundUserFloorText,
|
||||
);
|
||||
if (boundUserFloorMismatch) return null;
|
||||
|
||||
if (record.authoritativeInputUsed) {
|
||||
if (!matchesBoundUserFloor) return null;
|
||||
} else if (!matchesRecallInput && !matchesCurrentUserFloor) {
|
||||
const matchesPersistedRecord = record.authoritativeInputUsed
|
||||
? matchesBoundUserFloor
|
||||
: 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
onMessageEditedController,
|
||||
onMessageUpdatedController,
|
||||
registerCoreEventHooksController,
|
||||
} from "../host/event-binding.js";
|
||||
@@ -38,6 +39,38 @@ import {
|
||||
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 runtime = {
|
||||
|
||||
@@ -432,8 +432,210 @@ assert.equal(
|
||||
|
||||
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 = [
|
||||
@@ -494,11 +696,16 @@ const noReuseResult = await runRecallController(noReuseRuntime, {
|
||||
assert.equal(noReuseResult.status, "completed", "no-reuse should complete");
|
||||
assert.equal(
|
||||
noReuseRetrieveCalled,
|
||||
true,
|
||||
"retrieve() SHOULD be called when persisted record has empty recallInput",
|
||||
false,
|
||||
"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");
|
||||
|
||||
Reference in New Issue
Block a user