siyuan-note/siyuan · error · Error
Failed to save agent session
Error message
Failed to save agent session
What it means
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.
Source
Thrown at app/src/layout/dock/agent/SessionStore.ts:179
}
// 连续写入期间若三次读取都落后于本地已知版本,则不返回旧数据覆盖界面。
return null;
},
async save(session: AgentSession): Promise<SessionSaveResult> {
const snapshot = JSON.parse(JSON.stringify(session)) as AgentSession;
snapshot.updatedAt = Date.now();
const baseRevision = snapshot.expectedRevision ?? sessionRevisions.get(snapshot.id) ?? snapshot.revision ?? 0;
const previous = sessionSaveQueues.get(snapshot.id);
const persist = async (expectedRevision: number) => {
snapshot.expectedRevision = expectedRevision;
const resp = await fetchSyncPost(API + "/saveSession", snapshot, APP_HEADER) as {
code: number;
msg?: string;
data?: {revision?: number; session?: AgentSession};
};
if (!resp || resp.code !== 0) {
throw new Error(resp?.msg || "Failed to save agent session");
}
const revision = resp.data?.revision ?? expectedRevision;
sessionRevisions.set(snapshot.id, revision);
if (snapshot.commitTurnID || snapshot.recoveryTurnID) {
sessionRuntimeRevisions.set(snapshot.id, 0);
}
return {revision, session: resp.data?.session};
};
const save = previous ? previous.then((result) => persist(result.revision)) : persist(baseRevision);
sessionSaveQueues.set(snapshot.id, save);
try {
return await save;
} finally {
if (sessionSaveQueues.get(snapshot.id) === save) {
sessionSaveQueues.delete(snapshot.id);
}
}
},View on GitHub (pinned to 251596fc0d)
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.
Example fix
// before
try { await SessionStore.save(session); } catch (e) { /* swallow */ }
// after
try {
await SessionStore.save(session);
} catch (e) {
const fresh = await SessionStore.load(session.id);
if (fresh) { fresh.entries = session.entries; await SessionStore.save(fresh); }
else { showMessage(e.message || 'Failed to save agent session'); }
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!session || !session.id) throw new Error('Cannot save: session missing id');
const known = SessionStore.getRevision(session.id);
session.expectedRevision = session.expectedRevision ?? known; Type guard
function isSessionSavable(s: unknown): s is AgentSession {
return typeof s === 'object' && s !== null && typeof (s as AgentSession).id === 'string';
} Try / catch
try { await SessionStore.save(session); }
catch (e) {
const fresh = await SessionStore.load(session.id);
if (fresh) { Object.assign(fresh, session, {expectedRevision: fresh.revision}); await SessionStore.save(fresh); }
else showMessage(e.message || 'Failed to save agent session');
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to remove agent session
- Failed to update agent session permission
- ${data.error.message}
- Canvas 2D is unavailable
- WebGL2 is unavailable
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/e7d865c88101ad1f.
Report an issue: GitHub.