siyuan-note/siyuan · error

ambiguous asset path

Error message

ambiguous asset path: %s

What it means

During asset relink preflight, resolveRelinkAsset checks each notebook's assets/ root for the referenced asset. If the same asset-relative path resolves to an existing regular file under more than one root and the candidates differ, the path is ambiguous — the relinker cannot know which file the reference means, so it aborts rather than silently relinking to the wrong file.

Solutions

  1. Rename the asset in one notebook so the relative path is unique per root
  2. Delete the duplicate copy that is not actually referenced and update references to point at the surviving asset
  3. Relink with a single asset root so the path can only resolve once
  4. If intentional sharing is needed, keep one canonical copy under one root and reference it via that root only

Example fix

// before (two roots both contain assets/foo.png)
resolveRelinkAsset([]string{nb1Assets, nb2Assets}, "assets/foo.png", true)
// error: ambiguous asset path: assets/foo.png
// after (rename one copy so each root has a unique path)
resolveRelinkAsset([]string{nb1Assets, nb2Assets}, "assets/foo.png", true) // nb1/assets/foo.png -> nb1/assets/foo-1.png
Defensive patterns

Strategy: validation

Validate before calling

// before relinking, ensure each referenced asset path resolves to exactly one root
func isUnambiguous(roots []string, assetPath string) bool {
	count := 0
	for _, root := range roots {
		if _, err := os.Stat(filepath.Join(root, strings.TrimPrefix(assetPath, "assets/"))); err == nil {
			count++
		}
	}
	return count <= 1
}

Prevention

When it happens

Trigger: An asset reference like 'assets/foo.png' exists in two different notebook asset roots (e.g. notebook1/assets/foo.png and notebook2/assets/foo.png), or in a notebook assets dir plus a shared/synced assets root, and resolveRelinkAsset is invoked with multiple roots by scanMetadata during a relink scan.

Common situations: The same file was copied into two notebooks' assets folders; synced workspaces merged duplicate assets; a shared asset root was added alongside notebook-local assets so both resolve the same relative path.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/c0ebcdca3a8c976b. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/asset_relink.go:269

		if !info.Mode().IsRegular() {
			return "", fmt.Errorf("asset must be a regular file: %s", assetPath)
		}
		real, err := filepath.EvalSymlinks(candidate)
		if err != nil {
			return "", err
		}
		realRoot, err := filepath.EvalSymlinks(root)
		if err != nil || !gulu.File.IsSubPath(realRoot, real) {
			return "", fmt.Errorf("asset escapes its directory: %s", assetPath)
		}
		if err = validateRelinkStoragePath(real); err != nil {
			return "", err
		}
		if IsEncryptedAssetPath(real) {
			return "", errors.New("encrypted assets are not supported")
		}
		if found != "" && found != candidate {
			return "", fmt.Errorf("ambiguous asset path: %s", assetPath)
		}
		found = candidate
	}
	if required && found == "" {
		return "", fmt.Errorf("target asset does not exist locally: %s", assetPath)
	}
	return found, nil
}

func (p *assetRelinkPlan) scanViews() error {
	dir := filepath.Join(util.DataDir, "storage", "av")
	if err := p.observe(dir); err != nil {
		return err
	}
	entries, err := os.ReadDir(dir)
	if err != nil && !os.IsNotExist(err) {
		return err
	}

View on GitHub (pinned to 9f775e8a12)