mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: reconcile stale manualBackupBatchJournalCoverage when contiguous earlier journal coverage is rebuilt
- Add hasContiguousJournalCoverageThroughFloor to check if current batchJournal contiguously covers back through the old retained floor - Add reconcileManualBackupBatchJournalCoverage that clears stale coverage when actual journals have bridged the gap - Self-clean during normalizeGraphRuntimeState (load-time) and appendBatchJournal (write-time) - findJournalRecoveryPoint now uses reconciled coverage so stale manual backup floors no longer block valid reverse-journal recovery - Add regression tests for gap-not-yet-bridged and bridged scenarios
This commit is contained in:
@@ -124,10 +124,88 @@ function getEarliestJournalCoverageStartFloor(journals = []) {
|
|||||||
return earliestFloor;
|
return earliestFloor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasContiguousJournalCoverageThroughFloor(journals = [], targetFloor = null) {
|
||||||
|
const normalizedTargetFloor = Number.isFinite(Number(targetFloor))
|
||||||
|
? Math.max(0, Math.floor(Number(targetFloor)))
|
||||||
|
: null;
|
||||||
|
if (!Number.isFinite(normalizedTargetFloor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranges = (Array.isArray(journals) ? journals : [])
|
||||||
|
.map((journal) => {
|
||||||
|
const range = Array.isArray(journal?.processedRange)
|
||||||
|
? journal.processedRange
|
||||||
|
: [];
|
||||||
|
const startFloor = Number(range[0]);
|
||||||
|
const endFloor = Number(range[1]);
|
||||||
|
if (!Number.isFinite(startFloor) || !Number.isFinite(endFloor)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start: Math.max(0, Math.floor(startFloor)),
|
||||||
|
end: Math.max(0, Math.floor(endFloor)),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left, right) => left.start - right.start || left.end - right.end);
|
||||||
|
|
||||||
|
if (ranges.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let coveredUntil = null;
|
||||||
|
for (const range of ranges) {
|
||||||
|
if (coveredUntil == null) {
|
||||||
|
if (range.start > normalizedTargetFloor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
coveredUntil = range.end;
|
||||||
|
} else if (range.start > coveredUntil + 1) {
|
||||||
|
return coveredUntil >= normalizedTargetFloor;
|
||||||
|
} else {
|
||||||
|
coveredUntil = Math.max(coveredUntil, range.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coveredUntil >= normalizedTargetFloor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileManualBackupBatchJournalCoverage(coverage = null, journals = []) {
|
||||||
|
const normalizedCoverage = normalizeManualBackupBatchJournalCoverage(coverage);
|
||||||
|
if (!normalizedCoverage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manualCoverageFloor =
|
||||||
|
normalizedCoverage.truncated === true &&
|
||||||
|
Number.isFinite(normalizedCoverage.earliestRetainedFloor)
|
||||||
|
? normalizedCoverage.earliestRetainedFloor
|
||||||
|
: null;
|
||||||
|
const actualCoverageFloor = getEarliestJournalCoverageStartFloor(journals);
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedCoverage.truncated === true &&
|
||||||
|
Number.isFinite(actualCoverageFloor) &&
|
||||||
|
Number.isFinite(manualCoverageFloor) &&
|
||||||
|
actualCoverageFloor < manualCoverageFloor &&
|
||||||
|
hasContiguousJournalCoverageThroughFloor(journals, manualCoverageFloor)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedCoverage;
|
||||||
|
}
|
||||||
|
|
||||||
function getRequiredJournalCoverageStartFloor(graph, journals = []) {
|
function getRequiredJournalCoverageStartFloor(graph, journals = []) {
|
||||||
const actualCoverageFloor = getEarliestJournalCoverageStartFloor(journals);
|
const actualCoverageFloor = getEarliestJournalCoverageStartFloor(journals);
|
||||||
const manualCoverage = normalizeManualBackupBatchJournalCoverage(
|
const manualCoverage = reconcileManualBackupBatchJournalCoverage(
|
||||||
graph?.historyState?.[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
|
graph?.historyState?.[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
|
||||||
|
journals,
|
||||||
);
|
);
|
||||||
const manualCoverageFloor =
|
const manualCoverageFloor =
|
||||||
manualCoverage?.truncated === true &&
|
manualCoverage?.truncated === true &&
|
||||||
@@ -406,6 +484,11 @@ export function normalizeGraphRuntimeState(graph, chatId = "") {
|
|||||||
graph.batchJournal = Array.isArray(graph.batchJournal)
|
graph.batchJournal = Array.isArray(graph.batchJournal)
|
||||||
? graph.batchJournal.slice(-BATCH_JOURNAL_LIMIT)
|
? graph.batchJournal.slice(-BATCH_JOURNAL_LIMIT)
|
||||||
: createDefaultBatchJournal();
|
: createDefaultBatchJournal();
|
||||||
|
historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY] =
|
||||||
|
reconcileManualBackupBatchJournalCoverage(
|
||||||
|
historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
|
||||||
|
graph.batchJournal,
|
||||||
|
);
|
||||||
graph.maintenanceJournal = Array.isArray(graph.maintenanceJournal)
|
graph.maintenanceJournal = Array.isArray(graph.maintenanceJournal)
|
||||||
? graph.maintenanceJournal
|
? graph.maintenanceJournal
|
||||||
.filter((entry) => entry && typeof entry === "object")
|
.filter((entry) => entry && typeof entry === "object")
|
||||||
@@ -955,6 +1038,11 @@ export function appendBatchJournal(graph, entry) {
|
|||||||
if (graph.batchJournal.length > BATCH_JOURNAL_LIMIT) {
|
if (graph.batchJournal.length > BATCH_JOURNAL_LIMIT) {
|
||||||
graph.batchJournal = graph.batchJournal.slice(-BATCH_JOURNAL_LIMIT);
|
graph.batchJournal = graph.batchJournal.slice(-BATCH_JOURNAL_LIMIT);
|
||||||
}
|
}
|
||||||
|
graph.historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY] =
|
||||||
|
reconcileManualBackupBatchJournalCoverage(
|
||||||
|
graph.historyState?.[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
|
||||||
|
graph.batchJournal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMaintenanceJournalEntry(
|
export function createMaintenanceJournalEntry(
|
||||||
|
|||||||
@@ -291,6 +291,69 @@ assert.ok(retainedCoverageRecoveryPoint);
|
|||||||
assert.equal(retainedCoverageRecoveryPoint.path, "reverse-journal");
|
assert.equal(retainedCoverageRecoveryPoint.path, "reverse-journal");
|
||||||
assert.equal(retainedCoverageRecoveryPoint.affectedJournals.length, 3);
|
assert.equal(retainedCoverageRecoveryPoint.affectedJournals.length, 3);
|
||||||
|
|
||||||
|
const bridgedCoverageGraph = createEmptyGraph();
|
||||||
|
bridgedCoverageGraph.historyState.chatId = "chat-bridged-history-test";
|
||||||
|
bridgedCoverageGraph.historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY] = {
|
||||||
|
truncated: true,
|
||||||
|
earliestRetainedFloor: 4,
|
||||||
|
retainedCount: 4,
|
||||||
|
};
|
||||||
|
bridgedCoverageGraph.batchJournal = [
|
||||||
|
{ id: "journal-4", journalVersion: 2, processedRange: [4, 4] },
|
||||||
|
{ id: "journal-5", journalVersion: 2, processedRange: [5, 5] },
|
||||||
|
];
|
||||||
|
appendBatchJournal(bridgedCoverageGraph, {
|
||||||
|
id: "journal-2",
|
||||||
|
journalVersion: 2,
|
||||||
|
processedRange: [2, 2],
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
bridgedCoverageGraph.historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
|
||||||
|
{
|
||||||
|
truncated: true,
|
||||||
|
earliestRetainedFloor: 4,
|
||||||
|
retainedCount: 4,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
appendBatchJournal(bridgedCoverageGraph, {
|
||||||
|
id: "journal-3",
|
||||||
|
journalVersion: 2,
|
||||||
|
processedRange: [3, 3],
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
bridgedCoverageGraph.historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const recoveredCoverageGraph = createEmptyGraph();
|
||||||
|
recoveredCoverageGraph.historyState.chatId = "chat-recovered-history-test";
|
||||||
|
recoveredCoverageGraph.historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY] = {
|
||||||
|
truncated: true,
|
||||||
|
earliestRetainedFloor: 4,
|
||||||
|
retainedCount: 4,
|
||||||
|
};
|
||||||
|
recoveredCoverageGraph.batchJournal = [
|
||||||
|
{ id: "journal-2", journalVersion: 2, processedRange: [2, 2] },
|
||||||
|
{ id: "journal-3", journalVersion: 2, processedRange: [3, 3] },
|
||||||
|
{ id: "journal-4", journalVersion: 2, processedRange: [4, 4] },
|
||||||
|
{ id: "journal-5", journalVersion: 2, processedRange: [5, 5] },
|
||||||
|
];
|
||||||
|
normalizeGraphRuntimeState(
|
||||||
|
recoveredCoverageGraph,
|
||||||
|
recoveredCoverageGraph.historyState.chatId,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
recoveredCoverageGraph.historyState[MANUAL_BACKUP_BATCH_JOURNAL_COVERAGE_KEY],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const recoveredCoverageRecoveryPoint = findJournalRecoveryPoint(
|
||||||
|
recoveredCoverageGraph,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
assert.ok(recoveredCoverageRecoveryPoint);
|
||||||
|
assert.equal(recoveredCoverageRecoveryPoint.path, "reverse-journal");
|
||||||
|
assert.equal(recoveredCoverageRecoveryPoint.affectedJournals.length, 4);
|
||||||
|
|
||||||
rollbackBatch(graph, recoveryPoint.affectedJournals[0]);
|
rollbackBatch(graph, recoveryPoint.affectedJournals[0]);
|
||||||
assert.equal(graph.nodes.length, 0);
|
assert.equal(graph.nodes.length, 0);
|
||||||
assert.equal(graph.historyState.lastProcessedAssistantFloor, -1);
|
assert.equal(graph.historyState.lastProcessedAssistantFloor, -1);
|
||||||
|
|||||||
Reference in New Issue
Block a user