{"record":{"id":"71618d4f4ea2a5d1","repo":"siyuan-note/siyuan","slug":"marshal-legacy-ai-editor-actions-failed-w","errorCode":null,"errorMessage":"marshal legacy AI editor actions failed: %w","messagePattern":"marshal legacy AI editor actions failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/ai_editor.go","lineNumber":220,"sourceCode":"\treturn nil\n}\n\nfunc migrateLegacyAIEditorActions(data *aiEditorActionsData, persist bool) (err error) {\n\tlocalStorage := GetLocalStorage()\n\tlegacyRaw, ok := localStorage[legacyAIEditorActionsStorageKey]\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tvar legacyActions []*legacyAIEditorAction\n\tif legacyJSON, isString := legacyRaw.(string); isString {\n\t\tif err = gulu.JSON.UnmarshalJSON([]byte(legacyJSON), &legacyActions); err != nil {\n\t\t\treturn fmt.Errorf(\"unmarshal legacy AI editor actions failed: %w\", err)\n\t\t}\n\t} else {\n\t\tlegacyJSON, marshalErr := gulu.JSON.MarshalJSON(legacyRaw)\n\t\tif marshalErr != nil {\n\t\t\treturn fmt.Errorf(\"marshal legacy AI editor actions failed: %w\", marshalErr)\n\t\t}\n\t\tif err = gulu.JSON.UnmarshalJSON(legacyJSON, &legacyActions); err != nil {\n\t\t\treturn fmt.Errorf(\"unmarshal legacy AI editor actions failed: %w\", err)\n\t\t}\n\t}\n\n\ttype actionKey struct {\n\t\tname   string\n\t\taction string\n\t}\n\texisting := make(map[actionKey]struct{}, len(data.Actions))\n\tfor _, action := range data.Actions {\n\t\texisting[actionKey{name: action.Name, action: action.Action}] = struct{}{}\n\t}\n\n\tchanged := false\n\tfor _, legacyAction := range legacyActions {\n\t\tif legacyAction == nil || (legacyAction.Name == \"\" && legacyAction.Memo == \"\") {","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/model/ai_editor.go#L202-L238","documentation":"The localStorage value under \"local-ai\" is NOT a string, so migration re-marshals it with gulu.JSON.MarshalJSON before decoding, and that marshal failed. Go's encoding/json only fails on truly unmarshalable values (NaN/Inf floats, channels, funcs, cyclic structures), which cannot normally appear in JSON-parsed local.json — this fires only when the map was populated programmatically with such a value or the map is cyclic.","triggerScenarios":"In practice unreachable via on-disk local.json (JSON cannot contain those types); it can fire in tests or plugins that inject an invalid any value into the localStorage map via SetLocalStorageVals before calling GetAIEditorActions.","commonSituations":"Unit tests constructing localStorage maps with float64(math.NaN()) or non-serializable types under the \"local-ai\" key; in-memory mutation between GetLocalStorage() and the marshal.","solutions":["Inspect the in-memory/serialized value of local-ai and replace non-serializable entries (NaN, complex, chan) with plain JSON types","If local.json on disk is corrupt, restore it from backup or delete the local-ai key","Re-run the action that triggered migration (Get/Save AI editor actions) to confirm it now succeeds"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Reject non-JSON-serializable values before they can reach migration\nfunc assertSerializable(v any) bool { _, err := json.Marshal(v); return err == nil }","typeGuard":null,"tryCatchPattern":"if err := model.SetLocalStorageVals(vals); err != nil { return err } // surface at write time instead of during migration","preventionTips":["Only store plain JSON types (string/number/bool/map/slice) in localStorage","Marshal-check values in SetLocalStorageVals wrappers in tests"],"tags":["json","go","local-storage","migration","unreachable-code"],"backgroundTag":"json-marshal-unsupported-type","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}