mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
fix(persistence): repair legacy pending state safely
This commit is contained in:
208
sync/legacy-persistence-repair.js
Normal file
208
sync/legacy-persistence-repair.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// ST-BME: legacy persistence repair policy
|
||||
// Pure helpers only: no IO, no runtime mutation.
|
||||
|
||||
const ACCEPTED_GRAPH_TIERS = new Set([
|
||||
"authority-sql",
|
||||
"opfs",
|
||||
"indexeddb",
|
||||
"chat-state",
|
||||
"luker-chat-state",
|
||||
]);
|
||||
|
||||
const RECOVERY_ONLY_GRAPH_TIERS = new Set([
|
||||
"shadow",
|
||||
"metadata-full",
|
||||
"authority-blob",
|
||||
"authority-blob-checkpoint",
|
||||
"runtime-recovery",
|
||||
]);
|
||||
|
||||
const REPLICA_ONLY_GRAPH_TIERS = new Set([
|
||||
"trivium",
|
||||
"authority-trivium",
|
||||
"vector",
|
||||
]);
|
||||
|
||||
export function normalizeLegacyPersistenceTier(storageTier = "none") {
|
||||
return String(storageTier || "none").trim().toLowerCase() || "none";
|
||||
}
|
||||
|
||||
export function classifyLegacyPersistenceTier(storageTier = "none") {
|
||||
const tier = normalizeLegacyPersistenceTier(storageTier);
|
||||
if (ACCEPTED_GRAPH_TIERS.has(tier)) {
|
||||
return { tier, role: "accepted", accepted: true, recoverable: true };
|
||||
}
|
||||
if (RECOVERY_ONLY_GRAPH_TIERS.has(tier)) {
|
||||
return { tier, role: "recovery-only", accepted: false, recoverable: true };
|
||||
}
|
||||
if (REPLICA_ONLY_GRAPH_TIERS.has(tier)) {
|
||||
return { tier, role: "replica-only", accepted: false, recoverable: false };
|
||||
}
|
||||
return { tier, role: "unknown", accepted: false, recoverable: false };
|
||||
}
|
||||
|
||||
export function isAcceptedLegacyPersistenceTier(storageTier = "none") {
|
||||
return classifyLegacyPersistenceTier(storageTier).accepted === true;
|
||||
}
|
||||
|
||||
export function isRecoveryOnlyLegacyPersistenceTier(storageTier = "none") {
|
||||
const classified = classifyLegacyPersistenceTier(storageTier);
|
||||
return classified.role === "recovery-only";
|
||||
}
|
||||
|
||||
function firstMeaningfulTier(...values) {
|
||||
for (const value of values) {
|
||||
const tier = normalizeLegacyPersistenceTier(value);
|
||||
if (tier && tier !== "none") return tier;
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
export function getPendingPersistenceTargetRevision({
|
||||
batchPersistence = null,
|
||||
persistenceState = null,
|
||||
} = {}) {
|
||||
const persistenceRevision = Number(batchPersistence?.revision || 0);
|
||||
const queuedRevision = Number(persistenceState?.queuedPersistRevision || 0);
|
||||
const targetRevision = Math.max(
|
||||
Number.isFinite(persistenceRevision) ? persistenceRevision : 0,
|
||||
Number.isFinite(queuedRevision) ? queuedRevision : 0,
|
||||
);
|
||||
return Number.isFinite(targetRevision) && targetRevision > 0 ? targetRevision : 0;
|
||||
}
|
||||
|
||||
export function getAcceptedCommitMarkerRevision(marker = null) {
|
||||
if (!marker || typeof marker !== "object") return 0;
|
||||
if (marker.accepted !== true) return 0;
|
||||
const revision = Number(marker.revision || marker.acceptedRevision || 0);
|
||||
return Number.isFinite(revision) && revision > 0 ? Math.floor(revision) : 0;
|
||||
}
|
||||
|
||||
export function planAcceptedPendingPersistenceRepair({
|
||||
batchPersistence = null,
|
||||
persistenceState = null,
|
||||
commitMarker = null,
|
||||
activeChatId = "",
|
||||
queuedChatId = "",
|
||||
markerChatMatchesQueued = false,
|
||||
} = {}) {
|
||||
const targetRevision = getPendingPersistenceTargetRevision({
|
||||
batchPersistence,
|
||||
persistenceState,
|
||||
});
|
||||
if (persistenceState?.pendingPersist !== true) {
|
||||
return { action: "keep", reason: "not-pending", targetRevision };
|
||||
}
|
||||
if (targetRevision <= 0) {
|
||||
return { action: "keep", reason: "missing-target-revision", targetRevision };
|
||||
}
|
||||
const normalizedActiveChatId = String(activeChatId || "").trim();
|
||||
const normalizedQueuedChatId = String(queuedChatId || "").trim();
|
||||
if (!normalizedActiveChatId || !normalizedQueuedChatId) {
|
||||
return { action: "keep", reason: "missing-chat-id", targetRevision };
|
||||
}
|
||||
if (normalizedActiveChatId !== normalizedQueuedChatId) {
|
||||
return { action: "keep", reason: "queued-chat-mismatch", targetRevision };
|
||||
}
|
||||
|
||||
const stateAcceptedRevision = Number(persistenceState?.lastAcceptedRevision || 0);
|
||||
const markerAcceptedRevision = markerChatMatchesQueued
|
||||
? getAcceptedCommitMarkerRevision(commitMarker)
|
||||
: 0;
|
||||
const acceptedRevision = Math.max(
|
||||
Number.isFinite(stateAcceptedRevision) ? stateAcceptedRevision : 0,
|
||||
Number.isFinite(markerAcceptedRevision) ? markerAcceptedRevision : 0,
|
||||
);
|
||||
if (acceptedRevision < targetRevision) {
|
||||
return {
|
||||
action: "keep",
|
||||
reason: "accepted-revision-behind",
|
||||
targetRevision,
|
||||
acceptedRevision,
|
||||
};
|
||||
}
|
||||
|
||||
const tier = firstMeaningfulTier(
|
||||
persistenceState?.acceptedStorageTier,
|
||||
commitMarker?.storageTier,
|
||||
batchPersistence?.storageTier,
|
||||
);
|
||||
if (!isAcceptedLegacyPersistenceTier(tier)) {
|
||||
return {
|
||||
action: "keep",
|
||||
reason: "accepted-tier-not-canonical",
|
||||
targetRevision,
|
||||
acceptedRevision,
|
||||
tier,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
action: "clear-stale-pending",
|
||||
reason: "accepted-revision-covers-pending",
|
||||
targetRevision,
|
||||
acceptedRevision,
|
||||
tier,
|
||||
};
|
||||
}
|
||||
|
||||
export function repairLegacyLastBatchPersistenceStatus({
|
||||
batchStatus = null,
|
||||
persistenceState = null,
|
||||
commitMarker = null,
|
||||
activeChatId = "",
|
||||
commitMarkerChatMatchesActive = false,
|
||||
} = {}) {
|
||||
const persistence = batchStatus?.persistence;
|
||||
if (!batchStatus || !persistence || typeof persistence !== "object") {
|
||||
return { repaired: false, batchStatus };
|
||||
}
|
||||
const targetRevision = getPendingPersistenceTargetRevision({
|
||||
batchPersistence: persistence,
|
||||
persistenceState,
|
||||
});
|
||||
const normalizedActiveChatId = String(activeChatId || "").trim();
|
||||
const stateChatId = String(persistenceState?.chatId || "").trim();
|
||||
const stateMatchesActive =
|
||||
!normalizedActiveChatId || !stateChatId || normalizedActiveChatId === stateChatId;
|
||||
const acceptedRevision = Math.max(
|
||||
stateMatchesActive ? Number(persistenceState?.lastAcceptedRevision || 0) : 0,
|
||||
commitMarkerChatMatchesActive ? getAcceptedCommitMarkerRevision(commitMarker) : 0,
|
||||
);
|
||||
const tier = firstMeaningfulTier(
|
||||
stateMatchesActive ? persistenceState?.acceptedStorageTier : "",
|
||||
commitMarkerChatMatchesActive ? commitMarker?.storageTier : "",
|
||||
persistence.storageTier,
|
||||
);
|
||||
if (
|
||||
targetRevision <= 0 ||
|
||||
acceptedRevision < targetRevision ||
|
||||
!isAcceptedLegacyPersistenceTier(tier)
|
||||
) {
|
||||
return { repaired: false, batchStatus };
|
||||
}
|
||||
return {
|
||||
repaired: true,
|
||||
batchStatus: {
|
||||
...batchStatus,
|
||||
historyAdvanceAllowed: true,
|
||||
historyAdvanced: batchStatus.historyAdvanced === true,
|
||||
persistence: {
|
||||
...persistence,
|
||||
outcome: "accepted",
|
||||
accepted: true,
|
||||
saved: true,
|
||||
queued: false,
|
||||
blocked: false,
|
||||
recoverable: false,
|
||||
attempted: true,
|
||||
storageTier: tier,
|
||||
reason: "legacy-accepted-revision-repair",
|
||||
revision: targetRevision,
|
||||
},
|
||||
},
|
||||
targetRevision,
|
||||
acceptedRevision,
|
||||
tier,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user