Merge branch 'dev'

This commit is contained in:
youzini
2026-05-31 21:30:18 +00:00
4 changed files with 275 additions and 0 deletions

View File

@@ -1721,6 +1721,8 @@ let lastPreGenerationRecallAt = 0;
const generationRecallTransactionRuntime = createGenerationRecallTransactions({
getContext,
getCurrentChatId,
getActiveGenerationId: () =>
generationContextTracker.get?.({ allowStale: true })?.id || "",
getRecallUserMessageSourceLabel: (...args) =>
getRecallUserMessageSourceLabel(...args),
getSettings,

View File

@@ -6,6 +6,7 @@
"test:triviumdb-poc": "node tests/triviumdb-poc.mjs",
"test:runtime-history": "node tests/runtime-history.mjs",
"test:generation-context": "node tests/generation-context.mjs",
"test:generation-recall-transactions": "node tests/generation-recall-transaction-isolation.mjs",
"test:graph-persistence": "node tests/graph-persistence.mjs",
"test:index-slicing-ratchet": "node tests/index-slicing-ratchet.mjs",
"test:runtime-deps": "node tests/runtime-deps-completeness.mjs",

View File

@@ -7,6 +7,8 @@ export function createGenerationRecallTransactions(deps = {}) {
deps.normalizeRecallInputText?.(value) ?? String(value || "").trim();
const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args);
const getContext = (...args) => deps.getContext?.(...args);
const getActiveGenerationId = () =>
String(deps.getActiveGenerationId?.() || "").trim();
const getGenerationRecallTransactionTtlMs = () =>
Number.isFinite(Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS))
? Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS)
@@ -317,6 +319,7 @@ export function createGenerationRecallTransactions(deps = {}) {
chatId: normalizedChatId,
generationType: normalizedGenerationType,
recallKey: normalizedRecallKey,
generationId: getActiveGenerationId(),
hookStates: {},
createdAt: now,
frozenRecallOptions: null,
@@ -333,12 +336,23 @@ export function createGenerationRecallTransactions(deps = {}) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return null;
// 跨代际隔离:当宿主提供了当前生成代际 id 时,只桥接“同一次生成”的事务。
// 这阻止上一轮 normal 生成遗留的事务被本轮 reroll 复用,从而保证
// reroll 真正进入 runRecall → 持久召回复用门禁,而不是继承旧的 fresh 结果。
const activeGenerationId = getActiveGenerationId();
let latestTransaction = null;
for (const transaction of generationRecallTransactions.values()) {
if (!transaction || String(transaction.chatId || "") !== normalizedChatId)
continue;
if (!isGenerationRecallTransactionWithinBridgeWindow(transaction, now))
continue;
if (activeGenerationId) {
const transactionGenerationId = String(transaction.generationId || "").trim();
if (transactionGenerationId && transactionGenerationId !== activeGenerationId) {
continue;
}
}
if (
!latestTransaction ||
Number(transaction.updatedAt || 0) >

View File

@@ -0,0 +1,258 @@
import assert from "node:assert/strict";
import { createGenerationRecallTransactions } from "../runtime/generation-recall-transactions.js";
import {
hashRecallInput,
shouldRunRecallForTransaction,
} from "../ui/ui-status.js";
const CHAT_ID = "chat-generation-transaction-isolation";
const GENERATION_AFTER_COMMANDS = "GENERATION_AFTER_COMMANDS";
const GENERATE_BEFORE_COMBINE_PROMPTS = "GENERATE_BEFORE_COMBINE_PROMPTS";
function createTransactionHarness({ activeGenerationId = "gen-A" } = {}) {
let currentActiveGenerationId = activeGenerationId;
const chat = [
{ is_user: true, mes: "first stable user floor" },
{ is_user: false, mes: "first assistant reply", is_system: false },
{ is_user: true, mes: "second fresh user floor" },
{ is_user: false, mes: "second assistant reply", is_system: false },
];
const runtime = createGenerationRecallTransactions({
getContext: () => ({ chatId: CHAT_ID, chat }),
getCurrentChatId: () => CHAT_ID,
getActiveGenerationId: () => currentActiveGenerationId,
getRecallUserMessageSourceLabel: (source = "") => String(source || ""),
getSettings: () => ({ recallUseAuthoritativeGenerationInput: false }),
hashRecallInput,
normalizeChatIdCandidate: (value = "") => String(value ?? "").trim(),
normalizeRecallInputText: (value = "") => String(value ?? "").trim(),
peekPlannerRecallHandoff: () => null,
resolveGenerationTargetUserMessageIndex: (candidateChat = [], options = {}) => {
const normalizedType = String(options?.generationType || "normal").trim() || "normal";
for (let index = candidateChat.length - 1; index >= 0; index--) {
if (candidateChat[index]?.is_user) return index;
}
return normalizedType === "normal" ? null : null;
},
shouldRunRecallForTransaction,
GENERATION_RECALL_TRANSACTION_TTL_MS: 15000,
GENERATION_RECALL_HOOK_BRIDGE_MS: 1200,
});
return {
chat,
runtime,
setActiveGenerationId(value = "") {
currentActiveGenerationId = String(value || "").trim();
},
};
}
function createNormalAfterCommandsContext(runtime) {
return runtime.createGenerationRecallContext({
hookName: GENERATION_AFTER_COMMANDS,
generationType: "normal",
recallOptions: {
generationType: "normal",
targetUserMessageIndex: 2,
overrideUserMessage: "second fresh user floor",
overrideSource: "chat-tail-user",
overrideSourceLabel: "chat-tail-user",
overrideReason: "test-normal-generation",
},
});
}
function createRegenerateAfterCommandsContext(runtime) {
return runtime.createGenerationRecallContext({
hookName: GENERATION_AFTER_COMMANDS,
generationType: "regenerate",
recallOptions: {
generationType: "regenerate",
targetUserMessageIndex: 0,
overrideUserMessage: "first stable user floor",
overrideSource: "chat-last-user",
overrideSourceLabel: "chat-last-user",
overrideReason: "test-regenerate-generation",
},
});
}
function createPeerBeforeCombineContext(runtime, recallOptions = {}) {
return runtime.createGenerationRecallContext({
hookName: GENERATE_BEFORE_COMBINE_PROMPTS,
generationType: recallOptions.generationType || "normal",
recallOptions: {
generationType: "normal",
targetUserMessageIndex: 2,
overrideUserMessage: "second fresh user floor",
overrideSource: "chat-tail-user",
overrideSourceLabel: "chat-tail-user",
overrideReason: "test-peer-generation",
...recallOptions,
},
});
}
{
const { runtime, setActiveGenerationId } = createTransactionHarness({
activeGenerationId: "gen-A",
});
const normalContext = createNormalAfterCommandsContext(runtime);
assert.ok(normalContext.transaction, "normal generation should create a transaction");
assert.equal(normalContext.shouldRun, true, "normal after-commands should run initially");
assert.equal(
normalContext.transaction.generationId,
"gen-A",
"normal transaction should be stamped with generation A",
);
runtime.markGenerationRecallTransactionHookState(
normalContext.transaction,
GENERATION_AFTER_COMMANDS,
"completed",
);
runtime.markGenerationRecallTransactionHookState(
normalContext.transaction,
GENERATE_BEFORE_COMBINE_PROMPTS,
"completed",
);
runtime.storeGenerationRecallTransactionResult(
normalContext.transaction,
{
status: "completed",
didRecall: true,
injectionText: "fresh generation A recall result",
hookName: GENERATION_AFTER_COMMANDS,
},
{ hookName: GENERATION_AFTER_COMMANDS, deliveryMode: "immediate" },
);
setActiveGenerationId("gen-B");
const regenerateContext = createRegenerateAfterCommandsContext(runtime);
assert.ok(regenerateContext.transaction, "regenerate should create a transaction");
assert.notEqual(
regenerateContext.transaction.id,
normalContext.transaction.id,
"regenerate must not reuse the previous normal generation transaction",
);
assert.equal(
regenerateContext.transaction.generationId,
"gen-B",
"regenerate transaction should be stamped with generation B",
);
assert.equal(
regenerateContext.generationType,
"regenerate",
"regenerate context should keep the requested generation type",
);
assert.equal(
regenerateContext.transaction.generationType,
"history",
"the transaction bucket for regenerate is normalized to history",
);
assert.equal(
regenerateContext.recallOptions.targetUserMessageIndex,
0,
"regenerate recall options should bind to the requested target user floor",
);
assert.equal(
regenerateContext.recallOptions.overrideUserMessage,
"first stable user floor",
"regenerate must not inherit the normal transaction's frozen user message",
);
assert.equal(
runtime.getGenerationRecallTransactionResult(regenerateContext.transaction),
null,
"regenerate must not inherit the normal transaction's stored fresh result",
);
assert.equal(
regenerateContext.shouldRun,
true,
"regenerate should run recall instead of being short-circuited by old peer hook states",
);
console.log(" ✓ cross-generation regenerate creates an isolated recall transaction");
}
{
const { runtime } = createTransactionHarness({ activeGenerationId: "gen-A" });
const afterCommandsContext = createNormalAfterCommandsContext(runtime);
assert.ok(afterCommandsContext.transaction, "after-commands should create a transaction");
assert.equal(afterCommandsContext.transaction.generationId, "gen-A");
runtime.markGenerationRecallTransactionHookState(
afterCommandsContext.transaction,
GENERATION_AFTER_COMMANDS,
"running",
);
runtime.markGenerationRecallTransactionHookState(
afterCommandsContext.transaction,
GENERATION_AFTER_COMMANDS,
"completed",
);
const beforeCombineContext = createPeerBeforeCombineContext(runtime);
assert.ok(beforeCombineContext.transaction, "before-combine should return a transaction");
assert.equal(
beforeCombineContext.transaction.id,
afterCommandsContext.transaction.id,
"same-generation peer hook should reuse the after-commands transaction",
);
assert.equal(
beforeCombineContext.transaction.generationId,
"gen-A",
"same-generation peer bridge should preserve the generation id",
);
console.log(" ✓ same-generation peer hook bridge still reuses the transaction");
}
{
const { runtime } = createTransactionHarness({ activeGenerationId: "" });
const legacyContext = createNormalAfterCommandsContext(runtime);
assert.ok(legacyContext.transaction, "legacy no-generation-id path should create a transaction");
assert.equal(
legacyContext.transaction.generationId,
"",
"legacy transaction should carry an empty generation id",
);
runtime.markGenerationRecallTransactionHookState(
legacyContext.transaction,
GENERATION_AFTER_COMMANDS,
"completed",
);
const recentTransaction = runtime.findRecentGenerationRecallTransactionForChat(CHAT_ID);
assert.equal(
recentTransaction?.id,
legacyContext.transaction.id,
"empty active generation id should still find the recent same-chat transaction",
);
const legacyPeerContext = createPeerBeforeCombineContext(runtime, {
generationType: "regenerate",
targetUserMessageIndex: 0,
overrideUserMessage: "first stable user floor",
overrideSource: "chat-last-user",
overrideSourceLabel: "chat-last-user",
overrideReason: "test-legacy-regenerate-peer",
});
assert.equal(
legacyPeerContext.transaction?.id,
legacyContext.transaction.id,
"empty generation id should preserve legacy same-chat peer bridging behavior",
);
console.log(" ✓ empty generation id preserves legacy same-chat bridging");
}
console.log("generation-recall-transaction-isolation tests passed");