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
- Ensure the temp dir and blob dir reside on the same filesystem/mount so os.Rename works
- If cross-device is unavoidable, fall back to copy-then-delete instead of rename
- Check write permission on the blob destination directory for the service user
- 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
- Keep the temp dir and blob store on the same mount/volume so os.Rename stays atomic
- Check rename errors specifically for EXDEV and implement a copy fallback
- Ensure the blob destination directory exists and is writable before committing
- Avoid cleanup jobs racing with blob commits on the same directory
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
- failed to stat blob %s: %w
- blob for resource %q not found on disk
- failed to get flow screenshot: %w
- failed to get absolute path: %w
- failed to create tmp directory: %w
AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01).
Data as JSON: /api/errors/d346661a67d88720.
Report an issue: GitHub.