{"record":{"id":"6e5b406103857122","repo":"sipeed/picoclaw","slug":"failed-to-close-tts-audio-file-w","errorCode":null,"errorMessage":"failed to close tts audio file: %w","messagePattern":"failed to close tts audio file: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/audio/tts/tts.go","lineNumber":155,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\n\tremoveTemp := true\n\tdefer func() {\n\t\tif removeTemp {\n\t\t\t_ = os.Remove(file.Name())\n\t\t}\n\t}()\n\n\t_, err = io.Copy(file, stream)\n\tif err != nil {\n\t\t_ = file.Close()\n\t\treturn \"\", fmt.Errorf(\"failed to write tts audio: %w\", err)\n\t}\n\n\terr = file.Close()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to close tts audio file: %w\", err)\n\t}\n\n\tfilename = strings.TrimSpace(filename)\n\tif filename == \"\" {\n\t\tfilename = fmt.Sprintf(\"tts-%d%s\", time.Now().Unix(), fileExt)\n\t}\n\n\text := strings.ToLower(filepath.Ext(filename))\n\tif ext == \"\" {\n\t\tfilename += fileExt\n\t} else if ext != fileExt {\n\t\tfilename = strings.TrimSuffix(filename, filepath.Ext(filename)) + fileExt\n\t}\n\n\tscope := fmt.Sprintf(\"tool:send_tts:%s:%s:%d\", channel, chatID, time.Now().UnixNano())\n\tref, err := store.Store(file.Name(), media.MediaMeta{\n\t\tFilename:    filename,\n\t\tContentType: contentType,","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/audio/tts/tts.go#L137-L173","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Check disk space/quota for the media temp dir filesystem: df -h <dir>","If the temp dir is on NFS/network storage, move it to local disk — close-time EIO is a classic NFS symptom","Free space and retry; the temp file is auto-removed so nothing to clean up","If it recurs with plenty of space, capture the errno from the wrapped error and inspect storage health (dmesg for I/O errors)"],"exampleFix":"# before: media temp dir on NFS, close() flush fails with EIO\n# config: temp_dir = \"/mnt/nfs/media\"\n\n# after: spool locally, let the store handle remote persistence\n# config: temp_dir = \"/var/tmp/picoclaw-media\"","handlingStrategy":"try-catch","validationCode":"func scratchDirHealthy() error {\n    var stat syscall.Statfs_t\n    if err := syscall.Statfs(media.TempDir(), &stat); err != nil {\n        return err\n    }\n    if stat.Bavail*uint64(stat.Bsize) < 50*1024*1024 {\n        return fmt.Errorf(\"less than 50MB free on temp dir volume\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if _, err := tts.SynthesizeToStore(ctx, text, ch, chatID, name); err != nil {\n    if strings.Contains(err.Error(), \"failed to close tts audio file\") {\n        // final flush failed: almost always ENOSPC or storage fault\n        freeDiskSpace(); return err // temp file is auto-removed by the lib\n    }\n    return err\n}","preventionTips":["Keep the media temp dir on local disk, not NFS","Alert on <5% free space on the temp volume","Treat close-after-copy errors as storage faults, not transient network noise"],"tags":["go","filesystem","tts","io","nfs"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}