siyuan-note/siyuan · error
decode session data failed: %w
Error message
decode session data failed: %w
What it means
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.
Source
Thrown at kernel/agent/session.go:333
revision, _, err := SaveSessionState(data)
return revision, err
}
func SaveSessionState(data []byte) (int64, map[string]any, error) {
var meta sessionMeta
if err := gulu.JSON.UnmarshalJSON(data, &meta); err != nil || meta.ID == "" || !isValidSessionID(meta.ID) {
return 0, nil, fmt.Errorf("invalid session data")
}
lock := sessionLock(meta.ID)
lock.Lock()
defer lock.Unlock()
dir := filepath.Join(sessionsDir(), meta.ID)
path := filepath.Join(dir, "session.json")
var newData map[string]any
if err := gulu.JSON.UnmarshalJSON(data, &newData); err != nil {
return 0, nil, fmt.Errorf("decode session data failed: %w", err)
}
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]anyView on GitHub (pinned to afa823b6b4)
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
Defensive patterns
Strategy: validation
Validate before calling
// Round-trip check before sending: if it re-parses, both kernel decodes will succeed
try { JSON.parse(JSON.stringify(payload)); } catch { throw new Error('payload is not clean JSON'); } Type guard
null
Try / catch
null
Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- invalid session data
- encode session data failed: %w
- agent session revision conflict
- invalid session id
- decode existing session data failed: %w
AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18).
Data as JSON: /api/errors/ea1696ccf17c89a8.
Report an issue: GitHub.