siyuan-note/siyuan · error

duplicate document ID [%s]

Error message

duplicate document ID [%s]

What it means

Thrown by SetFileTreeSort when the docSorts slice contains two entries with the same document block ID. The function builds a docIDs deduplication map; a repeat ID is rejected before any sort configuration is persisted. This mirrors the notebook deduplication check but for the document level.

Source

Thrown at kernel/model/file.go:2606

			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)
		}
		docIDs[item.ID] = struct{}{}

		bt := treenode.GetBlockTree(item.ID)
		if nil == bt || nil == openedBoxes[bt.BoxID] {
			return ret, fmt.Errorf("document [%s] not found in opened and unlocked notebooks", item.ID)
		}
		if bt.ID != bt.RootID || "d" != bt.Type || IsBoxDoc(bt.BoxID, bt.RootID) {
			return ret, fmt.Errorf("block [%s] is not a sortable document", item.ID)
		}
		if nil == boxes[bt.BoxID] {
			return ret, fmt.Errorf("notebook [%s] not found for document [%s]", bt.BoxID, item.ID)
		}
		docPlans = append(docPlans, &docSortPlan{item: item, boxID: bt.BoxID, parentPath: path.Dir(bt.Path)})
	}

	docGroups := map[string]*docSortGroup{}
	for _, plan := range docPlans {

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Deduplicate the docSorts array by item.ID on the client side before sending the request.
  2. Ensure the drag-and-drop handler removes the source document from its old sort position before adding the new entry.
  3. Inspect the POST /api/filetree/setFileTreeSort payload in the browser network tab to identify the duplicated document ID.

Example fix

// before
payload = { notebookSorts: [], docSorts: [...staleDocs, ...movedDocs] }

// after
const seen = new Set()
payload = {
  notebookSorts: [],
  docSorts: [...staleDocs, ...movedDocs]
    .filter(item => {
      if (seen.has(item.id)) return false
      seen.add(item.id)
      return true
    })
}
Defensive patterns

Strategy: validation

Validate before calling

// Deduplicate docSorts by ID before calling setFileTreeSort
function dedupeSortItems(items) {
  const seen = new Set()
  return items.filter(item => {
    if (seen.has(item.id)) return false
    seen.add(item.id)
    return true
  })
}
const cleanDocSorts = dedupeSortItems(docSorts)

Prevention

When it happens

Trigger: Calling POST /api/filetree/setFileTreeSort with a docSorts array where two SortItem elements share the same id field. Typically caused by a UI drag-and-drop handler that appends a new position without removing the old entry for the same document.

Common situations: File tree drag-and-drop where the moved document appears in both its old and new position in the payload; an external API client merging multiple sort sources without deduplication; concurrent edits from two tabs that both include the same document ID.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/eb054120a3c16473. Report an issue: GitHub.