fix(persistence): clear stale pending confirmations

This commit is contained in:
opencode
2026-05-15 19:44:08 +00:00
parent c23633def4
commit 2823d18167
4 changed files with 407 additions and 1 deletions

View File

@@ -4029,6 +4029,138 @@ result = {
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-pending-persist-already-accepted",
globalChatId: "chat-pending-persist-already-accepted",
chatMetadata: {
integrity: "meta-pending-persist-already-accepted",
},
chat: [
{ is_user: true, mes: "用户发言" },
{ is_user: false, mes: "助手回复" },
],
});
const graph = createMeaningfulGraph(
"chat-pending-persist-already-accepted",
"pending-persist-already-accepted",
);
graph.historyState.lastProcessedAssistantFloor = 1;
graph.lastProcessedSeq = 1;
graph.historyState.lastBatchStatus = {
processedRange: [1, 1],
completed: true,
persistence: {
outcome: "queued",
accepted: false,
storageTier: "authority-sql",
reason: "extraction-batch-complete:pending",
revision: 7,
saveMode: "immediate",
saved: false,
queued: true,
blocked: true,
},
historyAdvanceAllowed: false,
historyAdvanced: false,
};
harness.api.setCurrentGraph(graph);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-pending-persist-already-accepted",
revision: 7,
lastPersistedRevision: 7,
lastAcceptedRevision: 7,
acceptedStorageTier: "authority-sql",
queuedPersistRevision: 7,
queuedPersistChatId: "chat-pending-persist-already-accepted",
queuedPersistMode: "immediate",
pendingPersist: true,
writesBlocked: false,
});
harness.runtimeContext.__markSyncDirtyShouldThrow = true;
const result = await harness.api.retryPendingGraphPersist({
reason: "queued-persist-already-accepted-test",
});
assert.equal(result.accepted, true);
assert.equal(
harness.api.getGraphPersistenceState().pendingPersist,
false,
"已被 lastAcceptedRevision 覆盖的 pendingPersist 应在重试时直接清除",
);
assert.equal(
harness.api.getCurrentGraph().historyState.lastBatchStatus.persistence.accepted,
true,
);
assert.equal(
harness.api.getCurrentGraph().historyState.lastBatchStatus.historyAdvanceAllowed,
true,
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-pending-current",
globalChatId: "chat-pending-current",
chatMetadata: {
integrity: "meta-pending-current",
},
chat: [
{ is_user: true, mes: "当前聊天用户发言" },
{ is_user: false, mes: "当前聊天助手回复" },
],
});
const graph = createMeaningfulGraph(
"chat-pending-current",
"pending-persist-chat-mismatch",
);
graph.historyState.lastBatchStatus = {
processedRange: [1, 1],
completed: true,
persistence: {
outcome: "queued",
accepted: false,
storageTier: "authority-sql",
reason: "extraction-batch-complete:pending",
revision: 7,
saveMode: "immediate",
saved: false,
queued: true,
blocked: true,
},
historyAdvanceAllowed: false,
historyAdvanced: false,
};
harness.api.setCurrentGraph(graph);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-pending-current",
revision: 9,
lastPersistedRevision: 9,
lastAcceptedRevision: 9,
acceptedStorageTier: "authority-sql",
queuedPersistRevision: 7,
queuedPersistChatId: "other-chat-pending",
queuedPersistMode: "immediate",
pendingPersist: true,
writesBlocked: false,
});
const result = await harness.api.retryPendingGraphPersist({
reason: "queued-persist-chat-mismatch-test",
});
assert.equal(result.accepted, false);
assert.equal(result.reason, "queued-chat-mismatch");
assert.equal(
harness.api.getGraphPersistenceState().pendingPersist,
true,
"其它聊天的 queued pending 不能被当前聊天 accepted revision 清掉",
);
}
{
const chatId = "meta-authority-indexeddb-migration";
const legacyGraph = stampPersistedGraph(

View File

@@ -5570,6 +5570,157 @@ async function testAutoExtractionContinuesWithRecoverablePendingPersistence() {
assert.deepEqual(deferredReasons, []);
}
async function testAutoExtractionIgnoresAcceptedPendingPersistenceFlag() {
const deferredReasons = [];
const executeCalls = [];
const currentGraph = {
historyState: {
lastBatchStatus: {
processedRange: [1, 1],
persistence: {
outcome: "queued",
accepted: false,
revision: 7,
reason: "extraction-batch-complete:pending",
storageTier: "authority-sql",
},
},
},
};
await runExtractionController({
console,
getIsExtracting: () => false,
getCurrentGraph: () => currentGraph,
getSettings: () => ({ enabled: true, extractEvery: 1 }),
getContext: () => ({
chat: [
{ is_user: true, mes: "u" },
{ is_user: false, mes: "a" },
],
}),
getAssistantTurns: () => [1],
getLastProcessedAssistantFloor: () => 0,
getGraphPersistenceState: () => ({
loadState: "loaded",
pendingPersist: true,
lastAcceptedRevision: 7,
queuedPersistRevision: 7,
lastRecoverableStorageTier: "none",
acceptedStorageTier: "authority-sql",
}),
ensureGraphMutationReady: () => true,
async retryPendingGraphPersist() {
throw new Error("accepted pending flag should not require retry");
},
async recoverHistoryIfNeeded() {
return true;
},
deferAutoExtraction(reason) {
deferredReasons.push(reason);
},
setIsExtracting() {},
beginStageAbortController() {
return { signal: {} };
},
setLastExtractionStatus() {},
async executeExtractionBatch(options) {
executeCalls.push(options);
return {
success: true,
result: {
newNodes: 0,
updatedNodes: 0,
newEdges: 0,
},
batchStatus: {
persistence: {
accepted: true,
},
},
historyAdvanceAllowed: true,
};
},
finishStageAbortController() {},
isAbortError: () => false,
notifyExtractionIssue() {},
});
assert.equal(executeCalls.length, 1);
assert.deepEqual(deferredReasons, []);
}
async function testAutoExtractionBlocksWhenQueuedRevisionStillPending() {
const deferredReasons = [];
const executeCalls = [];
const statusUpdates = [];
const currentGraph = {
historyState: {
lastBatchStatus: {
processedRange: [1, 1],
persistence: {
outcome: "queued",
accepted: false,
revision: 7,
reason: "extraction-batch-complete:pending",
storageTier: "authority-sql",
},
},
},
};
await runExtractionController({
console,
getIsExtracting: () => false,
getCurrentGraph: () => currentGraph,
getSettings: () => ({ enabled: true, extractEvery: 1 }),
getContext: () => ({
chat: [
{ is_user: true, mes: "u" },
{ is_user: false, mes: "a" },
],
}),
getAssistantTurns: () => [1],
getLastProcessedAssistantFloor: () => 0,
getGraphPersistenceState: () => ({
loadState: "loaded",
pendingPersist: true,
lastAcceptedRevision: 7,
queuedPersistRevision: 8,
lastRecoverableStorageTier: "none",
acceptedStorageTier: "authority-sql",
}),
ensureGraphMutationReady: () => true,
async retryPendingGraphPersist() {
return {
accepted: false,
reason: "queued-revision-still-pending",
};
},
async recoverHistoryIfNeeded() {
return true;
},
deferAutoExtraction(reason) {
deferredReasons.push(reason);
},
setIsExtracting() {},
setLastExtractionStatus(label, text, level) {
statusUpdates.push({ label, text, level });
},
async executeExtractionBatch(options) {
executeCalls.push(options);
return { success: true };
},
finishStageAbortController() {},
isAbortError: () => false,
notifyExtractionIssue() {},
});
assert.equal(executeCalls.length, 0);
assert.deepEqual(deferredReasons, ["pending-persist"]);
assert.equal(statusUpdates.at(-1)?.label, "等待持久化确认");
}
async function testRemoveNodeHandlesCyclicChildGraph() {
const graph = createEmptyGraph();
const nodeA = addNode(
@@ -8141,6 +8292,8 @@ await testAutoExtractionDefersWhenGraphNotReady();
await testAutoExtractionDefersWhenAlreadyExtracting();
await testAutoExtractionDefersWhenHistoryRecoveryBusy();
await testAutoExtractionContinuesWithRecoverablePendingPersistence();
await testAutoExtractionIgnoresAcceptedPendingPersistenceFlag();
await testAutoExtractionBlocksWhenQueuedRevisionStillPending();
await testRemoveNodeHandlesCyclicChildGraph();
await testGenerationRecallAppliesFinalInjectionOncePerTransaction();
await testHistoryGenerationReusesPersistedRecallForStableUserFloor();