sipeed/picoclaw · error

failed to close tts audio file: %w

Error message

failed to close tts audio file: %w

What it means

file.Close() returned an error at pkg/audio/tts/tts.go:155 after io.Copy succeeded. On Go, deferred buffer flushing happens at close, so this usually means the final flush failed: ENOSPC (disk filled exactly at the end), NFS write-back error, or storage backend fault. The removeTemp defer then deletes the temp file, so the failed audio does not linger.

Source

Thrown at pkg/audio/tts/tts.go:155

		return "", fmt.Errorf("failed to create temp file: %w", err)
	}

	removeTemp := true
	defer func() {
		if removeTemp {
			_ = os.Remove(file.Name())
		}
	}()

	_, err = io.Copy(file, stream)
	if err != nil {
		_ = file.Close()
		return "", fmt.Errorf("failed to write tts audio: %w", err)
	}

	err = file.Close()
	if err != nil {
		return "", fmt.Errorf("failed to close tts audio file: %w", err)
	}

	filename = strings.TrimSpace(filename)
	if filename == "" {
		filename = fmt.Sprintf("tts-%d%s", time.Now().Unix(), fileExt)
	}

	ext := strings.ToLower(filepath.Ext(filename))
	if ext == "" {
		filename += fileExt
	} else if ext != fileExt {
		filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + fileExt
	}

	scope := fmt.Sprintf("tool:send_tts:%s:%s:%d", channel, chatID, time.Now().UnixNano())
	ref, err := store.Store(file.Name(), media.MediaMeta{
		Filename:    filename,
		ContentType: contentType,

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check disk space/quota for the media temp dir filesystem: df -h <dir>
  2. If the temp dir is on NFS/network storage, move it to local disk — close-time EIO is a classic NFS symptom
  3. Free space and retry; the temp file is auto-removed so nothing to clean up
  4. If it recurs with plenty of space, capture the errno from the wrapped error and inspect storage health (dmesg for I/O errors)

Example fix

# before: media temp dir on NFS, close() flush fails with EIO
# config: temp_dir = "/mnt/nfs/media"

# after: spool locally, let the store handle remote persistence
# config: temp_dir = "/var/tmp/picoclaw-media"
Defensive patterns

Strategy: try-catch

Validate before calling

func scratchDirHealthy() error {
    var stat syscall.Statfs_t
    if err := syscall.Statfs(media.TempDir(), &stat); err != nil {
        return err
    }
    if stat.Bavail*uint64(stat.Bsize) < 50*1024*1024 {
        return fmt.Errorf("less than 50MB free on temp dir volume")
    }
    return nil
}

Try / catch

if _, err := tts.SynthesizeToStore(ctx, text, ch, chatID, name); err != nil {
    if strings.Contains(err.Error(), "failed to close tts audio file") {
        // final flush failed: almost always ENOSPC or storage fault
        freeDiskSpace(); return err // temp file is auto-removed by the lib
    }
    return err
}

Prevention

When it happens

Trigger: Disk becoming full between the copy finishing and close flushing buffered blocks; NFS/overlayfs returning EIO on final flush; the underlying file already invalidated by the OS (forced unmount, quota enforcement).

Common situations: Containers with size-capped writable layers (device-mapper quota) filling mid-request; network filesystems for the media temp dir; quota limits hit on shared storage.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/6e5b406103857122. Report an issue: GitHub.