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

  1. Inspect the in-memory/serialized value of local-ai and replace non-serializable entries (NaN, complex, chan) with plain JSON types
  2. If local.json on disk is corrupt, restore it from backup or delete the local-ai key
  3. 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

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


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