siyuan-note/siyuan · error

cannot allocate a replacement history directory

Error message

cannot allocate a replacement history directory

What it means

Before applying a relink, the planner creates a timestamped backup directory under the history dir named <time>-<op>. It tries up to 1000 successive seconds for a free name; if every Mkdir collides with an existing directory (or clock anomalies make all names taken), it gives up with this error rather than risk overwriting an existing history entry.

Solutions

  1. Clean up old/obsolete entries in data/history/ (or archive them elsewhere)
  2. Correct the system clock (NTP sync) if it is skewed relative to existing history dirs
  3. Check for existing dirs matching 2006-01-02-150405-replace and remove future-dated collisions
  4. Retry after freeing/normalizing the history directory

Example fix

// before
// data/history contains 2026-09-18-120000-replace ... many future dirs
// after
mv data/history/2026-09-18-120000-replace /archive/history-backup/
// re-run the relink apply
Defensive patterns

Strategy: retry

Validate before calling

// pre-check history dir for colliding future-dated replace dirs
pattern := time.Now().Format("2006-01-02")
matches, _ := filepath.Glob(filepath.Join(historyDir, pattern+"*-replace"))
if len(matches) > 900 {
	// archive/clean old history entries first
}

Try / catch

// retry after cleaning the history directory
if err := plan.Apply(); err != nil {
	if err.Error() == "cannot allocate a replacement history directory" {
		// archive old data/history entries, fix clock, then retry once
	}
	return err
}

Prevention

When it happens

Trigger: newAssetRelinkHistoryDir is called from apply; all 1000 candidate timestamp directories already exist in data/history/ — e.g. thousands of replace operations created dirs within the same second-range, or clock skew keeps regenerating identical names that pre-exist.

Common situations: System clock set far ahead so future-timestamped dirs already exist; history dir polluted by many manual/test entries; the same relink applied repeatedly across clock jumps.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/asset_relink.go:479

		}
	}
	p.result.References = append(p.result.References, ref)
	return nil
}

func newAssetRelinkHistoryDir() (string, error) {
	if err := os.MkdirAll(util.HistoryDir, 0755); err != nil {
		return "", err
	}
	for offset := 0; offset < 1000; offset++ {
		dir := filepath.Join(util.HistoryDir, time.Now().Add(time.Duration(offset)*time.Second).Format("2006-01-02-150405")+"-"+HistoryOpReplace)
		if err := os.Mkdir(dir, 0755); err == nil {
			return dir, nil
		} else if !os.IsExist(err) {
			return "", err
		}
	}
	return "", errors.New("cannot allocate a replacement history directory")
}

func validateRelinkStoragePath(abs string) error {
	real, err := filepath.EvalSymlinks(abs)
	if err != nil {
		return err
	}
	dataRoot, err := filepath.EvalSymlinks(util.DataDir)
	if err != nil || !gulu.File.IsSubPath(dataRoot, real) {
		return fmt.Errorf("resource escapes the data directory: %s", abs)
	}
	rel, err := filepath.Rel(dataRoot, real)
	if err != nil {
		return err
	}
	first, _, _ := strings.Cut(filepath.ToSlash(rel), "/")
	if ast.IsNodeIDPattern(first) && IsEncryptedBox(first) {
		return errors.New("encrypted resources are not supported")

View on GitHub (pinned to 9f775e8a12)