From fd63202a2006249bffe31ed823caee242043db7a Mon Sep 17 00:00:00 2001 From: youzini Date: Sat, 30 May 2026 14:02:05 +0000 Subject: [PATCH] refactor(rebirth): isolate vector and reroll gates --- index.js | 143 +++++++++----------- package.json | 2 + runtime/reroll-transaction-boundary.js | 124 +++++++++++++++++ tests/helpers/generation-recall-harness.mjs | 6 + tests/reroll-transaction-boundary.mjs | 81 +++++++++++ tests/vector-gate.mjs | 60 ++++++++ vector/vector-gate.js | 28 ++++ 7 files changed, 368 insertions(+), 76 deletions(-) create mode 100644 runtime/reroll-transaction-boundary.js create mode 100644 tests/reroll-transaction-boundary.mjs create mode 100644 tests/vector-gate.mjs create mode 100644 vector/vector-gate.js diff --git a/index.js b/index.js index 74a03d7..7e4e0dc 100644 --- a/index.js +++ b/index.js @@ -148,6 +148,10 @@ import { resolvePersistenceChatIdCore, resolveRuntimeGraphFallbackIdentityCore, } from "./runtime/identity-resolver.js"; +import { + consumeRerollRecallReuseMarker, + createRerollRecallReuseMarker, +} from "./runtime/reroll-transaction-boundary.js"; import { extractMemories, generateReflection, @@ -405,6 +409,7 @@ import { testVectorConnection, validateVectorConfig, } from "./vector/vector-index.js"; +import { planVectorReadyCheck } from "./vector/vector-gate.js"; import { createAuthorityTriviumClient } from "./vector/authority-vector-primary-adapter.js"; import { buildAuthorityJobIdempotencyKey, @@ -17386,33 +17391,56 @@ async function ensureVectorReadyIfNeeded( signal = undefined, ) { if (!currentGraph) return; - if ( - !isGraphMetadataWriteAllowed() && - !hasRuntimeGraphMutationContext(getContext(), currentGraph, { - allowNoChatState: true, - }) - ) { + let metadataWriteAllowed = isGraphMetadataWriteAllowed(); + let mutationContextAllowed = hasRuntimeGraphMutationContext(getContext(), currentGraph, { + allowNoChatState: true, + }); + let gate = planVectorReadyCheck({ + hasGraph: Boolean(currentGraph), + metadataWriteAllowed, + mutationContextAllowed, + repairAttempted: false, + dirty: currentGraph?.vectorIndexState?.dirty === true, + configValid: true, + }); + if (gate.action === "skip" || gate.action === "block") return; + + if (gate.action === "repair-identity") { repairRuntimeGraphIdentityFromPersistence("向量准备", { reason: "vector-ready-fallback", }); - } - if ( - !isGraphMetadataWriteAllowed() && - !hasRuntimeGraphMutationContext(getContext(), currentGraph, { + metadataWriteAllowed = isGraphMetadataWriteAllowed(); + mutationContextAllowed = hasRuntimeGraphMutationContext(getContext(), currentGraph, { allowNoChatState: true, - }) - ) { - return; + }); + gate = planVectorReadyCheck({ + hasGraph: Boolean(currentGraph), + metadataWriteAllowed, + mutationContextAllowed, + repairAttempted: true, + dirty: currentGraph?.vectorIndexState?.dirty === true, + configValid: true, + }); + if (gate.action === "skip" || gate.action === "block") return; } + ensureCurrentGraphRuntimeState({ chatId: getGraphOwnedChatId(currentGraph) || getCurrentChatId(), }); - if (!currentGraph.vectorIndexState?.dirty) return; - const config = getEmbeddingConfig(); const validation = validateVectorConfig(config); - if (!validation.valid) return; + // Permission/identity gate has already passed above; this final plan only + // decides whether dirty state + config validity should trigger sync. + gate = planVectorReadyCheck({ + hasGraph: Boolean(currentGraph), + metadataWriteAllowed: true, + mutationContextAllowed: true, + repairAttempted: true, + dirty: currentGraph?.vectorIndexState?.dirty === true, + configValid: validation.valid, + }); + if (gate.action !== "sync") return; const result = await syncVectorState({ force: true, @@ -19645,32 +19673,23 @@ function prepareRerollRecallReuse({ fromFloor = null, meta = null } = {}) { 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 = { + const prepared = createRerollRecallReuseMarker({ chatId, - fromFloor: Number.isFinite(Number(fromFloor)) ? Math.floor(Number(fromFloor)) : null, + fromFloor, targetUserMessageIndex, userText, - userHash: hashRecallInput(userText), - createdAt: Date.now(), + persistedRecord, + hashRecallInput, + now: Date.now(), meta, - }; + }); + if (!prepared.marker) { + pendingRerollRecallReuse = null; + return null; + } + + pendingRerollRecallReuse = prepared.marker; return pendingRerollRecallReuse; } @@ -19679,50 +19698,22 @@ function consumePendingRerollRecallReuse(chat = getContext()?.chat) { 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, - }; + const consumed = consumeRerollRecallReuseMarker({ + marker: reuse, + activeChatId, + latestUserMessageIndex: targetUserMessageIndex, + currentUserText: userText, + hashRecallInput, + now: Date.now(), + ttlMs: GENERATION_RECALL_TRANSACTION_TTL_MS, + }); + pendingRerollRecallReuse = consumed.marker; + return consumed.consumed ? consumed.override : null; } function buildRecallRecentMessages(chat, limit, syntheticUserMessage = "") { diff --git a/package.json b/package.json index f8157b4..0f5f63b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "test:identity-resolver": "node tests/identity-resolver.mjs", "test:persistence-reducer": "node tests/persistence-reducer.mjs", "test:graph-head": "node tests/graph-head.mjs", + "test:reroll-transaction-boundary": "node tests/reroll-transaction-boundary.mjs", + "test:vector-gate": "node tests/vector-gate.mjs", "test:hide-engine": "node tests/hide-engine.mjs", "test:maintenance-journal": "node tests/maintenance-journal.mjs", "test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs", diff --git a/runtime/reroll-transaction-boundary.js b/runtime/reroll-transaction-boundary.js new file mode 100644 index 0000000..b9e53bd --- /dev/null +++ b/runtime/reroll-transaction-boundary.js @@ -0,0 +1,124 @@ +// 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, + }, + }; +} diff --git a/tests/helpers/generation-recall-harness.mjs b/tests/helpers/generation-recall-harness.mjs index a37fc71..0be7228 100644 --- a/tests/helpers/generation-recall-harness.mjs +++ b/tests/helpers/generation-recall-harness.mjs @@ -58,6 +58,10 @@ import { normalizeAuthorityBrowserState, recordAuthorityAcceptedRevision, } from "../../sync/authority-browser-state.js"; +import { + consumeRerollRecallReuseMarker, + createRerollRecallReuseMarker, +} from "../../runtime/reroll-transaction-boundary.js"; const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const indexPath = path.resolve(moduleDir, "../../index.js"); @@ -109,6 +113,8 @@ export function createGenerationRecallHarness(options = {}) { getAuthorityBrowserStateSnapshot, normalizeAuthorityBrowserState, recordAuthorityAcceptedRevision, + consumeRerollRecallReuseMarker, + createRerollRecallReuseMarker, settings: {}, graphPersistenceState: createGraphPersistenceState(), extension_settings: { [MODULE_NAME]: {} }, diff --git a/tests/reroll-transaction-boundary.mjs b/tests/reroll-transaction-boundary.mjs new file mode 100644 index 0000000..def4601 --- /dev/null +++ b/tests/reroll-transaction-boundary.mjs @@ -0,0 +1,81 @@ +// ST-BME restrained rebirth — Phase 4 reroll transaction boundary tests. + +import assert from "node:assert/strict"; +import { + consumeRerollRecallReuseMarker, + createRerollRecallReuseMarker, +} from "../runtime/reroll-transaction-boundary.js"; + +const hashRecallInput = (text) => `h:${String(text || "").trim()}`; + +const prepared = createRerollRecallReuseMarker({ + chatId: "chat-a", + fromFloor: 4, + targetUserMessageIndex: 2, + userText: " hello\n", + persistedRecord: { + injectionText: "memory", + boundUserFloorText: "hello", + }, + hashRecallInput, + now: 1000, +}); +assert.equal(prepared.reason, "prepared"); +assert.equal(prepared.marker.chatId, "chat-a"); +assert.equal(prepared.marker.fromFloor, 4); +assert.equal(prepared.marker.targetUserMessageIndex, 2); +assert.equal(prepared.marker.userHash, "h:hello"); + +const consumed = consumeRerollRecallReuseMarker({ + marker: prepared.marker, + activeChatId: "chat-a", + latestUserMessageIndex: 2, + currentUserText: "hello", + hashRecallInput, + now: 1500, + ttlMs: 5000, +}); +assert.equal(consumed.consumed, true); +assert.equal(consumed.override.rerollRecallReuse, true); +assert.equal(consumed.override.targetUserMessageIndex, 2); + +console.log(" ✓ reroll recall reuse marker is one-shot and floor-bound"); + +assert.equal( + createRerollRecallReuseMarker({ + userText: "changed", + persistedRecord: { injectionText: "memory", boundUserFloorText: "original" }, + }).reason, + "bound-user-floor-mismatch", +); +assert.equal( + createRerollRecallReuseMarker({ + userText: "hello", + persistedRecord: { injectionText: "" }, + }).reason, + "missing-persisted-recall", +); + +for (const [caseName, options, reason] of [ + ["chat", { activeChatId: "other-chat" }, "chat-mismatch"], + ["ttl", { now: 7001, ttlMs: 5000 }, "expired"], + ["floor", { latestUserMessageIndex: 3 }, "target-user-floor-changed"], + ["text", { currentUserText: "changed" }, "user-text-changed"], +]) { + const result = consumeRerollRecallReuseMarker({ + marker: prepared.marker, + activeChatId: "chat-a", + latestUserMessageIndex: 2, + currentUserText: "hello", + hashRecallInput, + now: 1500, + ttlMs: 5000, + ...options, + }); + assert.equal(result.consumed, false, `${caseName} mismatch must reject reuse`); + assert.equal(result.reason, reason); + assert.equal(result.marker, null, `${caseName} mismatch must clear marker`); +} + +console.log(" ✓ reroll marker rejects stale, cross-chat, changed-floor, changed-text reuse"); +console.log("reroll-transaction-boundary tests passed"); diff --git a/tests/vector-gate.mjs b/tests/vector-gate.mjs new file mode 100644 index 0000000..6568f0e --- /dev/null +++ b/tests/vector-gate.mjs @@ -0,0 +1,60 @@ +// ST-BME restrained rebirth — Phase 4 vector readiness boundary tests. + +import assert from "node:assert/strict"; +import { planVectorReadyCheck } from "../vector/vector-gate.js"; + +assert.deepEqual(planVectorReadyCheck({ hasGraph: false }), { + action: "skip", + reason: "missing-graph", +}); + +assert.deepEqual( + planVectorReadyCheck({ + hasGraph: true, + metadataWriteAllowed: false, + mutationContextAllowed: false, + repairAttempted: false, + }), + { action: "repair-identity", reason: "missing-mutation-context" }, +); + +assert.deepEqual( + planVectorReadyCheck({ + hasGraph: true, + metadataWriteAllowed: false, + mutationContextAllowed: false, + repairAttempted: true, + }), + { action: "block", reason: "missing-mutation-context" }, +); + +assert.deepEqual( + planVectorReadyCheck({ + hasGraph: true, + metadataWriteAllowed: true, + dirty: false, + }), + { action: "skip", reason: "vector-clean" }, +); + +assert.deepEqual( + planVectorReadyCheck({ + hasGraph: true, + metadataWriteAllowed: true, + dirty: true, + configValid: false, + }), + { action: "skip", reason: "invalid-vector-config" }, +); + +assert.deepEqual( + planVectorReadyCheck({ + hasGraph: true, + metadataWriteAllowed: true, + dirty: true, + configValid: true, + }), + { action: "sync", reason: "vector-dirty" }, +); + +console.log("vector-gate tests passed"); diff --git a/vector/vector-gate.js b/vector/vector-gate.js new file mode 100644 index 0000000..79e8280 --- /dev/null +++ b/vector/vector-gate.js @@ -0,0 +1,28 @@ +// ST-BME vector readiness boundary helpers. +// +// Pure planning helpers only. They decide whether vector preparation should +// attempt identity repair, skip, block, or run sync; vector indexing/search +// algorithms stay in vector-index.js. + +export function planVectorReadyCheck({ + hasGraph = false, + metadataWriteAllowed = false, + mutationContextAllowed = false, + repairAttempted = false, + dirty = false, + configValid = false, +} = {}) { + if (!hasGraph) return { action: "skip", reason: "missing-graph" }; + + if (!metadataWriteAllowed && !mutationContextAllowed) { + if (!repairAttempted) { + return { action: "repair-identity", reason: "missing-mutation-context" }; + } + return { action: "block", reason: "missing-mutation-context" }; + } + + if (!dirty) return { action: "skip", reason: "vector-clean" }; + if (!configValid) return { action: "skip", reason: "invalid-vector-config" }; + + return { action: "sync", reason: "vector-dirty" }; +}