mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
Merge branch 'dev'
This commit is contained in:
2
index.js
2
index.js
@@ -1721,6 +1721,8 @@ let lastPreGenerationRecallAt = 0;
|
||||
const generationRecallTransactionRuntime = createGenerationRecallTransactions({
|
||||
getContext,
|
||||
getCurrentChatId,
|
||||
getActiveGenerationId: () =>
|
||||
generationContextTracker.get?.({ allowStale: true })?.id || "",
|
||||
getRecallUserMessageSourceLabel: (...args) =>
|
||||
getRecallUserMessageSourceLabel(...args),
|
||||
getSettings,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) >
|
||||
|
||||
258
tests/generation-recall-transaction-isolation.mjs
Normal file
258
tests/generation-recall-transaction-isolation.mjs
Normal 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");
|
||||
Reference in New Issue
Block a user