siyuan-note/siyuan · error

decode existing session data failed

Error message

decode existing session data failed: %w

What it means

When saving, SaveSessionState reads the existing session.json to compare revisions and committed turn ids. If that on-disk file exists but cannot be parsed as JSON, it fails closed with "decode existing session data failed: %w" instead of treating the store as empty — overwriting would destroy the old revision history and break commit idempotency.

Solutions

  1. Inspect the wrapped error and the corrupt session.json at data/<sessionsDir>/<id>/session.json
  2. Restore the file from backup or sync history, then retry the save
  3. If unrecoverable, move the corrupt file aside and rebuild the session (accepting history loss for that session)
  4. Fix the writer to save atomically (write temp file + rename) to prevent recurrence
Defensive patterns

Strategy: fallback

Validate before calling

raw, err := os.ReadFile(sessionPath)
if err == nil && len(raw) > 0 { var probe map[string]any; if json.Unmarshal(raw, &probe) != nil { /* corrupt: restore from backup before saving */ } }

Type guard

func existingSessionIsParseable(path string) bool {
    raw, err := os.ReadFile(path)
    if err != nil || len(raw) == 0 { return true }
    var m map[string]any
    return json.Unmarshal(raw, &m) == nil
}

Try / catch

if _, _, err := SaveSessionState(data); err != nil && strings.HasPrefix(err.Error(), "decode existing session data failed") {
    // restore session.json from backup/sync history, or quarantine it, then retry
}

Prevention

When it happens

Trigger: session.json on disk is truncated (crash mid-write, disk full), corrupted by an external process, or written by an incompatible older/newer format that no longer parses.

Common situations: Kernel killed during a previous save without atomic rename; manual edits to data/ai session files; restoring partial backups; disk-full leaving zero-length or partial files.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/091d88f4331a3d78. Report an issue: GitHub.

Appendix: source

Thrown at kernel/agent/session.go:354

	delete(newData, "expectedRevision")
	delete(newData, "commitTurnID")
	delete(newData, "recoveryTurnID")
	delete(newData, "recoveryState")
	delete(newData, "recoveryRevision")
	delete(newData, "agentRunning")
	delete(newData, "lastCommittedTurnID")
	commitTurnID := meta.CommitTurnID
	if commitTurnID == "" {
		commitTurnID = meta.RecoveryTurnID
	}

	currentRevision := int64(0)
	currentCommittedTurnID := ""
	existing, err := os.ReadFile(path)
	if err == nil && len(existing) > 0 {
		var existingData map[string]any
		if err := gulu.JSON.UnmarshalJSON(existing, &existingData); err != nil {
			return 0, nil, fmt.Errorf("decode existing session data failed: %w", err)
		} else {
			currentRevision = numberToInt64(existingData["revision"])
			currentCommittedTurnID, _ = existingData["lastCommittedTurnID"].(string)
			if commitTurnID != "" && currentCommittedTurnID == commitTurnID {
				// 提交响应丢失后,客户端可能原样重试同一个 commitTurnID。此判断要先于修订号校验,
				// 并且不能再用客户端快照覆盖已经由 runtime 生成的权威内容。
				if err := markRuntimeCommittedLocked(meta.ID, commitTurnID); err != nil {
					logging.LogWarnf("clean committed agent runtime failed: %s", err)
				}
				return currentRevision, existingData, nil
			}
			if meta.ExpectedRevision != nil && *meta.ExpectedRevision != currentRevision {
				return currentRevision, nil, ErrSessionConflict
			}
			for k, v := range existingData {
				if _, ok := newData[k]; !ok {
					// messages 是已废弃的旧会话字段,不再带入新格式;其他未知字段原样保留,
					// 避免前后端版本不一致时擦除较新版本写入的数据。

View on GitHub (pinned to 9f775e8a12)