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]any

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Log the exact body that triggered it and re-send it verbatim from a clean buffer
  2. If calling SaveSession from Go, marshal your map once with json.Marshal and pass that slice — never re-use a mutated buffer
  3. Verify no middleware/proxy rewrites the body between meta extraction and full read
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/ea1696ccf17c89a8. Report an issue: GitHub.