{"record":{"id":"9d25449ceefc09cd","repo":"iflytek/astron-agent","slug":"savesnapshotdidnotstabilizeerror","errorCode":null,"errorMessage":"SaveSnapshotDidNotStabilizeError","messagePattern":"SaveSnapshotDidNotStabilizeError","errorType":"exception","errorClass":"SaveSnapshotDidNotStabilizeError","httpStatus":null,"severity":"error","filePath":"console/frontend/src/components/workflow/store/workflow-save-coordinator.ts","lineNumber":112,"sourceCode":"      const currentSnapshot = options.captureSnapshot();\n      const isStable =\n        revision === requestedRevision &&\n        currentSnapshot?.fingerprint === snapshot.fingerprint;\n\n      // Writes are serialized, so every successful response is safe to apply\n      // in order. A later edit is persisted before the flush barrier resolves.\n      if (options.isSnapshotCurrent?.(snapshot.value) !== false) {\n        options.onPersisted?.(result, snapshot.value);\n      }\n      if (isStable) {\n        persistedRevision = revision;\n        persistedFingerprint = snapshot.fingerprint;\n        return;\n      }\n\n      attempts += 1;\n      if (attempts >= maxStabilizationAttempts) {\n        throw new SaveSnapshotDidNotStabilizeError();\n      }\n\n      // If an edit changed the observable snapshot without explicitly\n      // scheduling auto-save, the worker still treats it as a newer revision.\n      if (revision === requestedRevision) {\n        requestedRevision += 1;\n      }\n    }\n  };\n\n  const ensureRun = (): Promise<void> => {\n    const runGeneration = generation;\n    if (activeRun) {\n      if (activeRun.generation === runGeneration) {\n        return activeRun.promise;\n      }\n      return activeRun.promise.catch(() => undefined).then(() => ensureRun());\n    }","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/console/frontend/src/components/workflow/store/workflow-save-coordinator.ts#L94-L130","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: fingerprint includes volatile fields\nconst fingerprint = hash(JSON.stringify(store.getState().workflow));\n// after: exclude volatile fields from fingerprint\nconst { updatedAt, runtimeMeta, ...stable } = store.getState().workflow;\nconst fingerprint = hash(JSON.stringify(stable));","handlingStrategy":"validation","validationCode":"const knownRoutes = ['/api/v1/agents', '/api/v1/bots']; if (!knownRoutes.some(r => url.startsWith(r))) console.warn('unregistered route', url);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["autosave","state-management","retry-exhausted","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}