Fix stale pending persistence gate

This commit is contained in:
Hao19911125
2026-04-10 18:38:22 +08:00
parent c31c7e14c2
commit 4423034007
3 changed files with 166 additions and 3 deletions

View File

@@ -176,12 +176,31 @@ function buildCommittedBatchPersistSnapshot(
}; };
} }
function isPersistenceRevisionAccepted(runtime, persistence = null) {
if (!persistence || persistence.accepted === true) return true;
const graphPersistenceState = runtime?.getGraphPersistenceState?.() || {};
if (graphPersistenceState.pendingPersist === true) {
return false;
}
const persistenceRevision = Number(persistence?.revision || 0);
if (!Number.isFinite(persistenceRevision) || persistenceRevision <= 0) {
return false;
}
const lastAcceptedRevision = Math.max(
Number(graphPersistenceState?.lastAcceptedRevision || 0),
Number(graphPersistenceState?.commitMarker?.accepted === true
? graphPersistenceState?.commitMarker?.revision
: 0),
);
return Number.isFinite(lastAcceptedRevision) && lastAcceptedRevision >= persistenceRevision;
}
function getPendingPersistenceGateInfo(runtime) { function getPendingPersistenceGateInfo(runtime) {
const graph = runtime?.getCurrentGraph?.(); const graph = runtime?.getCurrentGraph?.();
const batchStatus = graph?.historyState?.lastBatchStatus || null; const batchStatus = graph?.historyState?.lastBatchStatus || null;
const persistence = batchStatus?.persistence || null; const persistence = batchStatus?.persistence || null;
const pendingPersist = runtime?.getGraphPersistenceState?.()?.pendingPersist === true; const pendingPersist = runtime?.getGraphPersistenceState?.()?.pendingPersist === true;
const accepted = persistence?.accepted === true; const accepted = isPersistenceRevisionAccepted(runtime, persistence);
if (!pendingPersist && (!persistence || accepted)) { if (!pendingPersist && (!persistence || accepted)) {
return null; return null;
} }

View File

@@ -206,6 +206,129 @@ async function testManualExtractNoBatchesDoesNotStayRunning() {
assert.notEqual(context.runtimeStatus.level, "running"); assert.notEqual(context.runtimeStatus.level, "running");
} }
async function testManualExtractIgnoresSupersededPendingPersistence() {
let executeExtractionBatchCalls = 0;
let assistantTurnCallCount = 0;
const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
const context = {
...createBaseStatusContext(),
isExtracting: false,
graphPersistenceState: {
pendingPersist: false,
lastAcceptedRevision: 7,
},
currentGraph: {
historyState: {
lastBatchStatus: {
processedRange: [1, 1],
persistence: {
outcome: "queued",
accepted: false,
revision: 7,
reason: "extraction-batch-complete:pending",
storageTier: "none",
},
},
},
},
getCurrentChatId() {
return "chat-mobile";
},
getCurrentGraph() {
return context.currentGraph;
},
getIsExtracting() {
return context.isExtracting;
},
getGraphPersistenceState() {
return {
pendingPersist: false,
lastAcceptedRevision: 7,
};
},
ensureGraphMutationReady() {
return true;
},
async recoverHistoryIfNeeded() {
return true;
},
normalizeGraphRuntimeState(graph) {
return graph;
},
setCurrentGraph(graph) {
context.currentGraph = graph;
},
createEmptyGraph() {
return {};
},
getContext() {
return { chat };
},
getAssistantTurns() {
assistantTurnCallCount += 1;
return assistantTurnCallCount <= 2 ? [1] : [];
},
getLastProcessedAssistantFloor() {
return 0;
},
clampInt(value, fallback) {
return Number.isFinite(Number(value)) ? Number(value) : fallback;
},
getSettings() {
return { extractEvery: 1 };
},
beginStageAbortController() {
return { signal: {} };
},
async executeExtractionBatch() {
executeExtractionBatchCalls += 1;
return {
success: true,
result: {
newNodes: 0,
updatedNodes: 0,
newEdges: 0,
},
effects: {},
batchStatus: {
persistence: {
accepted: true,
},
},
historyAdvanceAllowed: true,
};
},
async retryPendingGraphPersist() {
return {
accepted: false,
reason: "no-pending-persist",
};
},
isAbortError() {
return false;
},
onManualExtractController,
finishStageAbortController() {},
setIsExtracting(value) {
context.isExtracting = value;
},
setLastExtractionStatus(text, meta, level) {
context.lastExtractionStatus = { text, meta, level };
context.runtimeStatus = { text, meta, level };
},
toastr: {
info() {},
success() {},
warning() {},
error() {},
},
result: null,
};
await onManualExtractController(context, { drainAll: false });
assert.equal(executeExtractionBatchCalls, 1);
assert.notEqual(context.lastExtractionStatus.text, "等待持久化确认");
}
async function testManualRebuildSetsTerminalRuntimeStatus() { async function testManualRebuildSetsTerminalRuntimeStatus() {
const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }]; const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
const context = { const context = {
@@ -281,6 +404,7 @@ async function testManualRebuildSetsTerminalRuntimeStatus() {
testIndexDefinesLastProcessedAssistantFloorHelper(); testIndexDefinesLastProcessedAssistantFloorHelper();
await testVectorSyncTerminalStateUpdatesRuntime(); await testVectorSyncTerminalStateUpdatesRuntime();
await testManualExtractNoBatchesDoesNotStayRunning(); await testManualExtractNoBatchesDoesNotStayRunning();
await testManualExtractIgnoresSupersededPendingPersistence();
await testManualRebuildSetsTerminalRuntimeStatus(); await testManualRebuildSetsTerminalRuntimeStatus();
console.log("mobile-status-regressions tests passed"); console.log("mobile-status-regressions tests passed");

View File

@@ -9442,11 +9442,30 @@ function _formatPersistenceOutcomeLabel(outcome = "") {
} }
} }
function _isPersistenceRevisionAccepted(persistence = null, loadInfo = {}) {
if (!persistence || persistence.accepted === true) return true;
if (loadInfo?.pendingPersist === true) return false;
const persistenceRevision = Number(persistence?.revision || 0);
if (!Number.isFinite(persistenceRevision) || persistenceRevision <= 0) {
return false;
}
const commitMarkerRevision =
loadInfo?.commitMarker?.accepted === true
? Number(loadInfo.commitMarker.revision || 0)
: 0;
const lastAcceptedRevision = Math.max(
Number(loadInfo?.lastAcceptedRevision || 0),
commitMarkerRevision,
);
return Number.isFinite(lastAcceptedRevision) && lastAcceptedRevision >= persistenceRevision;
}
function _formatDashboardPersistMeta(loadInfo = {}, batchStatus = null) { function _formatDashboardPersistMeta(loadInfo = {}, batchStatus = null) {
const persistence = batchStatus?.persistence || null; const persistence = batchStatus?.persistence || null;
if (persistence) { if (persistence) {
const accepted = _isPersistenceRevisionAccepted(persistence, loadInfo);
const parts = [ const parts = [
_formatPersistenceOutcomeLabel(persistence.outcome), accepted ? "已确认" : _formatPersistenceOutcomeLabel(persistence.outcome),
persistence.storageTier ? `tier ${persistence.storageTier}` : "", persistence.storageTier ? `tier ${persistence.storageTier}` : "",
Number.isFinite(Number(persistence.revision)) && Number(persistence.revision) > 0 Number.isFinite(Number(persistence.revision)) && Number(persistence.revision) > 0
? `rev ${Number(persistence.revision)}` ? `rev ${Number(persistence.revision)}`
@@ -9477,6 +9496,7 @@ function _formatDashboardHistoryMeta(graph = null, loadInfo = {}, batchStatus =
const lastConfirmedFloor = const lastConfirmedFloor =
graph?.historyState?.lastProcessedAssistantFloor ?? -1; graph?.historyState?.lastProcessedAssistantFloor ?? -1;
const persistence = batchStatus?.persistence || null; const persistence = batchStatus?.persistence || null;
const accepted = _isPersistenceRevisionAccepted(persistence, loadInfo);
const processedRange = Array.isArray(batchStatus?.processedRange) const processedRange = Array.isArray(batchStatus?.processedRange)
? batchStatus.processedRange ? batchStatus.processedRange
: []; : [];
@@ -9485,7 +9505,7 @@ function _formatDashboardHistoryMeta(graph = null, loadInfo = {}, batchStatus =
? Number(processedRange[1]) ? Number(processedRange[1])
: null; : null;
if (persistence && persistence.accepted !== true && pendingFloor != null) { if (persistence && !accepted && pendingFloor != null) {
return `持久化待确认:本地已抽取到楼层 ${pendingFloor},已确认楼层 ${lastConfirmedFloor}`; return `持久化待确认:本地已抽取到楼层 ${pendingFloor},已确认楼层 ${lastConfirmedFloor}`;
} }