mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
fix(persistence): clear stale pending confirmations
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user