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
- Open actions.json, find the duplicated id, and give one copy a fresh unique ID (or delete the redundant entry)
- If unsure which to keep, delete the whole file — legacy actions are re-imported from localStorage "local-ai" and defaults regenerated
- 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 duplicating an entry to make a variant, always mint a new unique ID
- On import, strip IDs and let SaveAIEditorAction re-issue them
- Treat sync-merge duplicates as data bugs: re-id, never delete blindly
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
- unmarshal AI editor actions failed: %w
- invalid AI editor action data
- parse json [%s] to tree failed: %w
- unmarshal legacy AI editor actions failed: %w
- invalid session id
AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18).
Data as JSON: /api/errors/4a7374cf718462da.
Report an issue: GitHub.