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

  1. Deduplicate the notebookSorts list by ID before calling the API
  2. When building a new order, replace the existing entry for an ID instead of appending a second one
  3. 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

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


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, &notebookSortPlan{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)