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

  1. Reload the session via SessionStore.load(id) to refresh the revision, then retry save with the server-authoritative snapshot.
  2. 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.
  3. Confirm the kernel is running and /api/ai/agent/saveSession responds (check the main WebSocket and the Network panel).
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/e7d865c88101ad1f. Report an issue: GitHub.