@@ -126,25 +126,51 @@ export class RepoStorageBackend implements StorageBackend {
126126 }
127127
128128 public async updateFiles ( files : Record < string , string | null > ) : Promise < void > {
129+ const fileCount = Object . keys ( files ) . length ;
130+ const startTime = Date . now ( ) ;
131+
129132 // Get current HEAD commit SHA
130133 const headSha = await this . getHeadSha ( ) ;
131134
132135 // Get current tree
133136 const currentTree = await this . getTree ( headSha ) ;
134137
135- // Build new tree entries
138+ // Build new tree entries (includes blob creation)
139+ const blobStart = Date . now ( ) ;
136140 const treeEntries = await this . buildTreeEntries ( files , currentTree ) ;
141+ const blobDuration = Date . now ( ) - blobStart ;
142+ this . logProgress ( `Created ${ String ( treeEntries . length ) } blobs in ${ String ( blobDuration ) } ms` ) ;
137143
138144 // Create new tree
139145 const newTreeSha = await this . createTree ( treeEntries , currentTree . sha ) ;
140146
141147 // Create commit
142- const fileCount = String ( Object . keys ( files ) . length ) ;
143- const message = `Sync update: ${ fileCount } files` ;
148+ const message = `Sync update: ${ String ( fileCount ) } files` ;
144149 const commitSha = await this . createCommit ( message , newTreeSha , headSha ) ;
145150
146151 // Update HEAD ref
147152 await this . updateRef ( commitSha ) ;
153+
154+ const totalDuration = Date . now ( ) - startTime ;
155+ this . logProgress ( `Upload complete: ${ String ( fileCount ) } files in ${ String ( totalDuration ) } ms` ) ;
156+ }
157+
158+ /** Log progress for debugging */
159+ private logProgress ( message : string ) : void {
160+ try {
161+ // Use dynamic require to avoid module resolution issues
162+ /* eslint-disable @typescript-eslint/no-require-imports */
163+ const os = require ( 'node:os' ) as { homedir : ( ) => string } ;
164+ const fs = require ( 'node:fs' ) as { appendFileSync : ( path : string , data : string ) => void } ;
165+ const path = require ( 'node:path' ) as { join : ( ...parts : string [ ] ) => string } ;
166+ /* eslint-enable @typescript-eslint/no-require-imports */
167+ const logDir = path . join ( os . homedir ( ) , '.local/share/opencode/log' ) ;
168+ const logFile = path . join ( logDir , 'opencode-sync.log' ) ;
169+ const timestamp = new Date ( ) . toISOString ( ) ;
170+ fs . appendFileSync ( logFile , `${ timestamp } [opencode-sync] [REPO] ${ message } \n` ) ;
171+ } catch {
172+ // Ignore logging errors
173+ }
148174 }
149175
150176 public async listFiles ( ) : Promise < StorageFile [ ] > {
@@ -329,28 +355,68 @@ export class RepoStorageBackend implements StorageBackend {
329355 . filter ( ( e ) => ! updatedPaths . has ( e . path ) )
330356 . map ( ( e ) => ( { path : e . path , mode : e . mode , type : e . type , sha : e . sha } ) ) ;
331357
332- // Add new/updated files
358+ // Collect files that need blob creation
359+ const filesToUpload : { path : string ; content : string } [ ] = [ ] ;
333360 for ( const [ path , content ] of Object . entries ( files ) ) {
334- const fullPath = `${ SYNC_DIR } /${ path } ` ;
335-
336- if ( content === null ) {
337- // File deletion - already excluded from entries
338- continue ;
361+ if ( content !== null ) {
362+ filesToUpload . push ( { path : `${ SYNC_DIR } /${ path } ` , content } ) ;
339363 }
364+ }
340365
341- // Create blob for new content
342- const blobSha = await this . createBlob ( content ) ;
366+ // Create blobs in small parallel batches with delays to avoid secondary rate limits
367+ const BATCH_SIZE = 5 ;
368+ const blobResults = await this . createBlobsInBatches ( filesToUpload , BATCH_SIZE ) ;
369+
370+ // Add new entries from batch results
371+ for ( const result of blobResults ) {
343372 entries . push ( {
344- path : fullPath ,
373+ path : result . path ,
345374 mode : '100644' ,
346375 type : 'blob' ,
347- sha : blobSha ,
376+ sha : result . sha ,
348377 } ) ;
349378 }
350379
351380 return entries ;
352381 }
353382
383+ /**
384+ * Create blobs in parallel batches with rate limit protection.
385+ * Uses small batches with delays to avoid GitHub's secondary rate limits.
386+ */
387+ private async createBlobsInBatches (
388+ files : { path : string ; content : string } [ ] ,
389+ batchSize : number
390+ ) : Promise < { path : string ; sha : string } [ ] > {
391+ const results : { path : string ; sha : string } [ ] = [ ] ;
392+ const totalBatches = Math . ceil ( files . length / batchSize ) ;
393+
394+ for ( let i = 0 ; i < files . length ; i += batchSize ) {
395+ const batchNum = Math . floor ( i / batchSize ) + 1 ;
396+ const batch = files . slice ( i , i + batchSize ) ;
397+
398+ if ( files . length > batchSize ) {
399+ this . logProgress (
400+ `Batch ${ String ( batchNum ) } /${ String ( totalBatches ) } (${ String ( batch . length ) } files)`
401+ ) ;
402+ }
403+
404+ const batchPromises = batch . map ( async ( file ) => {
405+ const sha = await this . createBlob ( file . content ) ;
406+ return { path : file . path , sha } ;
407+ } ) ;
408+ const batchResults = await Promise . all ( batchPromises ) ;
409+ results . push ( ...batchResults ) ;
410+
411+ // Add delay between batches to avoid secondary rate limits
412+ if ( i + batchSize < files . length ) {
413+ await new Promise ( ( r ) => setTimeout ( r , 100 ) ) ;
414+ }
415+ }
416+
417+ return results ;
418+ }
419+
354420 private async createBlob ( content : string ) : Promise < string > {
355421 const body = JSON . stringify ( {
356422 content : Buffer . from ( content ) . toString ( 'base64' ) ,
0 commit comments