fix(persistence): repair legacy pending state safely

This commit is contained in:
youzini
2026-05-19 07:36:35 +00:00
parent 68ea62f52f
commit 0f0da1fbe8
5 changed files with 626 additions and 38 deletions

View 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,
};
}