mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
fix(recall): preserve reroll user-floor reuse
This commit is contained in:
@@ -467,9 +467,15 @@ export function onMessageUpdatedController(runtime, messageId, meta = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function onMessageSwipedController(runtime, messageId, meta = null) {
|
export async function onMessageSwipedController(runtime, messageId, meta = null) {
|
||||||
runtime.invalidateRecallAfterHistoryMutation("已切换楼层 swipe");
|
|
||||||
const parsedFloor = Number(messageId);
|
const parsedFloor = Number(messageId);
|
||||||
const fromFloor = Number.isFinite(parsedFloor) ? parsedFloor : undefined;
|
const fromFloor = Number.isFinite(parsedFloor) ? parsedFloor : undefined;
|
||||||
|
const preparedRerollReuse = runtime.prepareRerollRecallReuse?.({
|
||||||
|
fromFloor,
|
||||||
|
meta,
|
||||||
|
});
|
||||||
|
if (!preparedRerollReuse) {
|
||||||
|
runtime.invalidateRecallAfterHistoryMutation("已切换楼层 swipe");
|
||||||
|
}
|
||||||
let result = {
|
let result = {
|
||||||
success: false,
|
success: false,
|
||||||
rollbackPerformed: false,
|
rollbackPerformed: false,
|
||||||
@@ -503,6 +509,9 @@ export async function onMessageSwipedController(runtime, messageId, meta = null)
|
|||||||
{ messageId, meta },
|
{ messageId, meta },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!result?.success) {
|
||||||
|
runtime.clearPendingRerollRecallReuse?.("swipe-reroll-failed");
|
||||||
|
}
|
||||||
runtime.refreshPersistedRecallMessageUi?.();
|
runtime.refreshPersistedRecallMessageUi?.();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
139
index.js
139
index.js
@@ -1356,6 +1356,7 @@ const dismissedStageNoticeSignatures = new Map();
|
|||||||
let pendingRecallSendIntent = createRecallInputRecord();
|
let pendingRecallSendIntent = createRecallInputRecord();
|
||||||
let lastRecallSentUserMessage = createRecallInputRecord();
|
let lastRecallSentUserMessage = createRecallInputRecord();
|
||||||
let pendingHostGenerationInputSnapshot = createRecallInputRecord();
|
let pendingHostGenerationInputSnapshot = createRecallInputRecord();
|
||||||
|
let pendingRerollRecallReuse = null;
|
||||||
let currentGenerationTrivialSkip = null;
|
let currentGenerationTrivialSkip = null;
|
||||||
let coreEventBindingState = {
|
let coreEventBindingState = {
|
||||||
registered: false,
|
registered: false,
|
||||||
@@ -5002,6 +5003,7 @@ function clearRecallInputTracking() {
|
|||||||
clearPendingRecallSendIntent();
|
clearPendingRecallSendIntent();
|
||||||
lastRecallSentUserMessage = createRecallInputRecord();
|
lastRecallSentUserMessage = createRecallInputRecord();
|
||||||
clearPendingHostGenerationInputSnapshot();
|
clearPendingHostGenerationInputSnapshot();
|
||||||
|
clearPendingRerollRecallReuse("recall-input-tracking-cleared");
|
||||||
if (typeof recordMessageTraceSnapshot === "function") {
|
if (typeof recordMessageTraceSnapshot === "function") {
|
||||||
recordMessageTraceSnapshot({
|
recordMessageTraceSnapshot({
|
||||||
lastSentUserMessage: null,
|
lastSentUserMessage: null,
|
||||||
@@ -19466,6 +19468,18 @@ function getLatestUserChatMessage(chat) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findLatestUserChatMessageWithIndex(chat) {
|
||||||
|
if (!Array.isArray(chat)) return null;
|
||||||
|
|
||||||
|
for (let index = chat.length - 1; index >= 0; index--) {
|
||||||
|
const message = chat[index];
|
||||||
|
if (isSystemMessageForExtraction(message, { index, chat })) continue;
|
||||||
|
if (message?.is_user) return { message, index };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function getLastNonSystemChatMessage(chat) {
|
function getLastNonSystemChatMessage(chat) {
|
||||||
if (!Array.isArray(chat)) return null;
|
if (!Array.isArray(chat)) return null;
|
||||||
|
|
||||||
@@ -19479,6 +19493,124 @@ function getLastNonSystemChatMessage(chat) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPendingRerollRecallReuse() {
|
||||||
|
return pendingRerollRecallReuse;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingRerollRecallReuse(reason = "") {
|
||||||
|
const previous = pendingRerollRecallReuse;
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareRerollRecallReuse({ fromFloor = null, meta = null } = {}) {
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context?.chat;
|
||||||
|
if (!Array.isArray(chat) || chat.length === 0) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestUser = findLatestUserChatMessageWithIndex(chat);
|
||||||
|
const targetUserMessageIndex = Number.isFinite(latestUser?.index)
|
||||||
|
? latestUser.index
|
||||||
|
: null;
|
||||||
|
if (!Number.isFinite(targetUserMessageIndex)) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMessage = chat[targetUserMessageIndex];
|
||||||
|
const userText = normalizeRecallInputText(userMessage?.mes || "");
|
||||||
|
if (!userText) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistedRecord = readPersistedRecallFromUserMessage(
|
||||||
|
chat,
|
||||||
|
targetUserMessageIndex,
|
||||||
|
);
|
||||||
|
const persistedInjection = normalizeRecallInputText(
|
||||||
|
persistedRecord?.injectionText || "",
|
||||||
|
);
|
||||||
|
if (!persistedRecord || !persistedInjection) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundText = normalizeRecallInputText(
|
||||||
|
persistedRecord?.boundUserFloorText || persistedRecord?.recallInput || "",
|
||||||
|
);
|
||||||
|
if (boundText && boundText !== userText) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatId = normalizeChatIdCandidate(getCurrentChatId(context));
|
||||||
|
pendingRerollRecallReuse = {
|
||||||
|
chatId,
|
||||||
|
fromFloor: Number.isFinite(Number(fromFloor)) ? Math.floor(Number(fromFloor)) : null,
|
||||||
|
targetUserMessageIndex,
|
||||||
|
userText,
|
||||||
|
userHash: hashRecallInput(userText),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
meta,
|
||||||
|
};
|
||||||
|
return pendingRerollRecallReuse;
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumePendingRerollRecallReuse(chat = getContext()?.chat) {
|
||||||
|
const reuse = pendingRerollRecallReuse;
|
||||||
|
if (!reuse) return null;
|
||||||
|
|
||||||
|
const activeChatId = normalizeChatIdCandidate(getCurrentChatId());
|
||||||
|
if (reuse.chatId && activeChatId && reuse.chatId !== activeChatId) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Date.now() - Number(reuse.createdAt || 0) > GENERATION_RECALL_TRANSACTION_TTL_MS) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestUser = findLatestUserChatMessageWithIndex(chat);
|
||||||
|
const targetUserMessageIndex = Number.isFinite(latestUser?.index)
|
||||||
|
? latestUser.index
|
||||||
|
: reuse.targetUserMessageIndex;
|
||||||
|
if (targetUserMessageIndex !== reuse.targetUserMessageIndex) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userText = normalizeRecallInputText(chat?.[targetUserMessageIndex]?.mes || "");
|
||||||
|
if (!userText || hashRecallInput(userText) !== reuse.userHash) {
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRerollRecallReuse = null;
|
||||||
|
return {
|
||||||
|
overrideUserMessage: userText,
|
||||||
|
generationType: "normal",
|
||||||
|
targetUserMessageIndex,
|
||||||
|
overrideSource: "chat-last-user",
|
||||||
|
overrideSourceLabel: "历史最后用户楼层",
|
||||||
|
overrideReason: "reroll-user-floor-reuse",
|
||||||
|
sourceCandidates: [
|
||||||
|
{
|
||||||
|
text: userText,
|
||||||
|
source: "chat-last-user",
|
||||||
|
sourceLabel: "历史最后用户楼层",
|
||||||
|
reason: "reroll-user-floor-reuse",
|
||||||
|
includeSyntheticUserMessage: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
includeSyntheticUserMessage: false,
|
||||||
|
rerollRecallReuse: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildRecallRecentMessages(chat, limit, syntheticUserMessage = "") {
|
function buildRecallRecentMessages(chat, limit, syntheticUserMessage = "") {
|
||||||
return buildRecallRecentMessagesController(
|
return buildRecallRecentMessagesController(
|
||||||
chat,
|
chat,
|
||||||
@@ -19574,6 +19706,11 @@ function createTrivialRecallSkipSentinel(reason = "") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildNormalGenerationRecallInput(chat, options = {}) {
|
function buildNormalGenerationRecallInput(chat, options = {}) {
|
||||||
|
const rerollReuse = consumePendingRerollRecallReuse(chat);
|
||||||
|
if (rerollReuse) {
|
||||||
|
return rerollReuse;
|
||||||
|
}
|
||||||
|
|
||||||
const lastNonSystemMessage = getLastNonSystemChatMessage(chat);
|
const lastNonSystemMessage = getLastNonSystemChatMessage(chat);
|
||||||
const tailUserText = lastNonSystemMessage?.is_user
|
const tailUserText = lastNonSystemMessage?.is_user
|
||||||
? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
|
? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
|
||||||
@@ -22869,6 +23006,8 @@ async function onMessageSwiped(messageId, meta = null) {
|
|||||||
{
|
{
|
||||||
invalidateRecallAfterHistoryMutation,
|
invalidateRecallAfterHistoryMutation,
|
||||||
onReroll,
|
onReroll,
|
||||||
|
prepareRerollRecallReuse,
|
||||||
|
clearPendingRerollRecallReuse,
|
||||||
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
||||||
scheduleHistoryMutationRecheck,
|
scheduleHistoryMutationRecheck,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export function createGenerationRecallHarness(options = {}) {
|
|||||||
};
|
};
|
||||||
vm.createContext(context);
|
vm.createContext(context);
|
||||||
vm.runInContext(
|
vm.runInContext(
|
||||||
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, persistRecallInjectionRecord, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
|
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, persistRecallInjectionRecord, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, prepareRerollRecallReuse, getPendingRerollRecallReuse, clearPendingRerollRecallReuse, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
|
||||||
context,
|
context,
|
||||||
{ filename: indexPath },
|
{ filename: indexPath },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -508,6 +508,67 @@ assert.equal(
|
|||||||
|
|
||||||
console.log(" ✓ runRecallController reuses persisted record when host reports reroll as normal");
|
console.log(" ✓ runRecallController reuses persisted record when host reports reroll as normal");
|
||||||
|
|
||||||
|
const rerollInputHarness = await createGenerationRecallHarness({ realApplyFinal: true });
|
||||||
|
Object.assign(rerollInputHarness.settings, {
|
||||||
|
...defaultSettings,
|
||||||
|
enabled: true,
|
||||||
|
recallEnabled: true,
|
||||||
|
recallUseAuthoritativeGenerationInput: true,
|
||||||
|
});
|
||||||
|
rerollInputHarness.chat = [
|
||||||
|
{ is_user: true, mes: "重 roll replacement 应复用这一楼" },
|
||||||
|
{ is_user: false, mes: "旧 assistant 回复", is_system: false },
|
||||||
|
];
|
||||||
|
writePersistedRecallToUserMessage(
|
||||||
|
rerollInputHarness.chat,
|
||||||
|
0,
|
||||||
|
buildPersistedRecallRecord({
|
||||||
|
injectionText: "注入:重 roll replacement 应复用这一楼",
|
||||||
|
selectedNodeIds: ["node-reroll-input"],
|
||||||
|
recallInput: "重 roll replacement 应复用这一楼",
|
||||||
|
recallSource: "chat-last-user",
|
||||||
|
hookName: "GENERATION_AFTER_COMMANDS",
|
||||||
|
tokenEstimate: 6,
|
||||||
|
manuallyEdited: false,
|
||||||
|
boundUserFloorText: "重 roll replacement 应复用这一楼",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const preparedRerollReuse = rerollInputHarness.result.prepareRerollRecallReuse({
|
||||||
|
fromFloor: 1,
|
||||||
|
});
|
||||||
|
assert.ok(preparedRerollReuse, "assistant-only reroll should prepare recall reuse marker");
|
||||||
|
|
||||||
|
rerollInputHarness.result.recordRecallSendIntent(
|
||||||
|
"错误的主动输入不应覆盖 reroll 用户楼",
|
||||||
|
"send-intent",
|
||||||
|
);
|
||||||
|
rerollInputHarness.result.freezeHostGenerationInputSnapshot(
|
||||||
|
"错误的宿主快照不应覆盖 reroll 用户楼",
|
||||||
|
"host-generation-lifecycle",
|
||||||
|
);
|
||||||
|
|
||||||
|
const rerollReplacementInput = rerollInputHarness.result.buildNormalGenerationRecallInput(
|
||||||
|
rerollInputHarness.chat,
|
||||||
|
{
|
||||||
|
frozenInputSnapshot: rerollInputHarness.result.getPendingHostGenerationInputSnapshot(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
rerollReplacementInput.overrideUserMessage,
|
||||||
|
"重 roll replacement 应复用这一楼",
|
||||||
|
"reroll replacement should ignore stale live input sources and bind to stable user floor",
|
||||||
|
);
|
||||||
|
assert.equal(rerollReplacementInput.overrideSource, "chat-last-user");
|
||||||
|
assert.equal(rerollReplacementInput.overrideReason, "reroll-user-floor-reuse");
|
||||||
|
assert.equal(
|
||||||
|
rerollInputHarness.result.getPendingRerollRecallReuse(),
|
||||||
|
null,
|
||||||
|
"reroll reuse marker should be one-shot after binding recall input",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(" ✓ reroll replacement normal input is forced to stable user-floor recall source");
|
||||||
|
|
||||||
const legacyUnboundReuseChat = [
|
const legacyUnboundReuseChat = [
|
||||||
{ is_user: true, mes: "旧记录没有绑定楼层" },
|
{ is_user: true, mes: "旧记录没有绑定楼层" },
|
||||||
{ is_user: false, mes: "上一条回复。", is_system: false },
|
{ is_user: false, mes: "上一条回复。", is_system: false },
|
||||||
|
|||||||
Reference in New Issue
Block a user