@@ -24,6 +24,9 @@ import {
2424} from "./backup-create.js" ;
2525import { requireNodeSqlite } from "./node-sqlite.js" ;
2626
27+ const BACKUP_STAGING_OWNER_FILE = ".openclaw-backup-owner.json" ;
28+ const BACKUP_ARCHIVE_OWNER_SUFFIX = ".openclaw-owner.json" ;
29+
2730function makeResult ( overrides : Partial < BackupCreateResult > = { } ) : BackupCreateResult {
2831 return {
2932 createdAt : "2026-01-01T00:00:00.000Z" ,
@@ -1226,12 +1229,20 @@ describe("sweepStaleBackupArtifacts", () => {
12261229 await fs . rm ( testRoot , { recursive : true , force : true } ) . catch ( ( ) => undefined ) ;
12271230 } ) ;
12281231
1232+ async function markOld ( targetPath : string ) : Promise < void > {
1233+ const twoHoursAgo = Date . now ( ) - 2 * 60 * 60 * 1000 ;
1234+ await fs . utimes ( targetPath , new Date ( twoHoursAgo ) , new Date ( twoHoursAgo ) ) ;
1235+ }
1236+
1237+ async function writeOwner ( ownerPath : string , pid : number ) : Promise < void > {
1238+ await fs . writeFile ( ownerPath , `${ JSON . stringify ( { pid, startedAtMs : Date . now ( ) } ) } \n` , "utf8" ) ;
1239+ }
1240+
12291241 it ( "removes stale openclaw-backup-* directories from the selected temp root" , async ( ) => {
12301242 const staleDir = path . join ( tempRoot , "openclaw-backup-abc123" ) ;
12311243 await fs . mkdir ( staleDir ) ;
12321244 await fs . writeFile ( path . join ( staleDir , "manifest.json" ) , "{}" ) ;
1233- const twoHoursAgo = Date . now ( ) - 2 * 60 * 60 * 1000 ;
1234- await fs . utimes ( staleDir , new Date ( twoHoursAgo ) , new Date ( twoHoursAgo ) ) ;
1245+ await markOld ( staleDir ) ;
12351246
12361247 const entries = await fs . readdir ( tempRoot , { withFileTypes : true } ) ;
12371248 expect ( entries . some ( ( e ) => e . name === "openclaw-backup-abc123" ) ) . toBe ( true ) ;
@@ -1246,8 +1257,7 @@ describe("sweepStaleBackupArtifacts", () => {
12461257 const uuid = randomUUID ( ) ;
12471258 const tempFile = path . join ( outputDir , `backup.tar.gz.${ uuid } .tmp` ) ;
12481259 await fs . writeFile ( tempFile , "data" ) ;
1249- const twoHoursAgo = Date . now ( ) - 2 * 60 * 60 * 1000 ;
1250- await fs . utimes ( tempFile , new Date ( twoHoursAgo ) , new Date ( twoHoursAgo ) ) ;
1260+ await markOld ( tempFile ) ;
12511261
12521262 await sweepStaleBackupArtifacts ( { outputPath, tempRoot } ) ;
12531263
@@ -1276,8 +1286,7 @@ describe("sweepStaleBackupArtifacts", () => {
12761286 await fs . writeFile ( path . join ( outputDir , "unrelated.txt" ) , "keep me" ) ;
12771287 await fs . writeFile ( unrelatedArchiveTemp , "keep me" ) ;
12781288 await fs . mkdir ( path . join ( tempRoot , "some-other-dir" ) ) ;
1279- const twoHoursAgo = Date . now ( ) - 2 * 60 * 60 * 1000 ;
1280- await fs . utimes ( unrelatedArchiveTemp , new Date ( twoHoursAgo ) , new Date ( twoHoursAgo ) ) ;
1289+ await markOld ( unrelatedArchiveTemp ) ;
12811290
12821291 await sweepStaleBackupArtifacts ( { outputPath, tempRoot } ) ;
12831292
@@ -1286,4 +1295,60 @@ describe("sweepStaleBackupArtifacts", () => {
12861295 ) ;
12871296 expect ( await fs . readdir ( tempRoot ) ) . toContain ( "some-other-dir" ) ;
12881297 } ) ;
1298+
1299+ it ( "preserves stale artifacts while their owner process is still alive" , async ( ) => {
1300+ const staleDir = path . join ( tempRoot , "openclaw-backup-active" ) ;
1301+ await fs . mkdir ( staleDir ) ;
1302+ await fs . writeFile ( path . join ( staleDir , "manifest.json" ) , "{}" ) ;
1303+ await writeOwner ( path . join ( staleDir , BACKUP_STAGING_OWNER_FILE ) , process . pid ) ;
1304+ await markOld ( staleDir ) ;
1305+
1306+ const uuid = randomUUID ( ) ;
1307+ const tempFileName = `backup.tar.gz.${ uuid } .tmp` ;
1308+ const tempFile = path . join ( outputDir , tempFileName ) ;
1309+ await fs . writeFile ( tempFile , "data" ) ;
1310+ await writeOwner ( `${ tempFile } ${ BACKUP_ARCHIVE_OWNER_SUFFIX } ` , process . pid ) ;
1311+ await markOld ( tempFile ) ;
1312+
1313+ await sweepStaleBackupArtifacts ( { outputPath, tempRoot } ) ;
1314+
1315+ expect ( await fs . readdir ( tempRoot ) ) . toContain ( "openclaw-backup-active" ) ;
1316+ expect ( await fs . readdir ( outputDir ) ) . toEqual (
1317+ expect . arrayContaining ( [ tempFileName , `${ tempFileName } ${ BACKUP_ARCHIVE_OWNER_SUFFIX } ` ] ) ,
1318+ ) ;
1319+ } ) ;
1320+
1321+ it ( "removes stale artifacts and owner markers when their owner is not live" , async ( ) => {
1322+ const staleDir = path . join ( tempRoot , "openclaw-backup-dead-owner" ) ;
1323+ await fs . mkdir ( staleDir ) ;
1324+ await writeOwner ( path . join ( staleDir , BACKUP_STAGING_OWNER_FILE ) , - 1 ) ;
1325+ await markOld ( staleDir ) ;
1326+
1327+ const uuid = randomUUID ( ) ;
1328+ const tempFileName = `backup.tar.gz.${ uuid } .tmp` ;
1329+ const tempFile = path . join ( outputDir , tempFileName ) ;
1330+ const ownerFileName = `${ tempFileName } ${ BACKUP_ARCHIVE_OWNER_SUFFIX } ` ;
1331+ await fs . writeFile ( tempFile , "data" ) ;
1332+ await writeOwner ( path . join ( outputDir , ownerFileName ) , - 1 ) ;
1333+ await markOld ( tempFile ) ;
1334+
1335+ await sweepStaleBackupArtifacts ( { outputPath, tempRoot } ) ;
1336+
1337+ expect ( await fs . readdir ( tempRoot ) ) . not . toContain ( "openclaw-backup-dead-owner" ) ;
1338+ expect ( await fs . readdir ( outputDir ) ) . not . toEqual (
1339+ expect . arrayContaining ( [ tempFileName , ownerFileName ] ) ,
1340+ ) ;
1341+ } ) ;
1342+
1343+ it ( "preserves stale owner sidecars without archive files when their owner is still alive" , async ( ) => {
1344+ const uuid = randomUUID ( ) ;
1345+ const ownerFileName = `backup.tar.gz.${ uuid } .tmp${ BACKUP_ARCHIVE_OWNER_SUFFIX } ` ;
1346+ const ownerPath = path . join ( outputDir , ownerFileName ) ;
1347+ await writeOwner ( ownerPath , process . pid ) ;
1348+ await markOld ( ownerPath ) ;
1349+
1350+ await sweepStaleBackupArtifacts ( { outputPath, tempRoot } ) ;
1351+
1352+ expect ( await fs . readdir ( outputDir ) ) . toContain ( ownerFileName ) ;
1353+ } ) ;
12891354} ) ;
0 commit comments