@@ -308,6 +308,8 @@ class BuildStateManager {
308308 // Internal state
309309 this . _state = null ;
310310 this . _logBuffer = [ ] ;
311+ this . _persistenceAvailable = true ;
312+ this . _persistenceError = null ;
311313 }
312314
313315 // ─────────────────────────────────────────────────────────────────────────────────
@@ -367,14 +369,24 @@ class BuildStateManager {
367369 * @returns {Object|null } Loaded state or null if not exists
368370 */
369371 loadState ( ) {
372+ if ( ! this . _persistenceAvailable ) {
373+ return this . _state ;
374+ }
375+
370376 if ( ! fs . existsSync ( this . stateFilePath ) ) {
371377 return null ;
372378 }
373379
380+ let content ;
374381 try {
375- const content = fs . readFileSync ( this . stateFilePath , 'utf-8' ) ;
376- const state = JSON . parse ( content ) ;
382+ content = fs . readFileSync ( this . stateFilePath , 'utf-8' ) ;
383+ } catch ( error ) {
384+ this . _markPersistenceUnavailable ( error ) ;
385+ return this . _state ;
386+ }
377387
388+ try {
389+ const state = JSON . parse ( content ) ;
378390 // Validate
379391 const validation = validateBuildState ( state ) ;
380392 if ( ! validation . valid ) {
@@ -400,11 +412,6 @@ class BuildStateManager {
400412 throw new Error ( 'No state to save. Call createState() or loadState() first.' ) ;
401413 }
402414
403- // Ensure directory exists
404- if ( ! fs . existsSync ( this . planDir ) ) {
405- fs . mkdirSync ( this . planDir , { recursive : true } ) ;
406- }
407-
408415 // Only update timestamp if explicitly requested (via saveCheckpoint)
409416 if ( options . updateCheckpoint ) {
410417 this . _state . lastCheckpoint = new Date ( ) . toISOString ( ) ;
@@ -416,11 +423,24 @@ class BuildStateManager {
416423 throw new Error ( `Invalid state: ${ validation . errors . join ( ', ' ) } ` ) ;
417424 }
418425
419- // Write state file
420- fs . writeFileSync ( this . stateFilePath , JSON . stringify ( this . _state , null , 2 ) , 'utf-8' ) ;
426+ if ( ! this . _persistenceAvailable ) {
427+ return this . _state ;
428+ }
421429
422- // Flush log buffer
423- this . _flushLogBuffer ( ) ;
430+ try {
431+ // Ensure directory exists
432+ if ( ! fs . existsSync ( this . planDir ) ) {
433+ fs . mkdirSync ( this . planDir , { recursive : true } ) ;
434+ }
435+
436+ // Write state file
437+ fs . writeFileSync ( this . stateFilePath , JSON . stringify ( this . _state , null , 2 ) , 'utf-8' ) ;
438+
439+ // Flush log buffer
440+ this . _flushLogBuffer ( ) ;
441+ } catch ( error ) {
442+ this . _markPersistenceUnavailable ( error ) ;
443+ }
424444
425445 return this . _state ;
426446 }
@@ -464,11 +484,6 @@ class BuildStateManager {
464484 throw new Error ( 'No state loaded' ) ;
465485 }
466486
467- // Ensure checkpoint directory exists
468- if ( ! fs . existsSync ( this . checkpointDir ) ) {
469- fs . mkdirSync ( this . checkpointDir , { recursive : true } ) ;
470- }
471-
472487 const checkpointId = this . _generateCheckpointId ( ) ;
473488 const now = new Date ( ) . toISOString ( ) ;
474489
@@ -499,8 +514,19 @@ class BuildStateManager {
499514 this . _updateMetrics ( checkpoint ) ;
500515
501516 // Save checkpoint file
502- const checkpointPath = path . join ( this . checkpointDir , `${ checkpointId } .json` ) ;
503- fs . writeFileSync ( checkpointPath , JSON . stringify ( checkpoint , null , 2 ) , 'utf-8' ) ;
517+ if ( this . _persistenceAvailable ) {
518+ try {
519+ // Ensure checkpoint directory exists
520+ if ( ! fs . existsSync ( this . checkpointDir ) ) {
521+ fs . mkdirSync ( this . checkpointDir , { recursive : true } ) ;
522+ }
523+
524+ const checkpointPath = path . join ( this . checkpointDir , `${ checkpointId } .json` ) ;
525+ fs . writeFileSync ( checkpointPath , JSON . stringify ( checkpoint , null , 2 ) , 'utf-8' ) ;
526+ } catch ( error ) {
527+ this . _markPersistenceUnavailable ( error ) ;
528+ }
529+ }
504530
505531 // Save main state with checkpoint timestamp update
506532 this . saveState ( { updateCheckpoint : true } ) ;
@@ -1041,6 +1067,8 @@ class BuildStateManager {
10411067 * @private
10421068 */
10431069 _logAttempt ( subtaskId , action , details = { } ) {
1070+ if ( ! this . _persistenceAvailable ) return ;
1071+
10441072 const entry = {
10451073 timestamp : new Date ( ) . toISOString ( ) ,
10461074 storyId : this . storyId ,
@@ -1066,15 +1094,20 @@ class BuildStateManager {
10661094 */
10671095 _flushLogBuffer ( ) {
10681096 if ( this . _logBuffer . length === 0 ) return ;
1097+ if ( ! this . _persistenceAvailable ) return ;
10691098
1070- // Ensure directory exists
1071- if ( ! fs . existsSync ( this . planDir ) ) {
1072- fs . mkdirSync ( this . planDir , { recursive : true } ) ;
1073- }
1099+ try {
1100+ // Ensure directory exists
1101+ if ( ! fs . existsSync ( this . planDir ) ) {
1102+ fs . mkdirSync ( this . planDir , { recursive : true } ) ;
1103+ }
10741104
1075- // Append to log file
1076- fs . appendFileSync ( this . logFilePath , this . _logBuffer . join ( '' ) , 'utf-8' ) ;
1077- this . _logBuffer = [ ] ;
1105+ // Append to log file
1106+ fs . appendFileSync ( this . logFilePath , this . _logBuffer . join ( '' ) , 'utf-8' ) ;
1107+ this . _logBuffer = [ ] ;
1108+ } catch ( error ) {
1109+ this . _markPersistenceUnavailable ( error ) ;
1110+ }
10781111 }
10791112
10801113 /**
@@ -1084,11 +1117,22 @@ class BuildStateManager {
10841117 * @returns {string[] } Log lines
10851118 */
10861119 getAttemptLog ( options = { } ) {
1120+ if ( ! this . _persistenceAvailable ) {
1121+ return [ ] ;
1122+ }
1123+
10871124 if ( ! fs . existsSync ( this . logFilePath ) ) {
10881125 return [ ] ;
10891126 }
10901127
1091- const content = fs . readFileSync ( this . logFilePath , 'utf-8' ) ;
1128+ let content ;
1129+ try {
1130+ content = fs . readFileSync ( this . logFilePath , 'utf-8' ) ;
1131+ } catch ( error ) {
1132+ this . _markPersistenceUnavailable ( error ) ;
1133+ return [ ] ;
1134+ }
1135+
10921136 let lines = content . split ( '\n' ) . filter ( ( l ) => l . trim ( ) ) ;
10931137
10941138 // Filter by subtask if specified
@@ -1266,6 +1310,39 @@ class BuildStateManager {
12661310 // Silent by default - can be overridden
12671311 }
12681312
1313+ /**
1314+ * Check if file-backed persistence is still available.
1315+ *
1316+ * @returns {boolean } True when state/checkpoint/log writes are available.
1317+ */
1318+ isPersistenceAvailable ( ) {
1319+ return this . _persistenceAvailable ;
1320+ }
1321+
1322+ /**
1323+ * Get the first persistence error that disabled file-backed writes.
1324+ *
1325+ * @returns {Error|null } Persistence error or null when persistence is available.
1326+ */
1327+ getPersistenceError ( ) {
1328+ return this . _persistenceError ;
1329+ }
1330+
1331+ /**
1332+ * Disable file-backed persistence after an I/O failure.
1333+ *
1334+ * @param {Error } error - Underlying persistence error.
1335+ * @private
1336+ */
1337+ _markPersistenceUnavailable ( error ) {
1338+ if ( ! this . _persistenceAvailable ) return ;
1339+
1340+ this . _persistenceAvailable = false ;
1341+ this . _persistenceError = error instanceof Error ? error : new Error ( String ( error ) ) ;
1342+ this . _logBuffer = [ ] ;
1343+ this . _log ( `Persistence unavailable: ${ this . _persistenceError . message } ` ) ;
1344+ }
1345+
12691346 // ─────────────────────────────────────────────────────────────────────────────────
12701347 // CLI FORMATTING
12711348 // ─────────────────────────────────────────────────────────────────────────────────
0 commit comments