siyuan-note/siyuan · error
marshal legacy AI editor actions failed: %w
Error message
marshal legacy AI editor actions failed: %w
What it means
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.
Source
Thrown at kernel/model/ai_editor.go:220
return nil
}
func migrateLegacyAIEditorActions(data *aiEditorActionsData, persist bool) (err error) {
localStorage := GetLocalStorage()
legacyRaw, ok := localStorage[legacyAIEditorActionsStorageKey]
if !ok {
return nil
}
var legacyActions []*legacyAIEditorAction
if legacyJSON, isString := legacyRaw.(string); isString {
if err = gulu.JSON.UnmarshalJSON([]byte(legacyJSON), &legacyActions); err != nil {
return fmt.Errorf("unmarshal legacy AI editor actions failed: %w", err)
}
} else {
legacyJSON, marshalErr := gulu.JSON.MarshalJSON(legacyRaw)
if marshalErr != nil {
return fmt.Errorf("marshal legacy AI editor actions failed: %w", marshalErr)
}
if err = gulu.JSON.UnmarshalJSON(legacyJSON, &legacyActions); err != nil {
return fmt.Errorf("unmarshal legacy AI editor actions failed: %w", err)
}
}
type actionKey struct {
name string
action string
}
existing := make(map[actionKey]struct{}, len(data.Actions))
for _, action := range data.Actions {
existing[actionKey{name: action.Name, action: action.Action}] = struct{}{}
}
changed := false
for _, legacyAction := range legacyActions {
if legacyAction == nil || (legacyAction.Name == "" && legacyAction.Memo == "") {View on GitHub (pinned to afa823b6b4)
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
Defensive patterns
Strategy: validation
Validate before calling
// Reject non-JSON-serializable values before they can reach migration
func assertSerializable(v any) bool { _, err := json.Marshal(v); return err == nil } Try / catch
if err := model.SetLocalStorageVals(vals); err != nil { return err } // surface at write time instead of during migration Prevention
- Only store plain JSON types (string/number/bool/map/slice) in localStorage
- Marshal-check values in SetLocalStorageVals wrappers in tests
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- unmarshal legacy AI editor actions failed: %w
- remove legacy AI editor actions failed: %w
- attribute view spec is too new
- master password migration is pending
- Conf.Language(317)
AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18).
Data as JSON: /api/errors/71618d4f4ea2a5d1.
Report an issue: GitHub.