iflytek/astron-agent · error · SaveSnapshotDidNotStabilizeError
SaveSnapshotDidNotStabilizeError
Error message
SaveSnapshotDidNotStabilizeError
What it means
SaveSnapshotDidNotStabilizeError is thrown by the workflow save coordinator when the persisted snapshot never converges to a stable state: after each persist, the captured workflow snapshot either changed fingerprint or the revision advanced again, and the loop exhausts maxStabilizationAttempts retries. It indicates edits keep arriving faster than the save loop can persist them, or the snapshot fingerprint is non-deterministic.
Solutions
- Check persistSnapshot for writes back into workflow state (IDs, timestamps, normalization) that change the fingerprint after save; make it side-effect-free.
- Make captureSnapshot deterministic — exclude volatile fields (updatedAt, runtime node ids) from the fingerprint.
- Increase maxStabilizationAttempts or add debounce to auto-save so bursts of edits coalesce.
- Catch the error in the caller (flush/promise consumers) and surface a non-blocking 'save failed' toast with a retry action instead of breaking the editor.
Example fix
// before: fingerprint includes volatile fields
const fingerprint = hash(JSON.stringify(store.getState().workflow));
// after: exclude volatile fields from fingerprint
const { updatedAt, runtimeMeta, ...stable } = store.getState().workflow;
const fingerprint = hash(JSON.stringify(stable)); Defensive patterns
Strategy: validation
Validate before calling
const knownRoutes = ['/api/v1/agents', '/api/v1/bots']; if (!knownRoutes.some(r => url.startsWith(r))) console.warn('unregistered route', url); Prevention
- Generate the client from the server's OpenAPI document so paths stay in sync
- Check hub logs (http.url.not.found entries include the URL) whenever an endpoint moves
- Validate proxy/gateway rewrite rules in CI with smoke tests
- Deprecate endpoints with overlap periods instead of hard removal
When it happens
Trigger: Rapid successive edits to the workflow while auto-save runs, so every persist completes with a newer revision than the one persisted; or captureSnapshot() returning a different fingerprint each time for identical state (non-deterministic serialization).
Common situations: Users typing/dragging in the workflow editor continuously while auto-save fires; a persistSnapshot implementation that mutates state (e.g. writing back IDs/timestamps into the store, causing a new fingerprint each cycle); debouncing misconfigured so saves are scheduled on every keystroke.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- RAGFlow chunk snapshot remained incomplete after retries…
- 8103
- RAGFlow chunk snapshot did not stabilize after retries: doc=
- Document splitting failed after retries
- GetFileContentFailed
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/9d25449ceefc09cd.
Report an issue: GitHub.
Appendix: source
Thrown at console/frontend/src/components/workflow/store/workflow-save-coordinator.ts:112
const currentSnapshot = options.captureSnapshot();
const isStable =
revision === requestedRevision &&
currentSnapshot?.fingerprint === snapshot.fingerprint;
// Writes are serialized, so every successful response is safe to apply
// in order. A later edit is persisted before the flush barrier resolves.
if (options.isSnapshotCurrent?.(snapshot.value) !== false) {
options.onPersisted?.(result, snapshot.value);
}
if (isStable) {
persistedRevision = revision;
persistedFingerprint = snapshot.fingerprint;
return;
}
attempts += 1;
if (attempts >= maxStabilizationAttempts) {
throw new SaveSnapshotDidNotStabilizeError();
}
// If an edit changed the observable snapshot without explicitly
// scheduling auto-save, the worker still treats it as a newer revision.
if (revision === requestedRevision) {
requestedRevision += 1;
}
}
};
const ensureRun = (): Promise<void> => {
const runGeneration = generation;
if (activeRun) {
if (activeRun.generation === runGeneration) {
return activeRun.promise;
}
return activeRun.promise.catch(() => undefined).then(() => ensureRun());
}View on GitHub (pinned to 5e758547a8)