vxcontrol/pentagi · error

failed to commit blob %s: %w

Error message

failed to commit blob %s: %w

What it means

CommitBlob atomically moves the temp file to its content-addressed .blob destination with os.Rename; this error wraps a rename failure for the given hash. Renames fail across filesystem boundaries (EXDEV) or on permission/existence conflicts.

Source

Thrown at backend/pkg/resources/resources.go:318

// CommitBlob atomically moves tmpPath to the .blob destination for hash.  If
// the blob already exists (race with concurrent upload of identical file) the
// tmp file is removed and no error is returned.
func CommitBlob(dataDir, hash, tmpPath string) error {
	if err := validateBlobHash(hash); err != nil {
		return err
	}
	if err := EnsureResourcesDir(dataDir); err != nil {
		return err
	}

	dest := BlobPath(dataDir, hash)
	if _, err := os.Lstat(dest); err == nil {
		// Already exists — remove tmp and consider success.
		os.Remove(tmpPath)
		return nil
	}
	if err := os.Rename(tmpPath, dest); err != nil {
		return fmt.Errorf("failed to commit blob %s: %w", hash, err)
	}
	return nil
}

// ZipResources writes a ZIP archive to w containing all entries in files.
// Each ZipEntry maps a .blob file on disk to a path inside the archive.
func ZipResources(w io.Writer, entries []ZipEntry) (err error) {
	// The streaming HTTP caller commits its 200 status on the first byte written,
	// so a missing blob must be caught before then, or the client gets a truncated
	// archive under 200. Stat all blobs up front; don't fold into the write loop.
	for _, e := range entries {
		if _, statErr := os.Lstat(e.BlobPath); statErr != nil {
			return fmt.Errorf("failed to stat blob %s: %w", e.BlobPath, statErr)
		}
	}

	zw := zip.NewWriter(w)
	defer func() {

View on GitHub (pinned to ea665308ba)

Solutions

  1. Ensure the temp dir and blob dir reside on the same filesystem/mount so os.Rename works
  2. If cross-device is unavoidable, fall back to copy-then-delete instead of rename
  3. Check write permission on the blob destination directory for the service user
  4. Check for concurrent jobs removing the blob directory during uploads

Example fix

// before
# temp on tmpfs, blobs on volume → EXDEV
volumes: ["/tmp/pentagi-tmp:/tmp:rw", "blobdata:/data/blobs"]
// after
# both on the same volume
volumes: ["blobdata:/data"]  # tmp = /data/tmp, blobs = /data/blobs
Defensive patterns

Strategy: try-catch

Validate before calling

func sameFilesystem(a, b string) (bool, error) {
	var sa, sb syscall.Stat_t
	if err := syscall.Stat(a, &sa); err != nil {
		return false, err
	}
	if err := syscall.Stat(b, &sb); err != nil {
		return false, err
	}
	return sa.Dev == sb.Dev, nil
}

Try / catch

err := resources.CommitBlob(tmpPath, hash)
if err != nil {
	var linkErr *os.LinkError
	if errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EXDEV) {
		// fallback: copy then remove
		dest := filepath.Join(blobRoot, hash)
		if cerr := copyFile(tmpPath, dest); cerr == nil {
			os.Remove(tmpPath)
			return nil
		}
	}
	return err
}

Prevention

When it happens

Trigger: Calling CommitBlob (via UploadResources or promoteToResources) where temp dir and blob dir are on different devices (rename returns 'invalid cross-device link'); destination parent missing or unwritable; concurrent cleanup removing the destination directory.

Common situations: Temp dir configured on tmpfs while blobs live on a mounted volume — the classic EXDEV case; container volume remounted read-only; blob directory deleted by a cleanup job mid-upload.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/d346661a67d88720. Report an issue: GitHub.