@@ -170,30 +170,53 @@ function isTarEofRaceError(err: unknown): boolean {
170170
171171export type BackupTarRetryLogger = ( message : string ) => void ;
172172
173+ function resolveBackupTarAttemptTempPath ( tempArchivePath : string , attempt : number ) : string {
174+ return attempt === 1 ? tempArchivePath : `${ tempArchivePath } .retry-${ attempt } ` ;
175+ }
176+
177+ function resolveBackupTarAttemptTempPaths ( tempArchivePath : string ) : string [ ] {
178+ return Array . from ( { length : BACKUP_TAR_MAX_ATTEMPTS } , ( _value , index ) =>
179+ resolveBackupTarAttemptTempPath ( tempArchivePath , index + 1 ) ,
180+ ) ;
181+ }
182+
183+ async function removeBackupTempArchiveBestEffort ( tempArchivePath : string ) : Promise < void > {
184+ await fs . rm ( tempArchivePath , { force : true } ) . catch ( ( ) => undefined ) ;
185+ }
186+
173187async function writeTarArchiveWithRetry ( params : {
174188 tempArchivePath : string ;
175- runTar : ( ) => Promise < void > ;
189+ runTar : ( tempArchivePath : string ) => Promise < void > ;
176190 log ?: BackupTarRetryLogger ;
177191 sleepMs ?: ( ms : number ) => Promise < void > ;
178- } ) : Promise < void > {
192+ } ) : Promise < string > {
179193 const sleepFn = params . sleepMs ?? sleep ;
180194 let lastErr : unknown ;
195+ const attemptTempArchivePaths : string [ ] = [ ] ;
181196 for ( let attempt = 1 ; attempt <= BACKUP_TAR_MAX_ATTEMPTS ; attempt += 1 ) {
197+ const attemptTempArchivePath = resolveBackupTarAttemptTempPath ( params . tempArchivePath , attempt ) ;
198+ attemptTempArchivePaths . push ( attemptTempArchivePath ) ;
182199 try {
183- await params . runTar ( ) ;
184- return ;
200+ await params . runTar ( attemptTempArchivePath ) ;
201+ for ( const staleTempArchivePath of attemptTempArchivePaths . slice ( 0 , - 1 ) ) {
202+ await removeBackupTempArchiveBestEffort ( staleTempArchivePath ) ;
203+ }
204+ return attemptTempArchivePath ;
185205 } catch ( err ) {
186206 lastErr = err ;
187207 if ( ! isTarEofRaceError ( err ) || attempt === BACKUP_TAR_MAX_ATTEMPTS ) {
208+ for ( const staleTempArchivePath of attemptTempArchivePaths ) {
209+ await removeBackupTempArchiveBestEffort ( staleTempArchivePath ) ;
210+ }
188211 break ;
189212 }
190213 try {
191- await fs . rm ( params . tempArchivePath , { force : true } ) ;
214+ await fs . rm ( attemptTempArchivePath , { force : true } ) ;
192215 } catch ( cleanupErr ) {
193216 const code = ( cleanupErr as NodeJS . ErrnoException ) . code ;
194217 if ( code && code !== "ENOENT" ) {
195218 params . log ?.(
196- `Backup archiver could not remove temp archive ${ params . tempArchivePath } between retries: ${ code } . Continuing.` ,
219+ `Backup archiver could not remove temp archive ${ attemptTempArchivePath } between retries: ${ code } . Continuing.` ,
197220 ) ;
198221 }
199222 }
@@ -778,6 +801,7 @@ export async function createBackupArchive(
778801 const tempDir = await fs . mkdtemp ( path . join ( tempRoot , "openclaw-backup-" ) ) ;
779802 const manifestPath = path . join ( tempDir , "manifest.json" ) ;
780803 const tempArchivePath = buildTempArchivePath ( outputPath ) ;
804+ const tempArchiveCleanupPaths = resolveBackupTarAttemptTempPaths ( tempArchivePath ) ;
781805 const stateAsset = result . assets . find ( ( asset ) => asset . kind === "state" ) ;
782806 try {
783807 const stateSqliteBackup = stateAsset
@@ -855,18 +879,18 @@ export async function createBackupArchive(
855879 }
856880 return true ;
857881 } ;
858- await writeTarArchiveWithRetry ( {
882+ const completedTempArchivePath = await writeTarArchiveWithRetry ( {
859883 tempArchivePath,
860884 log : opts . log ,
861- runTar : async ( ) => {
885+ runTar : async ( attemptTempArchivePath ) => {
862886 // tar.c re-walks the tree (and thus re-invokes tarFilter) on every
863887 // attempt, so reset the closure counter here or retries would report
864888 // cumulative skip counts across attempts instead of the final one.
865889 skippedVolatileCount = 0 ;
866890 unexpectedSqliteSourcePaths . length = 0 ;
867891 await tar . c (
868892 {
869- file : tempArchivePath ,
893+ file : attemptTempArchivePath ,
870894 gzip : true ,
871895 portable : true ,
872896 preservePaths : true ,
@@ -904,9 +928,11 @@ export async function createBackupArchive(
904928 } (live sessions, cron logs, queues, sockets, pid/tmp).`,
905929 ) ;
906930 }
907- await publishTempArchive ( { tempArchivePath, outputPath } ) ;
931+ await publishTempArchive ( { tempArchivePath : completedTempArchivePath , outputPath } ) ;
908932 } finally {
909- await fs . rm ( tempArchivePath , { force : true } ) . catch ( ( ) => undefined ) ;
933+ for ( const cleanupPath of tempArchiveCleanupPaths ) {
934+ await removeBackupTempArchiveBestEffort ( cleanupPath ) ;
935+ }
910936 await fs . rm ( tempDir , { recursive : true , force : true } ) . catch ( ( ) => undefined ) ;
911937 }
912938
0 commit comments