Skip to content

Commit e34ed95

Browse files
committed
fix(health-check): address Copilot review on proactive-sentinel
- Fix config-integrity to not falsely claim YAML validation - Make disk-space check recursive for subdirectories - Handle DEGRADED status in consecutiveFailures tracking - Simplify stale-locks to direct scan instead of glob patterns - Auto-register timers for watchpoints added after start() - Use ?? instead of || for optional numeric defaults - Copy module to .aiox-core/ for package shipping
1 parent 549e82e commit e34ed95

3 files changed

Lines changed: 810 additions & 83 deletions

File tree

.aios-core/core/health-check/proactive-sentinel.js

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)