mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Fix recall card persistence backfill
This commit is contained in:
@@ -427,8 +427,16 @@ export async function onGenerationAfterCommandsController(
|
||||
// 后续 GENERATE_BEFORE_COMBINE_PROMPTS 阶段会通过
|
||||
// applyFinalRecallInjectionForGeneration 做 deferred rewrite 兜底。
|
||||
if (deliveryMode === "immediate") {
|
||||
// immediate 路径下 runRecall 已经完成持久化 recall record,
|
||||
// 这里补一次 UI 刷新,避免需要等到消息编辑/历史恢复后才看到 Recall Card。
|
||||
runtime.ensurePersistedRecallRecordForGeneration?.({
|
||||
generationType: recallContext.generationType,
|
||||
recallResult,
|
||||
transaction: recallContext.transaction,
|
||||
recallOptions: runtimeRecallOptions,
|
||||
hookName: recallContext.hookName,
|
||||
});
|
||||
// immediate 路径通常会在 runRecall 内完成持久化;如果当时 user 楼层还没稳定,
|
||||
// 上面的兜底补写会把 fresh recall 绑定回最终 user 楼层。
|
||||
// 这里再补一次 UI 刷新,避免需要等到消息编辑/历史恢复后才看到 Recall Card。
|
||||
runtime.refreshPersistedRecallMessageUi?.();
|
||||
console.warn("[ST-BME:DIAG] DONE: immediate mode, injection via setExtensionPrompt in runRecall");
|
||||
return recallResult;
|
||||
|
||||
186
index.js
186
index.js
@@ -1128,6 +1128,16 @@ function normalizeRecallNodeIdList(nodeIds = []) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function areRecallNodeIdListsEqual(left = [], right = []) {
|
||||
const normalizedLeft = normalizeRecallNodeIdList(left);
|
||||
const normalizedRight = normalizeRecallNodeIdList(right);
|
||||
if (normalizedLeft.length !== normalizedRight.length) return false;
|
||||
for (let index = 0; index < normalizedLeft.length; index++) {
|
||||
if (normalizedLeft[index] !== normalizedRight[index]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLatestPersistedRecallDisplayRecord(chat = getContext()?.chat) {
|
||||
if (!Array.isArray(chat) || chat.length === 0) return null;
|
||||
for (let index = chat.length - 1; index >= 0; index--) {
|
||||
@@ -1541,6 +1551,170 @@ function persistRecallInjectionRecord({
|
||||
};
|
||||
}
|
||||
|
||||
function ensurePersistedRecallRecordForGeneration({
|
||||
generationType = "normal",
|
||||
recallResult = null,
|
||||
transaction = null,
|
||||
recallOptions = null,
|
||||
hookName = "",
|
||||
} = {}) {
|
||||
const injectionText = String(recallResult?.injectionText || "").trim();
|
||||
if (
|
||||
recallResult?.status !== "completed" ||
|
||||
!recallResult?.didRecall ||
|
||||
!injectionText
|
||||
) {
|
||||
return {
|
||||
persisted: false,
|
||||
reason: "no-fresh-recall",
|
||||
targetUserMessageIndex: null,
|
||||
record: null,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getContext()?.chat;
|
||||
if (!Array.isArray(chat) || chat.length === 0) {
|
||||
return {
|
||||
persisted: false,
|
||||
reason: "missing-chat",
|
||||
targetUserMessageIndex: null,
|
||||
record: null,
|
||||
};
|
||||
}
|
||||
|
||||
const frozenRecallOptions =
|
||||
transaction?.frozenRecallOptions &&
|
||||
typeof transaction.frozenRecallOptions === "object"
|
||||
? transaction.frozenRecallOptions
|
||||
: null;
|
||||
const targetUserMessageIndex = resolveRecallPersistenceTargetUserMessageIndex(
|
||||
chat,
|
||||
{
|
||||
generationType,
|
||||
explicitTargetUserMessageIndex:
|
||||
frozenRecallOptions?.targetUserMessageIndex ??
|
||||
recallOptions?.targetUserMessageIndex ??
|
||||
recallOptions?.explicitTargetUserMessageIndex ??
|
||||
null,
|
||||
candidateTexts: [
|
||||
frozenRecallOptions?.overrideUserMessage,
|
||||
frozenRecallOptions?.userMessage,
|
||||
recallOptions?.overrideUserMessage,
|
||||
recallOptions?.userMessage,
|
||||
recallResult?.recallInput,
|
||||
recallResult?.userMessage,
|
||||
...(Array.isArray(recallResult?.sourceCandidates)
|
||||
? recallResult.sourceCandidates.map((candidate) => candidate?.text)
|
||||
: []),
|
||||
lastRecallSentUserMessage?.text,
|
||||
],
|
||||
preferredRecord: lastRecallSentUserMessage,
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
!Number.isFinite(targetUserMessageIndex) ||
|
||||
!chat[targetUserMessageIndex]?.is_user
|
||||
) {
|
||||
return {
|
||||
persisted: false,
|
||||
reason: "target-unresolved",
|
||||
targetUserMessageIndex: Number.isFinite(targetUserMessageIndex)
|
||||
? targetUserMessageIndex
|
||||
: null,
|
||||
record: null,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedNodeIds = normalizeRecallNodeIdList(
|
||||
recallResult?.selectedNodeIds || [],
|
||||
);
|
||||
const existingRecord = readPersistedRecallFromUserMessage(
|
||||
chat,
|
||||
targetUserMessageIndex,
|
||||
);
|
||||
if (
|
||||
existingRecord &&
|
||||
String(existingRecord.injectionText || "").trim() === injectionText &&
|
||||
areRecallNodeIdListsEqual(existingRecord.selectedNodeIds, selectedNodeIds)
|
||||
) {
|
||||
return {
|
||||
persisted: false,
|
||||
reason: "already-up-to-date",
|
||||
targetUserMessageIndex,
|
||||
record: existingRecord,
|
||||
};
|
||||
}
|
||||
|
||||
const nextRecord = buildPersistedRecallRecord(
|
||||
{
|
||||
injectionText,
|
||||
selectedNodeIds,
|
||||
recallInput: String(
|
||||
recallResult?.recallInput ||
|
||||
recallResult?.userMessage ||
|
||||
frozenRecallOptions?.overrideUserMessage ||
|
||||
recallOptions?.overrideUserMessage ||
|
||||
recallOptions?.userMessage ||
|
||||
"",
|
||||
),
|
||||
recallSource: String(
|
||||
recallResult?.source ||
|
||||
frozenRecallOptions?.lockedSource ||
|
||||
frozenRecallOptions?.overrideSource ||
|
||||
recallOptions?.overrideSource ||
|
||||
"",
|
||||
),
|
||||
hookName: String(
|
||||
hookName ||
|
||||
recallResult?.hookName ||
|
||||
frozenRecallOptions?.hookName ||
|
||||
recallOptions?.hookName ||
|
||||
"",
|
||||
),
|
||||
tokenEstimate: estimateTokens(injectionText),
|
||||
manuallyEdited: false,
|
||||
},
|
||||
existingRecord,
|
||||
);
|
||||
|
||||
if (!writePersistedRecallToUserMessage(chat, targetUserMessageIndex, nextRecord)) {
|
||||
return {
|
||||
persisted: false,
|
||||
reason: "write-failed",
|
||||
targetUserMessageIndex,
|
||||
record: null,
|
||||
};
|
||||
}
|
||||
|
||||
triggerChatMetadataSave(getContext(), { immediate: false });
|
||||
schedulePersistedRecallMessageUiRefresh();
|
||||
debugPersistedRecallPersistence(
|
||||
"最终阶段已补写召回记录",
|
||||
{
|
||||
targetUserMessageIndex,
|
||||
hookName:
|
||||
String(
|
||||
hookName ||
|
||||
recallResult?.hookName ||
|
||||
frozenRecallOptions?.hookName ||
|
||||
recallOptions?.hookName ||
|
||||
"",
|
||||
) || "",
|
||||
injectionTextLength: injectionText.length,
|
||||
selectedNodeCount: selectedNodeIds.length,
|
||||
},
|
||||
`finalize-persist:${targetUserMessageIndex}`,
|
||||
);
|
||||
|
||||
return {
|
||||
persisted: true,
|
||||
reason: "backfilled",
|
||||
targetUserMessageIndex,
|
||||
record: nextRecord,
|
||||
};
|
||||
}
|
||||
|
||||
function removeMessageRecallRecord(messageIndex) {
|
||||
const chat = getContext()?.chat;
|
||||
if (!Array.isArray(chat)) return false;
|
||||
@@ -1753,6 +1927,14 @@ function applyFinalRecallInjectionForGeneration({
|
||||
return emptyResolution;
|
||||
}
|
||||
|
||||
const ensuredPersistence = ensurePersistedRecallRecordForGeneration({
|
||||
generationType,
|
||||
recallResult,
|
||||
transaction,
|
||||
recallOptions: transaction?.frozenRecallOptions || null,
|
||||
hookName,
|
||||
});
|
||||
|
||||
targetUserMessageIndex = resolveRecallPersistenceTargetUserMessageIndex(chat, {
|
||||
generationType,
|
||||
explicitTargetUserMessageIndex:
|
||||
@@ -1766,6 +1948,9 @@ function applyFinalRecallInjectionForGeneration({
|
||||
],
|
||||
preferredRecord: lastRecallSentUserMessage,
|
||||
});
|
||||
if (Number.isFinite(ensuredPersistence?.targetUserMessageIndex)) {
|
||||
targetUserMessageIndex = ensuredPersistence.targetUserMessageIndex;
|
||||
}
|
||||
|
||||
const persistedRecord = Number.isFinite(targetUserMessageIndex)
|
||||
? readPersistedRecallFromUserMessage(chat, targetUserMessageIndex)
|
||||
@@ -8628,6 +8813,7 @@ async function onGenerationAfterCommands(type, params = {}, dryRun = false) {
|
||||
clearLiveRecallInjectionPromptForRewrite,
|
||||
consumeHostGenerationInputSnapshot,
|
||||
createGenerationRecallContext,
|
||||
ensurePersistedRecallRecordForGeneration,
|
||||
getContext,
|
||||
getGenerationRecallHookStateFromResult,
|
||||
getGenerationRecallTransactionResult,
|
||||
|
||||
@@ -365,15 +365,11 @@ function createGenerationRecallHarness(options = {}) {
|
||||
onBeforeCombinePromptsController,
|
||||
onGenerationAfterCommandsController,
|
||||
onGenerationStartedController,
|
||||
readPersistedRecallFromUserMessage: () => null,
|
||||
resolveFinalRecallInjectionSource: ({
|
||||
freshRecallResult = null,
|
||||
} = {}) => ({
|
||||
source: freshRecallResult?.didRecall ? "fresh" : "none",
|
||||
injectionText: String(freshRecallResult?.injectionText || ""),
|
||||
record: null,
|
||||
}),
|
||||
bumpPersistedRecallGenerationCount: () => null,
|
||||
readPersistedRecallFromUserMessage,
|
||||
writePersistedRecallToUserMessage,
|
||||
buildPersistedRecallRecord,
|
||||
resolveFinalRecallInjectionSource,
|
||||
bumpPersistedRecallGenerationCount,
|
||||
applyModuleInjectionPrompt: (text = "") => {
|
||||
const normalizedText = String(text || "");
|
||||
context.moduleInjectionCalls.push(normalizedText);
|
||||
@@ -384,14 +380,23 @@ function createGenerationRecallHarness(options = {}) {
|
||||
};
|
||||
},
|
||||
getSettings: () => ({}),
|
||||
triggerChatMetadataSave: () => "debounced",
|
||||
triggerChatMetadataSave: () => {
|
||||
context.metadataSaveCalls += 1;
|
||||
return "debounced";
|
||||
},
|
||||
refreshPanelLiveState: () => {
|
||||
context.refreshPanelCalls += 1;
|
||||
},
|
||||
recordInjectionSnapshot: (_kind, snapshot = {}) => {
|
||||
context.recordedInjectionSnapshots.push({ ...snapshot });
|
||||
},
|
||||
schedulePersistedRecallMessageUiRefresh: () => {},
|
||||
schedulePersistedRecallMessageUiRefresh: () => {
|
||||
context.recallUiRefreshCalls += 1;
|
||||
},
|
||||
estimateTokens: (text = "") =>
|
||||
normalizeRecallInputText(text)
|
||||
.split(/\s+/)
|
||||
.filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
|
||||
resolveGenerationTargetUserMessageIndex: (
|
||||
chat = [],
|
||||
{ generationType } = {},
|
||||
@@ -404,6 +409,8 @@ function createGenerationRecallHarness(options = {}) {
|
||||
if (chat[index]?.is_user) return index;
|
||||
return null;
|
||||
},
|
||||
metadataSaveCalls: 0,
|
||||
recallUiRefreshCalls: 0,
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(
|
||||
@@ -3634,6 +3641,71 @@ async function testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor(
|
||||
}
|
||||
}
|
||||
|
||||
async function testGenerationRecallFinalInjectionBackfillsPersistedRecord() {
|
||||
const harness = await createGenerationRecallHarness({ realApplyFinal: true });
|
||||
harness.chat = [
|
||||
{ is_user: true, mes: "最终阶段补写目标" },
|
||||
{ is_user: false, mes: "assistant-tail" },
|
||||
];
|
||||
harness.result.recordRecallSentUserMessage(0, "最终阶段补写目标", "message-sent");
|
||||
|
||||
const resolution =
|
||||
harness.result.applyFinalRecallInjectionForGeneration({
|
||||
generationType: "normal",
|
||||
hookName: "GENERATION_AFTER_COMMANDS",
|
||||
freshRecallResult: {
|
||||
status: "completed",
|
||||
didRecall: true,
|
||||
injectionText: "fresh-memory",
|
||||
selectedNodeIds: ["node-a", "node-b"],
|
||||
},
|
||||
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(
|
||||
harness.chat[0]?.extra?.bme_recall?.injectionText,
|
||||
"fresh-memory",
|
||||
);
|
||||
assert.deepEqual(
|
||||
harness.chat[0]?.extra?.bme_recall?.selectedNodeIds,
|
||||
["node-a", "node-b"],
|
||||
);
|
||||
assert.equal(harness.metadataSaveCalls > 0, true);
|
||||
}
|
||||
|
||||
async function testGenerationRecallImmediateAfterCommandsBackfillsPersistedRecord() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: true, mes: "即时模式补写目标" }];
|
||||
harness.result.recordRecallSentUserMessage(0, "即时模式补写目标", "message-sent");
|
||||
|
||||
const result = await harness.result.onGenerationAfterCommands(
|
||||
"normal",
|
||||
{},
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(result?.status, "completed");
|
||||
assert.equal(
|
||||
harness.chat[0]?.extra?.bme_recall?.injectionText,
|
||||
"注入:即时模式补写目标",
|
||||
);
|
||||
assert.deepEqual(
|
||||
harness.chat[0]?.extra?.bme_recall?.selectedNodeIds,
|
||||
["node-test-1"],
|
||||
);
|
||||
assert.equal(harness.metadataSaveCalls > 0, true);
|
||||
}
|
||||
|
||||
async function testRecallSubGraphAndDataLayerEntryPoints() {
|
||||
// Sub-graph build test (pure function, no DOM needed)
|
||||
const { buildRecallSubGraph } = await import("../recall-message-ui.js");
|
||||
@@ -4443,6 +4515,8 @@ await testGenerationRecallDeferredRewriteMutatesFinalMesSendPayload();
|
||||
await testPersistentRecallDataLayerLifecycleAndCompatibility();
|
||||
await testPersistentRecallSourceResolutionAndTargetRouting();
|
||||
await testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor();
|
||||
await testGenerationRecallFinalInjectionBackfillsPersistedRecord();
|
||||
await testGenerationRecallImmediateAfterCommandsBackfillsPersistedRecord();
|
||||
await testRecallCardMountsOnStandardUserMessageDom();
|
||||
await testRecallCardSkipsMountWithoutStableMessageIndex();
|
||||
await testRecallCardDelayedDomInsertionEventuallyRenders();
|
||||
|
||||
Reference in New Issue
Block a user