@@ -25,12 +25,28 @@ const limit = 2 * 1024 * 1024
2525const core = [ "-c" , "core.longpaths=true" , "-c" , "core.symlinks=true" ]
2626const cfg = [ "-c" , "core.autocrlf=false" , ...core ]
2727const quote = [ ...cfg , "-c" , "core.quotepath=false" ]
28+ const retryDelay = Duration . millis ( 500 )
29+ const circuitThreshold = 3
2830interface GitResult {
2931 readonly code : ChildProcessSpawner . ExitCode
3032 readonly text : string
3133 readonly stderr : string
3234}
3335
36+ const transientPatterns : readonly string [ ] = [
37+ "paging file" ,
38+ "out of memory" ,
39+ "malloc failed" ,
40+ "resource temporarily unavailable" ,
41+ "spawn enomem" ,
42+ "cannot allocate memory" ,
43+ ]
44+
45+ export function isTransientGitError ( stderr : string ) : boolean {
46+ const lower = stderr . toLowerCase ( )
47+ return transientPatterns . some ( ( pattern ) => lower . includes ( pattern ) )
48+ }
49+
3450type State = Omit < Interface , "init" >
3551
3652export interface Interface {
@@ -70,33 +86,69 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
7086 worktree : ctx . worktree ,
7187 gitdir : path . join ( Global . Path . data , "snapshot" , ctx . project . id , Hash . fast ( ctx . worktree ) ) ,
7288 vcs : ctx . project . vcs ,
89+ consecutiveFailures : 0 ,
90+ tripped : false ,
7391 }
7492
7593 const args = ( cmd : string [ ] ) => [ "--git-dir" , state . gitdir , "--work-tree" , state . worktree , ...cmd ]
7694
95+ const recordFailure = Effect . fnUntraced ( function * ( ) {
96+ state . consecutiveFailures += 1
97+ if ( state . consecutiveFailures >= circuitThreshold && ! state . tripped ) {
98+ state . tripped = true
99+ yield * Effect . logError ( "snapshot circuit breaker tripped — snapshots disabled until next success" , {
100+ consecutiveFailures : state . consecutiveFailures ,
101+ gitdir : state . gitdir ,
102+ } )
103+ }
104+ } )
105+
106+ const recordSuccess = Effect . fnUntraced ( function * ( ) {
107+ if ( state . tripped ) {
108+ state . tripped = false
109+ yield * Effect . logInfo ( "snapshot circuit breaker reset — snapshots re-enabled" , {
110+ gitdir : state . gitdir ,
111+ } )
112+ }
113+ state . consecutiveFailures = 0
114+ } )
115+
77116 const encodeNulTerminatedPaths = ( files : string [ ] ) => files . join ( "\0" ) + "\0"
78117 const encodeTopLevelLiteralPathspecs = ( files : string [ ] ) =>
79118 encodeNulTerminatedPaths ( files . map ( ( file ) => `:(top,literal)${ file } ` ) )
80119
120+ const execGit = ( cmd : string [ ] , opts ?: { cwd ?: string ; env ?: Record < string , string > ; stdin ?: string } ) =>
121+ appProcess
122+ . run ( ChildProcess . make ( "git" , cmd , { cwd : opts ?. cwd , env : opts ?. env , extendEnv : true } ) , {
123+ stdin : opts ?. stdin ,
124+ } )
125+ . pipe (
126+ Effect . map ( ( result ) => ( {
127+ code : ChildProcessSpawner . ExitCode ( result . exitCode ) ,
128+ text : result . stdout . toString ( "utf8" ) ,
129+ stderr : result . stderr . toString ( "utf8" ) ,
130+ } ) ) ,
131+ Effect . catch ( ( err ) =>
132+ Effect . succeed ( {
133+ code : ChildProcessSpawner . ExitCode ( 1 ) ,
134+ text : "" ,
135+ stderr : err instanceof Error ? err . message : String ( err ) ,
136+ } ) ,
137+ ) ,
138+ )
139+
81140 const git = Effect . fnUntraced (
82141 function * ( cmd : string [ ] , opts ?: { cwd ?: string ; env ?: Record < string , string > ; stdin ?: string } ) {
83- const result = yield * appProcess . run (
84- ChildProcess . make ( "git" , cmd , { cwd : opts ?. cwd , env : opts ?. env , extendEnv : true } ) ,
85- { stdin : opts ?. stdin } ,
86- )
87- return {
88- code : ChildProcessSpawner . ExitCode ( result . exitCode ) ,
89- text : result . stdout . toString ( "utf8" ) ,
90- stderr : result . stderr . toString ( "utf8" ) ,
91- } satisfies GitResult
142+ const first = yield * execGit ( cmd , opts )
143+ if ( first . code !== 0 && isTransientGitError ( first . stderr ) ) {
144+ yield * Effect . logWarning ( "snapshot git transient error, retrying" , {
145+ stderr : first . stderr ,
146+ } )
147+ yield * Effect . sleep ( retryDelay )
148+ return yield * execGit ( cmd , opts )
149+ }
150+ return first
92151 } ,
93- Effect . catch ( ( err ) =>
94- Effect . succeed ( {
95- code : ChildProcessSpawner . ExitCode ( 1 ) ,
96- text : "" ,
97- stderr : err instanceof Error ? err . message : String ( err ) ,
98- } ) ,
99- ) ,
100152 )
101153
102154 const ignore = Effect . fnUntraced ( function * ( files : string [ ] ) {
@@ -153,6 +205,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
153205 } ,
154206 )
155207 if ( result . code === 0 ) return
208+ yield * recordFailure ( )
156209 yield * Effect . logWarning ( "failed to add snapshot files" , {
157210 exitCode : result . code ,
158211 stderr : result . stderr ,
@@ -233,6 +286,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
233286 } )
234287
235288 const add = Effect . fnUntraced ( function * ( ) {
289+ if ( state . tripped ) return false
236290 yield * sync ( )
237291 const [ diff , other ] = yield * Effect . all (
238292 [
@@ -246,13 +300,14 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
246300 { concurrency : 2 } ,
247301 )
248302 if ( diff . code !== 0 || other . code !== 0 ) {
303+ yield * recordFailure ( )
249304 yield * Effect . logWarning ( "failed to list snapshot files" , {
250305 diffCode : diff . code ,
251306 diffStderr : diff . stderr ,
252307 otherCode : other . code ,
253308 otherStderr : other . stderr ,
254309 } )
255- return
310+ return false
256311 }
257312
258313 const tracked = diff . text . split ( "\0" ) . filter ( Boolean )
@@ -293,8 +348,9 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
293348 )
294349 const block = new Set ( untracked . filter ( ( item ) => large . has ( item ) ) )
295350 yield * sync ( Array . from ( block ) )
296- // Stage only the allowed candidate paths so snapshot updates stay scoped.
297351 yield * stage ( allow . filter ( ( item ) => ! block . has ( item ) ) )
352+ yield * recordSuccess ( )
353+ return true
298354 } )
299355
300356 const cleanup = Effect . fnUntraced ( function * ( ) {
@@ -319,6 +375,12 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
319375 return yield * locked (
320376 Effect . gen ( function * ( ) {
321377 if ( ! ( yield * enabled ( ) ) ) return
378+ if ( state . tripped ) {
379+ yield * Effect . logWarning ( "snapshot skipped — circuit breaker tripped" , {
380+ consecutiveFailures : state . consecutiveFailures ,
381+ } )
382+ return
383+ }
322384 const existed = yield * exists ( state . gitdir )
323385 yield * fs . ensureDir ( state . gitdir ) . pipe ( Effect . orDie )
324386 if ( ! existed ) {
@@ -337,8 +399,18 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
337399 yield * seed ( )
338400 yield * Effect . logInfo ( "initialized" )
339401 }
340- yield * add ( )
402+ const ok = yield * add ( )
403+ if ( ! ok ) return
341404 const result = yield * git ( args ( [ "write-tree" ] ) , { cwd : state . directory } )
405+ if ( result . code !== 0 ) {
406+ yield * recordFailure ( )
407+ yield * Effect . logWarning ( "failed to write snapshot tree" , {
408+ exitCode : result . code ,
409+ stderr : result . stderr ,
410+ } )
411+ return
412+ }
413+ yield * recordSuccess ( )
342414 const hash = result . text . trim ( )
343415 yield * Effect . logInfo ( "tracking" , { hash, cwd : state . directory , git : state . gitdir } )
344416 return hash
@@ -349,6 +421,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
349421 const patch = Effect . fnUntraced ( function * ( hash : string ) {
350422 return yield * locked (
351423 Effect . gen ( function * ( ) {
424+ if ( state . tripped ) return { hash, files : [ ] }
352425 yield * add ( )
353426 const result = yield * git (
354427 [ ...quote , ...args ( [ "diff" , "--cached" , "--no-ext-diff" , "--name-only" , hash , "--" , "." ] ) ] ,
@@ -526,6 +599,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
526599 const diff = Effect . fnUntraced ( function * ( hash : string ) {
527600 return yield * locked (
528601 Effect . gen ( function * ( ) {
602+ if ( state . tripped ) return ""
529603 yield * add ( )
530604 const result = yield * git ( [ ...quote , ...args ( [ "diff" , "--cached" , "--no-ext-diff" , hash , "--" , "." ] ) ] , {
531605 cwd : state . worktree ,
@@ -546,6 +620,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
546620 const diffFull = Effect . fnUntraced ( function * ( from : string , to : string ) {
547621 return yield * locked (
548622 Effect . gen ( function * ( ) {
623+ if ( state . tripped ) return [ ]
549624 type Row = {
550625 file : string
551626 status : "added" | "deleted" | "modified"
0 commit comments