{"record":{"id":"e7d865c88101ad1f","repo":"siyuan-note/siyuan","slug":"failed-to-save-agent-session","errorCode":null,"errorMessage":"Failed to save agent session","messagePattern":"Failed to save agent session","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/layout/dock/agent/SessionStore.ts","lineNumber":179,"sourceCode":"        }\n        // 连续写入期间若三次读取都落后于本地已知版本，则不返回旧数据覆盖界面。\n        return null;\n    },\n\n    async save(session: AgentSession): Promise<SessionSaveResult> {\n        const snapshot = JSON.parse(JSON.stringify(session)) as AgentSession;\n        snapshot.updatedAt = Date.now();\n        const baseRevision = snapshot.expectedRevision ?? sessionRevisions.get(snapshot.id) ?? snapshot.revision ?? 0;\n        const previous = sessionSaveQueues.get(snapshot.id);\n        const persist = async (expectedRevision: number) => {\n            snapshot.expectedRevision = expectedRevision;\n            const resp = await fetchSyncPost(API + \"/saveSession\", snapshot, APP_HEADER) as {\n                code: number;\n                msg?: string;\n                data?: {revision?: number; session?: AgentSession};\n            };\n            if (!resp || resp.code !== 0) {\n                throw new Error(resp?.msg || \"Failed to save agent session\");\n            }\n            const revision = resp.data?.revision ?? expectedRevision;\n            sessionRevisions.set(snapshot.id, revision);\n            if (snapshot.commitTurnID || snapshot.recoveryTurnID) {\n                sessionRuntimeRevisions.set(snapshot.id, 0);\n            }\n            return {revision, session: resp.data?.session};\n        };\n        const save = previous ? previous.then((result) => persist(result.revision)) : persist(baseRevision);\n        sessionSaveQueues.set(snapshot.id, save);\n        try {\n            return await save;\n        } finally {\n            if (sessionSaveQueues.get(snapshot.id) === save) {\n                sessionSaveQueues.delete(snapshot.id);\n            }\n        }\n    },","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/app/src/layout/dock/agent/SessionStore.ts#L161-L197","documentation":"Thrown by SessionStore.save() when POST /api/ai/agent/saveSession returns a falsy response or a body whose code is not 0. The kernel's reply msg is used if present; otherwise the generic 'Failed to save agent session' string is used. Saves are serialized per session through sessionSaveQueues and chained off the previous revision, so the failure usually reflects a server-side rejection of the optimistic-concurrency payload.","triggerScenarios":"Calling SessionStore.save(session) when the kernel is unreachable (fetchSyncPost resolves null), when the snapshot's expectedRevision is stale (server-side revision conflict), when the session payload fails server validation, or when the kernel returns a non-zero code with a msg describing a conflict/validation failure.","commonSituations":"Two SiYuan instances editing the same agent session on different devices (revision race); kernel restarted mid-turn so in-memory revision is ahead of disk; corrupted session JSON sent across; backend API version mismatch after a partial upgrade.","solutions":["Reload the session via SessionStore.load(id) to refresh the revision, then retry save with the server-authoritative snapshot.","Inspect resp.msg in the thrown Error to get the kernel's specific rejection reason; surface it to the user instead of the generic fallback.","Confirm the kernel is running and /api/ai/agent/saveSession responds (check the main WebSocket and the Network panel).","If concurrent edits are expected, serialize writes through a single SessionStore.save() queue per session id (already done) and do not bypass it with direct fetch calls."],"exampleFix":"// before\ntry { await SessionStore.save(session); } catch (e) { /* swallow */ }\n// after\ntry {\n    await SessionStore.save(session);\n} catch (e) {\n    const fresh = await SessionStore.load(session.id);\n    if (fresh) { fresh.entries = session.entries; await SessionStore.save(fresh); }\n    else { showMessage(e.message || 'Failed to save agent session'); }\n}","handlingStrategy":"try-catch","validationCode":"if (!session || !session.id) throw new Error('Cannot save: session missing id');\nconst known = SessionStore.getRevision(session.id);\nsession.expectedRevision = session.expectedRevision ?? known;","typeGuard":"function isSessionSavable(s: unknown): s is AgentSession {\n    return typeof s === 'object' && s !== null && typeof (s as AgentSession).id === 'string';\n}","tryCatchPattern":"try { await SessionStore.save(session); }\ncatch (e) {\n    const fresh = await SessionStore.load(session.id);\n    if (fresh) { Object.assign(fresh, session, {expectedRevision: fresh.revision}); await SessionStore.save(fresh); }\n    else showMessage(e.message || 'Failed to save agent session');\n}","preventionTips":["Always pass a non-null session with an id; never construct a session inline without an id.","Set session.expectedRevision from SessionStore.getRevision(id) before saving to avoid stale writes.","Do not bypass SessionStore.save with direct fetch calls; the per-session queue relies on it.","Surface resp.msg to the user instead of the generic fallback so conflicts are visible."],"tags":["network","agent-session","optimistic-concurrency","typescript"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}