{"record":{"id":"091d88f4331a3d78","repo":"siyuan-note/siyuan","slug":"decode-existing-session-data-failed-w","errorCode":null,"errorMessage":"decode existing session data failed: %w","messagePattern":"decode existing session data failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/agent/session.go","lineNumber":354,"sourceCode":"\tdelete(newData, \"expectedRevision\")\n\tdelete(newData, \"commitTurnID\")\n\tdelete(newData, \"recoveryTurnID\")\n\tdelete(newData, \"recoveryState\")\n\tdelete(newData, \"recoveryRevision\")\n\tdelete(newData, \"agentRunning\")\n\tdelete(newData, \"lastCommittedTurnID\")\n\tcommitTurnID := meta.CommitTurnID\n\tif commitTurnID == \"\" {\n\t\tcommitTurnID = meta.RecoveryTurnID\n\t}\n\n\tcurrentRevision := int64(0)\n\tcurrentCommittedTurnID := \"\"\n\texisting, err := os.ReadFile(path)\n\tif err == nil && len(existing) > 0 {\n\t\tvar existingData map[string]any\n\t\tif err := gulu.JSON.UnmarshalJSON(existing, &existingData); err != nil {\n\t\t\treturn 0, nil, fmt.Errorf(\"decode existing session data failed: %w\", err)\n\t\t} else {\n\t\t\tcurrentRevision = numberToInt64(existingData[\"revision\"])\n\t\t\tcurrentCommittedTurnID, _ = existingData[\"lastCommittedTurnID\"].(string)\n\t\t\tif commitTurnID != \"\" && currentCommittedTurnID == commitTurnID {\n\t\t\t\t// 提交响应丢失后，客户端可能原样重试同一个 commitTurnID。此判断要先于修订号校验，\n\t\t\t\t// 并且不能再用客户端快照覆盖已经由 runtime 生成的权威内容。\n\t\t\t\tif err := markRuntimeCommittedLocked(meta.ID, commitTurnID); err != nil {\n\t\t\t\t\tlogging.LogWarnf(\"clean committed agent runtime failed: %s\", err)\n\t\t\t\t}\n\t\t\t\treturn currentRevision, existingData, nil\n\t\t\t}\n\t\t\tif meta.ExpectedRevision != nil && *meta.ExpectedRevision != currentRevision {\n\t\t\t\treturn currentRevision, nil, ErrSessionConflict\n\t\t\t}\n\t\t\tfor k, v := range existingData {\n\t\t\t\tif _, ok := newData[k]; !ok {\n\t\t\t\t\t// messages 是已废弃的旧会话字段，不再带入新格式；其他未知字段原样保留，\n\t\t\t\t\t// 避免前后端版本不一致时擦除较新版本写入的数据。","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/agent/session.go#L336-L372","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the wrapped error and the corrupt session.json at data/<sessionsDir>/<id>/session.json","Restore the file from backup or sync history, then retry the save","If unrecoverable, move the corrupt file aside and rebuild the session (accepting history loss for that session)","Fix the writer to save atomically (write temp file + rename) to prevent recurrence"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"raw, err := os.ReadFile(sessionPath)\nif err == nil && len(raw) > 0 { var probe map[string]any; if json.Unmarshal(raw, &probe) != nil { /* corrupt: restore from backup before saving */ } }","typeGuard":"func existingSessionIsParseable(path string) bool {\n    raw, err := os.ReadFile(path)\n    if err != nil || len(raw) == 0 { return true }\n    var m map[string]any\n    return json.Unmarshal(raw, &m) == nil\n}","tryCatchPattern":"if _, _, err := SaveSessionState(data); err != nil && strings.HasPrefix(err.Error(), \"decode existing session data failed\") {\n    // restore session.json from backup/sync history, or quarantine it, then retry\n}","preventionTips":["Make the kernel's session writes atomic (temp file + rename) to avoid truncation","Do not hand-edit files under the sessions directory","Keep backups/sync history for the AI sessions directory","Monitor disk-full conditions on the workspace volume"],"tags":["go","agent","json","corruption","file-read"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}