siyuan-note/siyuan · error

duplicate AI editor action ID [%s]

Error message

duplicate AI editor action ID [%s]

What it means

loadAIEditorActions (kernel/model/ai_editor.go:178) builds an ID set while scanning entries and rejects the file if two actions share the same ID. Duplicate IDs would make update/delete ambiguous (SaveAIEditorAction replaces the first match), so the loader refuses the whole store instead of picking a winner.

Source

Thrown at kernel/model/ai_editor.go:178

		return nil, fmt.Errorf("read AI editor actions failed: %w", err)
	}
	if err = gulu.JSON.UnmarshalJSON(data, ret); err != nil {
		return nil, fmt.Errorf("unmarshal AI editor actions failed: %w", err)
	}
	if ret.Version != aiEditorActionsVersion {
		return nil, fmt.Errorf("unsupported AI editor actions version [%d]", ret.Version)
	}
	if ret.Actions == nil {
		ret.Actions = []*AIEditorAction{}
	}

	ids := make(map[string]struct{}, len(ret.Actions))
	for _, action := range ret.Actions {
		if action == nil || !ast.IsNodeIDPattern(action.ID) || (action.Name == "" && action.Action == "") {
			return nil, errors.New("invalid AI editor action data")
		}
		if _, ok := ids[action.ID]; ok {
			return nil, fmt.Errorf("duplicate AI editor action ID [%s]", action.ID)
		}
		ids[action.ID] = struct{}{}
	}
	return ret, nil
}

func saveAIEditorActions(data *aiEditorActionsData) (err error) {
	data.Version = aiEditorActionsVersion
	if data.Actions == nil {
		data.Actions = []*AIEditorAction{}
	}

	dirPath := filepath.Dir(aiEditorActionsPath())
	if err = os.MkdirAll(dirPath, 0755); err != nil {
		return fmt.Errorf("create AI editor actions directory failed: %w", err)
	}
	bytes, err := gulu.JSON.MarshalIndentJSON(data, "", "  ")
	if err != nil {

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Open actions.json, find the duplicated id, and give one copy a fresh unique ID (or delete the redundant entry)
  2. If unsure which to keep, delete the whole file — legacy actions are re-imported from localStorage "local-ai" and defaults regenerated
  3. When exporting/importing actions, strip ids on import and let the server re-issue them

Example fix

// before: two entries with "id":"20240101120000-a1b2c3d"
// after: keep one; second becomes "id":"20240101120001-b2c3d4e" (any unique 14-digit-time + 7-char-suffix value)
Defensive patterns

Strategy: try-catch

Validate before calling

ids := map[string]bool{}
for _, a := range actions {
    if ids[a.ID] {
        return fmt.Errorf("duplicate id %s in import payload; dedupe first", a.ID)
    }
    ids[a.ID] = true
}

Try / catch

if err != nil && strings.HasPrefix(err.Error(), "duplicate AI editor action ID") {
    dedupeActionsFileByID() // keep first occurrence, re-id the rest
    actions, err = model.GetAIEditorActions()
}

Prevention

When it happens

Trigger: actions.json contains two objects with the same id — usually after a manual copy-paste of an entry, a sync/merge that duplicated a block, or two devices generating colliding hand-written IDs. The JSON parses and each entry is individually valid; only the cross-entry uniqueness check fails.

Common situations: Duplicating an action by copying its JSON to make a variant and forgetting to change the id; a cloud-sync merge duplicating lines; importing an action list that already contains the exported ID.

Related errors


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