Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 99 additions & 63 deletions scripts/adaptive-cpu-alerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const DEFAULT_CONFIG = {

class AdaptiveCpuAlerts {
constructor(options = {}) {
this.lane = options.lane || 'archivist';
this.lane = options.lane || 'swarmmind';
this.stateDir = options.stateDir;
this.config = Object.assign({}, DEFAULT_CONFIG, options.config || {});
this.statePath = path.join(this.stateDir, 'adaptive-alert-state.json');
Expand Down Expand Up @@ -144,48 +144,83 @@ class AdaptiveCpuAlerts {

let deltaCpuUsec;
let wallSeconds;
const isFirstSample = this._prevCpu === null || this._prevWallMs === null;

if (this._prevCpu !== null && this._prevWallMs !== null) {
if (!isFirstSample) {
deltaCpuUsec = Math.max(0, cpuTotalUsec - this._prevCpu);
wallSeconds = _wallSecondsOverride != null
? _wallSecondsOverride
: (now - this._prevWallMs) / 1000;
} else {
// First sample: initialize state but do not evaluate CPU thresholds
// Memory threshold still evaluated on first sample
this._state.consecutiveHighCpu = 0;
this._state.consecutiveCriticalCpu = 0;
this._state.consecutiveEmergencyCpu = 0;
deltaCpuUsec = cpuTotalUsec;
wallSeconds = _wallSecondsOverride != null
? _wallSecondsOverride
: Math.max(this.config.sample_window_seconds, process.uptime());
}

const cpuPct = this._normalizeCpuPct(deltaCpuUsec, wallSeconds);

this._prevWallMs = now;
this._prevCpu = cpuTotalUsec;

const thresholds = this._getAdaptiveThresholds();
const result = this._checkThresholds(cpuPct, memRss, thresholds);

if (cpuPct < thresholds.warningPct) {
this._state.samples.push({
timestamp: new Date().toISOString(),
cpuPct: Math.round(cpuPct * 1000) / 1000,
cpuDeltaUsec: deltaCpuUsec,
cpuTotalUsec,
wallSeconds: Math.round(wallSeconds * 10) / 10,
});
}
// Skip CPU threshold evaluation on first sample
if (isFirstSample) {
// Still check memory threshold
const memAlerts = this._checkMemoryThreshold(memRss);
this.saveState();
return memAlerts.length > 0 ? {
shouldAlert: true,
severity: memAlerts[0].severity,
alerts: memAlerts,
cpuPct: 0,
thresholds: this._getAdaptiveThresholds(),
escalate: memAlerts.some(a => a.severity === 'CRITICAL'),
} : null;
Comment on lines +165 to +177

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the test helper and first-sample assertions.
sed -n '1,130p' scripts/test-adaptive-cpu-alerts.js

# Find direct evaluator consumers and inspect their null handling.
rg -n -C 4 --type js '\.evaluate\s*\(' scripts

Repository: vortsghost2025/Archivist-Agent

Length of output: 22398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect AdaptiveCpuAlerts constructor/state initialization and evaluate contract.
sed -n '1,240p' scripts/adaptive-cpu-alerts.js

# Inspect adaptive alert caller and first-sample handling in LaneWorker.
sed -n '360,460p' scripts/lane-worker.js
sed -n '88,106p' scripts/test-lane-worker-adaptive-alerts.js

Repository: vortsghost2025/Archivist-Agent

Length of output: 12528


Make evaluate return a non-alert result object when memory is normal.

AdaptiveCpuAlerts returns null on normal memory usage, but scripts/test-lane-worker-adaptive-alerts.js asserts that evaluate returns a result with shouldAlert. Replace the first-sample null return with { shouldAlert: false, ... }, and align similar null returns with the same result contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/adaptive-cpu-alerts.js` around lines 165 - 177, Update
AdaptiveCpuAlerts.evaluate’s first-sample branch to always return a result
object with shouldAlert, using shouldAlert: false and the appropriate non-alert
fields when _checkMemoryThreshold reports no alerts; preserve alert details when
memory alerts exist. Review other normal-path null returns in evaluate and align
them with this same result contract.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the evaluate result-object contract

For a normal first sample this now returns null, whereas existing callers and tests inspect result.shouldAlert; scripts/test-lane-worker-adaptive-alerts.js explicitly requires a result object, and running node scripts/test-adaptive-cpu-alerts.js against this commit produces nine failures from the same contract change. Skip CPU thresholding on the first sample without replacing the status result with null.

Useful? React with 👍 / 👎.

}

const cpuPct = this._normalizeCpuPct(deltaCpuUsec, wallSeconds);

const maxSamples = this.config.baseline_window_samples * 2;
if (this._state.samples.length > maxSamples) {
this._state.samples = this._state.samples.slice(-this.config.baseline_window_samples);
}
const thresholds = this._getAdaptiveThresholds();
const result = this._checkThresholds(cpuPct, memRss, thresholds);

if (cpuPct < thresholds.warningPct) {
this._state.samples.push({
timestamp: new Date().toISOString(),
cpuPct: Math.round(cpuPct * 1000) / 1000,
cpuDeltaUsec: deltaCpuUsec,
cpuTotalUsec,
wallSeconds: Math.round(wallSeconds * 10) / 10,
});
}

this.saveState();
return result;
}
const maxSamples = this.config.baseline_window_samples * 2;
if (this._state.samples.length > maxSamples) {
this._state.samples = this._state.samples.slice(-this.config.baseline_window_samples);
}

this.saveState();
return result.shouldAlert ? result : null;
}

_checkMemoryThreshold(memRss) {
const alerts = [];
if (memRss > this.config.mem_threshold_bytes) {
const key = `mem-warning-${this.lane}`;
if (!this._isCooldownActive(key)) {
const memPct = (memRss / this.config.mem_threshold_bytes) * 100;
alerts.push({
severity: memPct > 200 ? 'CRITICAL' : 'WARNING',
metric: 'memory',
value: memRss,
threshold: this.config.mem_threshold_bytes,
thresholdType: 'static',
mode: 'static',
message: `Memory RSS at ${(memRss / 1024 / 1024).toFixed(1)}MB exceeds threshold ${(this.config.mem_threshold_bytes / 1024 / 1024).toFixed(0)}MB`,
});
this._setCooldown(key, this.config.mem_cooldown_seconds);
}
}
return alerts;
}

_checkThresholds(cpuPct, memRss, thresholds) {
const alerts = [];
Expand Down Expand Up @@ -258,44 +293,45 @@ class AdaptiveCpuAlerts {
this._state.consecutiveHighCpu = 0;
}
}
} else {
this._state.consecutiveHighCpu = 0;
this._state.consecutiveCriticalCpu = 0;
this._state.consecutiveEmergencyCpu = 0;
}

if (memRss > this.config.mem_threshold_bytes) {
const key = `mem-warning-${this.lane}`;
if (!this._isCooldownActive(key)) {
const memPct = (memRss / this.config.mem_threshold_bytes) * 100;
alerts.push({
severity: memPct > 200 ? 'CRITICAL' : 'WARNING',
metric: 'memory',
value: memRss,
threshold: this.config.mem_threshold_bytes,
thresholdType: 'static',
mode: 'static',
message: `Memory RSS at ${(memRss / 1024 / 1024).toFixed(1)}MB exceeds threshold ${(this.config.mem_threshold_bytes / 1024 / 1024).toFixed(0)}MB`,
});
this._setCooldown(key, this.config.mem_cooldown_seconds);
}
}
} else {
this._state.consecutiveHighCpu = 0;
this._state.consecutiveCriticalCpu = 0;
this._state.consecutiveEmergencyCpu = 0;
}

const escalate = alerts.some(a => a.severity === 'CRITICAL');
const maxAlert = alerts.length > 0 ? alerts.reduce((a, b) => {
const rank = { CRITICAL: 3, WARNING: 2, INFO: 1 };
return (rank[a.severity] || 0) >= (rank[b.severity] || 0) ? a : b;
}) : null;
// Memory threshold check (always evaluated, not just first sample)
if (memRss > this.config.mem_threshold_bytes) {
const key = `mem-warning-${this.lane}`;
if (!this._isCooldownActive(key)) {
const memPct = (memRss / this.config.mem_threshold_bytes) * 100;
alerts.push({
severity: memPct > 200 ? 'CRITICAL' : 'WARNING',
metric: 'memory',
value: memRss,
threshold: this.config.mem_threshold_bytes,
thresholdType: 'static',
mode: 'static',
message: `Memory RSS at ${(memRss / 1024 / 1024).toFixed(1)}MB exceeds threshold ${(this.config.mem_threshold_bytes / 1024 / 1024).toFixed(0)}MB`,
});
this._setCooldown(key, this.config.mem_cooldown_seconds);
}
}

return {
shouldAlert: alerts.length > 0,
severity: maxAlert ? maxAlert.severity : null,
alerts,
cpuPct,
thresholds,
escalate,
};
}
const escalate = alerts.some(a => a.severity === 'CRITICAL');
const maxAlert = alerts.length > 0 ? alerts.reduce((a, b) => {
const rank = { CRITICAL: 3, WARNING: 2, INFO: 1 };
return (rank[a.severity] || 0) >= (rank[b.severity] || 0) ? a : b;
}) : null;

return {
shouldAlert: alerts.length > 0,
severity: maxAlert ? maxAlert.severity : null,
alerts,
cpuPct,
thresholds,
escalate,
};
}

getStatus() {
this.loadState();
Expand Down
Loading
Loading