siyuan-note/siyuan · error
duplicate notebook ID
Error message
duplicate notebook ID [%s]
What it means
Notebook sort planning rejects duplicate IDs: if the same notebook ID appears more than once in notebookSorts, the second occurrence fails with "duplicate notebook ID [%s]". A notebook can occupy exactly one position in an order, so duplicates represent a malformed request that would produce an ambiguous ordering.
Solutions
- Deduplicate the notebookSorts list by ID before calling the API
- When building a new order, replace the existing entry for an ID instead of appending a second one
- Log and inspect the request body to find which ID repeats
Example fix
// before
customSortNotebooks(append(persistedOrder, newItem))
// after: replace-or-append by ID
merged := map[string]*SortItem{}
for _, it := range persistedOrder { merged[it.ID] = it }
merged[newItem.ID] = newItem
out := make([]*SortItem, 0, len(merged))
for _, it := range merged { out = append(out, it) }
customSortNotebooks(out) Defensive patterns
Strategy: validation
Validate before calling
const ids = notebookSorts.map(i => i.id);
if (new Set(ids).size !== ids.length) throw new Error('duplicate notebook IDs in sort payload'); Type guard
const isUnique = (items) => new Set(items.map(i => i.id)).size === items.length;
Try / catch
try { await api.sortNotebooks(items); } catch (e) { if (String(e).includes('duplicate notebook ID')) { await api.sortNotebooks(dedupeById(items)); } else { throw e; } } Prevention
- Dedupe by ID when merging persisted and new order lists
- Replace existing entries by ID rather than appending
- Add a unit test asserting sort payloads have unique IDs
When it happens
Trigger: Sending the notebook sort API a list containing the same notebook ID twice, e.g. merging two order lists without deduplication, or double-appending in plugin code.
Common situations: Plugin concatenating persisted order with a new item already present; copy-paste building of sort payloads; frontend drag handler appending the item without removing it from its old slot.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- source ID [ ] is not in the current order
- target ID [ ] is not in the current order
- all asset mappings failed
- assetPath is not an image referenced by the document
- assetPath is required for analyze
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/747d0456f2a758ce.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/file.go:2912
fileTreeSortLock.Lock()
defer fileTreeSortLock.Unlock()
boxes := map[string]*Box{}
for _, box := range Conf.GetBoxes() {
boxes[box.ID] = box
}
openedBoxes := map[string]*Box{}
for _, box := range Conf.GetOpenedBoxes() {
openedBoxes[box.ID] = box
}
notebookPlans := make([]*notebookSortPlan, 0, len(notebookSorts))
notebookIDs := map[string]struct{}{}
for _, item := range notebookSorts {
if nil == item {
return ret, errors.New("notebook sort item must not be nil")
}
if _, ok := notebookIDs[item.ID]; ok {
return ret, fmt.Errorf("duplicate notebook ID [%s]", item.ID)
}
notebookIDs[item.ID] = struct{}{}
box := boxes[item.ID]
if nil == box {
return ret, fmt.Errorf("notebook [%s] not found", item.ID)
}
notebookPlans = append(notebookPlans, ¬ebookSortPlan{item: item, box: box})
}
docPlans := make([]*docSortPlan, 0, len(docSorts))
docIDs := map[string]struct{}{}
for _, item := range docSorts {
if nil == item {
return ret, errors.New("document sort item must not be nil")
}
if _, ok := docIDs[item.ID]; ok {
return ret, fmt.Errorf("duplicate document ID [%s]", item.ID)View on GitHub (pinned to 9f775e8a12)