fix: orphan accepted commit marker self-healing — auto-clear stale marker when no recoverable graph source exists

- Add maybeResolveOrphanAcceptedCommitMarker() for conservative orphan detection:
  - Chat-state sidecar rescue first (preserves marker, keeps mismatch diagnostic)
  - Only clears marker when ALL local sources confirmed absent
  - Guards: shadow-available, migration-failed, chat-switched all block clearing
- Enhance clearCurrentChatCommitMarker() with resetAcceptedRevision option
  - Resets lastAcceptedRevision and acceptedStorageTier when marker is orphan
- Integrate orphan resolution into loadGraphFromIndexedDb() empty+mismatch branch
- Update onDeleteCurrentIdbController/onDeleteAllIdbController to resetAcceptedRevision
- Update graph-persistence regressions:
  - Orphan marker scenario now auto-heals to EMPTY_CONFIRMED (was: permanently BLOCKED)
  - Add chat-state rescue guard test: sidecar data prevents marker clearing
This commit is contained in:
Youzini-afk
2026-04-12 20:35:29 +08:00
parent 648a7a1741
commit 5bd29c99d5
3 changed files with 257 additions and 3 deletions

174
index.js
View File

@@ -325,6 +325,7 @@ function clearCurrentChatCommitMarker(
context = getContext(),
reason = "manual-clear-commit-marker",
immediate = true,
resetAcceptedRevision = false,
} = {},
) {
if (!context) {
@@ -337,15 +338,23 @@ function clearCurrentChatCommitMarker(
}
const marker = getChatCommitMarker(context);
const acceptedRevision = getAcceptedCommitMarkerRevision(marker);
writeChatMetadataPatch(context, {
[GRAPH_COMMIT_MARKER_KEY]: null,
});
const saveMode = triggerChatMetadataSave(context, { immediate });
const shouldResetAcceptedRevision = resetAcceptedRevision === true;
updateGraphPersistenceState({
commitMarker: null,
persistMismatchReason: "",
lastPersistReason: String(reason || "manual-clear-commit-marker"),
lastPersistMode: `commit-marker-clear:${saveMode}`,
acceptedStorageTier: shouldResetAcceptedRevision
? "none"
: String(graphPersistenceState.acceptedStorageTier || "none"),
lastAcceptedRevision: shouldResetAcceptedRevision
? 0
: Number(graphPersistenceState.lastAcceptedRevision || 0),
});
return {
@@ -353,6 +362,7 @@ function clearCurrentChatCommitMarker(
reason: String(reason || "manual-clear-commit-marker"),
saveMode,
marker: cloneRuntimeDebugValue(marker, null),
acceptedRevision,
};
}
@@ -5663,6 +5673,148 @@ function applyIndexedDbEmptyToRuntime(
};
}
async function maybeResolveOrphanAcceptedCommitMarker(
chatId,
{
source = "indexeddb-probe",
attemptIndex = 0,
commitMarker = null,
migrationResult = null,
shadowSnapshot = null,
applyEmptyState = false,
} = {},
) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
const context = getContext();
const activeIdentity = resolveCurrentChatIdentity(context);
const activePersistenceChatId =
normalizeChatIdCandidate(activeIdentity?.chatId) || normalizedChatId;
const acceptedRevision = getAcceptedCommitMarkerRevision(commitMarker);
if (!normalizedChatId || acceptedRevision <= 0) {
return {
resolved: false,
reason: "marker-not-accepted",
result: null,
chatId: normalizedChatId || "",
};
}
if (!doesChatIdMatchResolvedGraphIdentity(normalizedChatId, activeIdentity)) {
return {
resolved: false,
reason: "chat-switched",
result: null,
chatId: normalizedChatId,
};
}
let chatStateResult = null;
if (canUseHostGraphChatStatePersistence(context)) {
chatStateResult = await loadGraphFromChatState(activePersistenceChatId, {
source: `${source}:orphan-chat-state-fallback`,
attemptIndex,
allowOverride: true,
});
if (chatStateResult?.loaded) {
return {
resolved: true,
reason: "chat-state-loaded",
result: chatStateResult,
chatId: normalizedChatId,
chatStateResult,
orphanCleared: false,
};
}
const chatStateReason = String(chatStateResult?.reason || "");
if (
chatStateReason &&
chatStateReason !== "chat-state-empty" &&
chatStateReason !== "chat-state-unavailable"
) {
return {
resolved: false,
reason: chatStateReason,
result: null,
chatId: normalizedChatId,
chatStateResult,
};
}
}
if (shadowSnapshot) {
return {
resolved: false,
reason: "shadow-available",
result: null,
chatId: normalizedChatId,
chatStateResult,
};
}
if (String(migrationResult?.reason || "").trim() === "migration-failed") {
return {
resolved: false,
reason: "migration-failed",
result: null,
chatId: normalizedChatId,
chatStateResult,
};
}
const clearResult = clearCurrentChatCommitMarker({
context,
reason: `orphan-accepted-marker:${source}`,
immediate: true,
resetAcceptedRevision: true,
});
debugDebug("[ST-BME] 已自动清理孤儿 accepted commit marker", {
chatId: normalizedChatId,
source,
acceptedRevision,
migrationReason: String(migrationResult?.reason || ""),
chatStateReason: String(chatStateResult?.reason || ""),
});
if (applyEmptyState) {
const emptyResult = applyIndexedDbEmptyToRuntime(activePersistenceChatId, {
source: `${source}:orphan-accepted-marker`,
attemptIndex,
});
return {
resolved: true,
reason: "orphan-accepted-marker-cleared",
result: {
...emptyResult,
orphanCommitMarkerCleared: true,
clearedMarkerRevision: acceptedRevision,
},
chatId: normalizedChatId,
chatStateResult,
clearResult,
orphanCleared: true,
};
}
return {
resolved: true,
reason: "orphan-accepted-marker-cleared",
result: {
success: false,
loaded: false,
reason: "indexeddb-empty",
chatId: normalizedChatId,
attemptIndex,
orphanCommitMarkerCleared: true,
clearedMarkerRevision: acceptedRevision,
},
chatId: normalizedChatId,
chatStateResult,
clearResult,
orphanCleared: true,
};
}
function applyIndexedDbSnapshotToRuntime(
chatId,
snapshot,
@@ -6078,6 +6230,28 @@ async function loadGraphFromIndexedDb(
return shadowRestoreResult;
}
}
if (commitMarkerDiagnostic?.reason) {
const orphanMarkerResolution =
await maybeResolveOrphanAcceptedCommitMarker(normalizedChatId, {
source,
attemptIndex,
commitMarker,
migrationResult,
shadowSnapshot,
applyEmptyState,
});
if (orphanMarkerResolution?.result) {
if (
!orphanMarkerResolution.orphanCleared &&
orphanMarkerResolution.result?.loaded
) {
updateGraphPersistenceState({
persistMismatchReason: commitMarkerDiagnostic.reason,
});
}
return orphanMarkerResolution.result;
}
}
if (
applyEmptyState &&
!commitMarkerDiagnostic?.reason &&