@@ -169,16 +169,26 @@ class ProactiveSentinel extends EventEmitter {
169169 name : watchpoint . name ,
170170 description : watchpoint . description || '' ,
171171 check : watchpoint . check ,
172- intervalMs : watchpoint . intervalMs || this . config . defaultIntervalMs ,
173- severity : watchpoint . severity || AlertSeverity . WARNING ,
174- autoHeal : watchpoint . autoHeal || false ,
172+ intervalMs : watchpoint . intervalMs ?? this . config . defaultIntervalMs ,
173+ severity : watchpoint . severity ?? AlertSeverity . WARNING ,
174+ autoHeal : watchpoint . autoHeal ?? false ,
175175 healerId : watchpoint . healerId || null ,
176176 lastStatus : WatchpointStatus . UNKNOWN ,
177177 lastCheck : null ,
178178 consecutiveFailures : 0 ,
179179 } ;
180180
181181 this . watchpoints . set ( watchpoint . id , entry ) ;
182+
183+ // If sentinel is already running, create a timer for the new watchpoint immediately
184+ if ( this . running ) {
185+ const timer = setInterval ( ( ) => {
186+ this . evaluateWatchpoint ( entry . id ) . catch ( ( ) => { } ) ;
187+ } , entry . intervalMs ) ;
188+ if ( timer . unref ) timer . unref ( ) ;
189+ this . _timers . set ( entry . id , timer ) ;
190+ }
191+
182192 this . emit ( Events . WATCHPOINT_REGISTERED , { id : entry . id , name : entry . name } ) ;
183193 return entry ;
184194 }
@@ -223,8 +233,8 @@ class ProactiveSentinel extends EventEmitter {
223233 wp . lastStatus = result . status || WatchpointStatus . UNKNOWN ;
224234 wp . lastCheck = new Date ( ) . toISOString ( ) ;
225235
226- // Track consecutive failures
227- if ( wp . lastStatus === WatchpointStatus . FAILING ) {
236+ // Track consecutive failures (DEGRADED counts as a soft failure)
237+ if ( wp . lastStatus === WatchpointStatus . FAILING || wp . lastStatus === WatchpointStatus . DEGRADED ) {
228238 wp . consecutiveFailures ++ ;
229239 } else if ( wp . lastStatus === WatchpointStatus . HEALTHY ) {
230240 wp . consecutiveFailures = 0 ;
@@ -243,7 +253,7 @@ class ProactiveSentinel extends EventEmitter {
243253
244254 this . emit ( Events . WATCHPOINT_EVALUATED , evaluation ) ;
245255
246- // Fire alert on status change to degraded/failing
256+ // Fire alert on non-healthy status or repeated failures
247257 if (
248258 wp . lastStatus !== WatchpointStatus . HEALTHY &&
249259 wp . lastStatus !== WatchpointStatus . UNKNOWN &&
@@ -410,7 +420,7 @@ class ProactiveSentinel extends EventEmitter {
410420 this . registerWatchpoint ( {
411421 id : 'config-integrity' ,
412422 name : 'Config File Integrity' ,
413- description : 'Checks that core config files are valid JSON/ YAML' ,
423+ description : 'Checks that core JSON config files are valid and YAML files are readable ' ,
414424 severity : AlertSeverity . CRITICAL ,
415425 check : async ( ) => {
416426 const configFiles = [
@@ -424,6 +434,15 @@ class ProactiveSentinel extends EventEmitter {
424434 const content = fs . readFileSync ( file , 'utf8' ) ;
425435 if ( file . endsWith ( '.json' ) ) {
426436 JSON . parse ( content ) ;
437+ } else if ( file . endsWith ( '.yaml' ) || file . endsWith ( '.yml' ) ) {
438+ // Without a YAML parser, verify the file is readable and non-empty
439+ if ( ! content . trim ( ) ) {
440+ return {
441+ status : WatchpointStatus . FAILING ,
442+ message : `Empty config: ${ path . basename ( file ) } ` ,
443+ data : { file } ,
444+ } ;
445+ }
427446 }
428447 } catch {
429448 return {
@@ -479,24 +498,21 @@ class ProactiveSentinel extends EventEmitter {
479498 severity : AlertSeverity . WARNING ,
480499 autoHeal : true ,
481500 check : async ( ) => {
482- const lockPatterns = [
483- path . join ( projectRoot , '.aiox' , '*.lock' ) ,
484- path . join ( projectRoot , '.aiox' , 'locks' , '*' ) ,
485- ] ;
486-
501+ const lockDir = path . join ( projectRoot , '.aiox' ) ;
487502 const staleLocks = [ ] ;
488503 const maxAge = 30 * 60 * 1000 ; // 30 minutes
489504
490- for ( const pattern of lockPatterns ) {
491- const dir = path . dirname ( pattern ) ;
492- if ( ! fs . existsSync ( dir ) ) continue ;
493-
494- const files = fs . readdirSync ( dir ) . filter ( ( f ) => f . endsWith ( '.lock' ) ) ;
505+ if ( fs . existsSync ( lockDir ) ) {
506+ const files = fs . readdirSync ( lockDir ) . filter ( ( f ) => f . endsWith ( '.lock' ) ) ;
495507 for ( const file of files ) {
496- const filePath = path . join ( dir , file ) ;
497- const stat = fs . statSync ( filePath ) ;
498- if ( Date . now ( ) - stat . mtimeMs > maxAge ) {
499- staleLocks . push ( filePath ) ;
508+ const filePath = path . join ( lockDir , file ) ;
509+ try {
510+ const stat = fs . statSync ( filePath ) ;
511+ if ( Date . now ( ) - stat . mtimeMs > maxAge ) {
512+ staleLocks . push ( filePath ) ;
513+ }
514+ } catch {
515+ // ignore inaccessible files
500516 }
501517 }
502518 }
@@ -524,16 +540,7 @@ class ProactiveSentinel extends EventEmitter {
524540 return { status : WatchpointStatus . HEALTHY , message : 'No .aiox dir' } ;
525541 }
526542
527- let totalSize = 0 ;
528- const files = fs . readdirSync ( memDir ) ;
529- for ( const file of files ) {
530- try {
531- const stat = fs . statSync ( path . join ( memDir , file ) ) ;
532- totalSize += stat . size ;
533- } catch {
534- // ignore
535- }
536- }
543+ const totalSize = this . _getDirectorySize ( memDir ) ;
537544
538545 const maxSize = 50 * 1024 * 1024 ; // 50MB warning threshold
539546 if ( totalSize > maxSize ) {
@@ -581,6 +588,35 @@ class ProactiveSentinel extends EventEmitter {
581588 // INTERNAL METHODS
582589 // ═══════════════════════════════════════════════════════════════════════════════
583590
591+ /**
592+ * Recursively compute the total size of a directory in bytes
593+ * @param {string } dir - Directory path
594+ * @returns {number } Total size in bytes
595+ * @private
596+ */
597+ _getDirectorySize ( dir ) {
598+ let totalSize = 0 ;
599+ try {
600+ const entries = fs . readdirSync ( dir , { withFileTypes : true } ) ;
601+ for ( const entry of entries ) {
602+ const fullPath = path . join ( dir , entry . name ) ;
603+ try {
604+ if ( entry . isDirectory ( ) ) {
605+ totalSize += this . _getDirectorySize ( fullPath ) ;
606+ } else {
607+ const stat = fs . statSync ( fullPath ) ;
608+ totalSize += stat . size ;
609+ }
610+ } catch {
611+ // ignore inaccessible entries
612+ }
613+ }
614+ } catch {
615+ // ignore unreadable directories
616+ }
617+ return totalSize ;
618+ }
619+
584620 /**
585621 * Fire an alert
586622 * @private
0 commit comments