fix(recall): drive reroll reuse from host generation context

This commit is contained in:
youzini
2026-05-31 20:36:14 +00:00
parent 86ecdbeeae
commit f25ad9eaa1
11 changed files with 196 additions and 150 deletions

View File

@@ -99,6 +99,7 @@ function clonePlain(value, fallback = null) {
export function createGenerationContextTracker(deps = {}) {
let current = null;
let pendingSwipe = null;
let recentAssistantTailDelete = null;
let sequence = 0;
const now = () =>
@@ -123,11 +124,24 @@ export function createGenerationContextTracker(deps = {}) {
function begin(type = "normal", params = {}, { dryRun = false, phase = "" } = {}) {
if (dryRun) return null;
const at = now();
const generationType = normalizeGenerationType(type);
const kind = classifyGenerationKind(generationType, params);
const rawType = normalizeGenerationType(type);
const activeChatId = getChatId();
const freshInput = Boolean(params?.__stBmeFreshInputHint);
const canInferRerollFromDelete = Boolean(
rawType === "normal" &&
!freshInput &&
recentAssistantTailDelete &&
recentAssistantTailDelete.chatId === activeChatId &&
at - Number(recentAssistantTailDelete.at || 0) <= ttlMs(),
);
const generationType = canInferRerollFromDelete ? "regenerate" : rawType;
const kind = canInferRerollFromDelete
? "no-new-user"
: classifyGenerationKind(generationType, params);
const context = {
id: `${at}:${++sequence}`,
type: generationType,
rawType,
kind,
chatId: getChatId(),
params: clonePlain(params, {}),
@@ -142,8 +156,12 @@ export function createGenerationContextTracker(deps = {}) {
swipeMeta: generationType === "swipe" ? clonePlain(pendingSwipe?.meta, null) : null,
expectedMutation: "",
expectedMutationAt: 0,
inferredFrom: canInferRerollFromDelete
? "assistant-tail-delete-without-fresh-input"
: "",
};
pendingSwipe = null;
recentAssistantTailDelete = null;
current = context;
return { ...context };
}
@@ -151,7 +169,26 @@ export function createGenerationContextTracker(deps = {}) {
function update(type = "normal", params = {}, { dryRun = false, phase = "" } = {}) {
if (dryRun) return null;
const at = now();
const generationType = normalizeGenerationType(type);
const rawType = normalizeGenerationType(type);
if (
current?.inferredFrom &&
current.rawType === rawType &&
current.chatId === getChatId()
) {
current = {
...current,
rawType,
params: clonePlain(params, current.params || {}),
updatedAt: at,
afterCommandsAt:
String(phase || "") === "GENERATION_AFTER_COMMANDS"
? at
: current.afterCommandsAt || 0,
phase: String(phase || current.phase || ""),
};
return { ...current };
}
const generationType = rawType;
const kind = classifyGenerationKind(generationType, params);
if (!current || current.type !== generationType || current.chatId !== getChatId()) {
return begin(generationType, params, { dryRun, phase });
@@ -202,15 +239,26 @@ export function createGenerationContextTracker(deps = {}) {
const previous = current;
current = null;
pendingSwipe = null;
recentAssistantTailDelete = null;
return previous ? { ...previous, clearReason: String(reason || "") } : null;
}
function noteAssistantTailDelete(payload = {}) {
recentAssistantTailDelete = {
chatId: getChatId(),
at: now(),
...clonePlain(payload, {}),
};
return { ...recentAssistantTailDelete };
}
return {
begin,
update,
get,
clear,
noteSwipe,
noteAssistantTailDelete,
markExpectedMutation,
};
}

View File

@@ -1,124 +0,0 @@
// ST-BME reroll transaction boundary helpers.
//
// Pure helpers only. They keep the one-shot reroll recall reuse marker small,
// expiring, chat-bound, and tied to an unchanged parent user floor.
function normalizeText(value = "") {
return String(value ?? "").replace(/\r\n/g, "\n").trim();
}
function normalizeChatId(value = "") {
return String(value ?? "").trim();
}
function normalizeIndex(value = null) {
return Number.isFinite(Number(value)) ? Math.floor(Number(value)) : null;
}
export function createRerollRecallReuseMarker({
chatId = "",
fromFloor = null,
targetUserMessageIndex = null,
userText = "",
persistedRecord = null,
hashRecallInput = null,
now = Date.now(),
meta = null,
} = {}) {
const normalizedUserText = normalizeText(userText);
if (!normalizedUserText) return { marker: null, reason: "missing-user-text" };
const persistedInjection = normalizeText(persistedRecord?.injectionText || "");
if (!persistedRecord || !persistedInjection) {
return { marker: null, reason: "missing-persisted-recall" };
}
const boundText = normalizeText(
persistedRecord?.boundUserFloorText || persistedRecord?.recallInput || "",
);
if (boundText && boundText !== normalizedUserText) {
return { marker: null, reason: "bound-user-floor-mismatch" };
}
const hash =
typeof hashRecallInput === "function"
? hashRecallInput(normalizedUserText)
: normalizedUserText;
return {
marker: {
chatId: normalizeChatId(chatId),
fromFloor: normalizeIndex(fromFloor),
targetUserMessageIndex: normalizeIndex(targetUserMessageIndex),
userText: normalizedUserText,
userHash: String(hash || ""),
createdAt: Number(now || 0),
meta,
},
reason: "prepared",
};
}
export function consumeRerollRecallReuseMarker({
marker = null,
activeChatId = "",
latestUserMessageIndex = null,
currentUserText = "",
hashRecallInput = null,
now = Date.now(),
ttlMs = 0,
} = {}) {
if (!marker || typeof marker !== "object") {
return { consumed: false, marker: null, reason: "missing-marker", override: null };
}
const markerChatId = normalizeChatId(marker.chatId);
const normalizedActiveChatId = normalizeChatId(activeChatId);
if (markerChatId && normalizedActiveChatId && markerChatId !== normalizedActiveChatId) {
return { consumed: false, marker: null, reason: "chat-mismatch", override: null };
}
if (ttlMs > 0 && Number(now || 0) - Number(marker.createdAt || 0) > ttlMs) {
return { consumed: false, marker: null, reason: "expired", override: null };
}
const targetUserMessageIndex = normalizeIndex(latestUserMessageIndex);
const markerTargetIndex = normalizeIndex(marker.targetUserMessageIndex);
if (targetUserMessageIndex !== markerTargetIndex) {
return { consumed: false, marker: null, reason: "target-user-floor-changed", override: null };
}
const normalizedUserText = normalizeText(currentUserText);
const currentHash =
typeof hashRecallInput === "function"
? hashRecallInput(normalizedUserText)
: normalizedUserText;
if (!normalizedUserText || String(currentHash || "") !== String(marker.userHash || "")) {
return { consumed: false, marker: null, reason: "user-text-changed", override: null };
}
return {
consumed: true,
marker: null,
reason: "consumed",
override: {
overrideUserMessage: normalizedUserText,
generationType: "normal",
targetUserMessageIndex: markerTargetIndex,
overrideSource: "chat-last-user",
overrideSourceLabel: "历史最后用户楼层",
overrideReason: "reroll-user-floor-reuse",
sourceCandidates: [
{
text: normalizedUserText,
source: "chat-last-user",
sourceLabel: "历史最后用户楼层",
reason: "reroll-user-floor-reuse",
includeSyntheticUserMessage: false,
},
],
includeSyntheticUserMessage: false,
rerollRecallReuse: true,
},
};
}