{"record":{"id":"ea1696ccf17c89a8","repo":"siyuan-note/siyuan","slug":"decode-session-data-failed-w","errorCode":null,"errorMessage":"decode session data failed: %w","messagePattern":"decode session data failed: %w","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"kernel/agent/session.go","lineNumber":333,"sourceCode":"\trevision, _, err := SaveSessionState(data)\n\treturn revision, err\n}\n\nfunc SaveSessionState(data []byte) (int64, map[string]any, error) {\n\tvar meta sessionMeta\n\tif err := gulu.JSON.UnmarshalJSON(data, &meta); err != nil || meta.ID == \"\" || !isValidSessionID(meta.ID) {\n\t\treturn 0, nil, fmt.Errorf(\"invalid session data\")\n\t}\n\tlock := sessionLock(meta.ID)\n\tlock.Lock()\n\tdefer lock.Unlock()\n\n\tdir := filepath.Join(sessionsDir(), meta.ID)\n\tpath := filepath.Join(dir, \"session.json\")\n\n\tvar newData map[string]any\n\tif err := gulu.JSON.UnmarshalJSON(data, &newData); err != nil {\n\t\treturn 0, nil, fmt.Errorf(\"decode session data failed: %w\", err)\n\t}\n\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","sourceCodeStart":315,"sourceCodeEnd":351,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/agent/session.go#L315-L351","documentation":"After the sessionMeta gate, SaveSessionState decodes the same body a second time into a generic map (kernel/agent/session.go:332) to manipulate fields (expectedRevision, commitTurnID, agentRunning etc. are stripped at lines 335-341). A failure there is wrapped as 'decode session data failed'. Because both decodes consume identical bytes, hitting this after passing the struct decode means the payload is malformed in a way the first decode tolerated — effectively a defensive guard against non-representable payloads.","triggerScenarios":"POST /api/ai/agent/saveSession where the body decodes into sessionMeta but not into map[string]any — e.g. a top-level JSON value with exotic number formats, trailing content after the object, or a body mutated between decodes (not possible over HTTP, but reachable for in-process callers passing re-used buffers).","commonSituations":"Almost never seen from HTTP clients; realistic for kernel-internal or plugin code that calls agent.SaveSession with a hand-built byte slice; also a symptom of memory corruption or a tampered request body in tests.","solutions":["Log the exact body that triggered it and re-send it verbatim from a clean buffer","If calling SaveSession from Go, marshal your map once with json.Marshal and pass that slice — never re-use a mutated buffer","Verify no middleware/proxy rewrites the body between meta extraction and full read","If reproducible, minimize the payload and report it — this path indicates a decoder discrepancy"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Round-trip check before sending: if it re-parses, both kernel decodes will succeed\ntry { JSON.parse(JSON.stringify(payload)); } catch { throw new Error('payload is not clean JSON'); }","typeGuard":"null","tryCatchPattern":"null","preventionTips":["For in-process Go callers, pass a freshly marshalled buffer, not a mutated or shared one","Avoid injecting non-JSON values into the payload map"],"tags":["agent","session","json","payload"],"backgroundTag":"json-decode-failed","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}