Skip to content
Merged
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
58 changes: 54 additions & 4 deletions packages/user-intent-kit/src/host-telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ export const defaultSources = {
return out;
},
readThermalZone: (zone) => readFileSync(`${THERMAL_ROOT}/${zone}/temp`, 'utf8'),
// The zone's own critical trip point, in millidegrees, or '' when the zone
// declares none. A zone advertises several trips (passive, hot, critical);
// only 'critical' is the number a reading should be judged against.
readThermalCriticalMilli: (zone) => {
const dir = `${THERMAL_ROOT}/${zone}`;
for (const entry of readdirSync(dir)) {
const m = entry.match(/^trip_point_(\d+)_type$/);
if (!m) continue;
if (readFileSync(`${dir}/${entry}`, 'utf8').trim() !== 'critical') continue;
return readFileSync(`${dir}/trip_point_${m[1]}_temp`, 'utf8');
}
return '';
},
};

/**
Expand Down Expand Up @@ -318,17 +331,46 @@ function readLinuxTempC(sources) {
}

let hottest;
let hottestZone;
for (const zone of zones || []) {
try {
const celsius = Number(String(sources.readThermalZone(zone)).trim()) / 1000;
if (celsius >= TEMP_MIN_C && celsius <= TEMP_MAX_C) {
if (hottest === undefined || celsius > hottest) hottest = celsius;
if (hottest === undefined || celsius > hottest) {
hottest = celsius;
hottestZone = zone;
}
}
} catch {
// Unreadable zone: skip it, keep whatever the other zones gave us.
}
}
return hottest === undefined ? undefined : Math.round(hottest * 10) / 10;
if (hottest === undefined) return undefined;

// The limit MUST come from the same zone as the reading. Pairing the
// hottest zone's temperature with some other zone's critical point would
// produce a headroom figure describing neither.
let limitC;
try {
const raw = String(sources.readThermalCriticalMilli(hottestZone) ?? '').trim();
const c = Number(raw) / 1000;
// Plausibility is judged on the VALUE ALONE, never against the current
// reading. An earlier cut required `c > hottest`, reasoning that a
// critical point below the temperature had to be a broken table. It is
// the opposite: crossing the critical trip is a protection event the
// kernel acts on, so a box reading 105 C against a 100 C critical is in
// exactly the trouble a dashboard exists to show. That guard deleted the
// limit at the only moment it mattered, and a reader falling back to a
// default would have painted a thermally emergency box calmer than it
// was. (codexmb, 17 Sep 2026.)
if (raw !== '' && Number.isFinite(c) && c >= TEMP_MIN_C && c <= TEMP_MAX_C) {
limitC = Math.round(c * 10) / 10;
}
} catch {
// No trip table: publish the reading without a limit.
}

return { tempC: Math.round(hottest * 10) / 10, limitC };
}

/**
Expand Down Expand Up @@ -369,8 +411,16 @@ export function collectHostTelemetry({ machine, kind, model, sources = defaultSo

try {
if (sources.platform() === 'linux') {
const tempC = readLinuxTempC(sources);
if (tempC !== undefined) host.temp_c = tempC;
const reading = readLinuxTempC(sources);
if (reading !== undefined) {
host.temp_c = reading.tempC;
// Published so a dashboard can colour a temperature against THIS
// machine's own limit instead of a guessed scale: 49 C is idle on a
// box that trips at 104 and alarming on one that trips at 60. A host
// that declares no critical point simply omits this, and the reader
// falls back to its own default (petrus, 17 Sep 2026).
if (reading.limitC !== undefined) host.temp_limit_c = reading.limitC;
}
}
} catch {
// No thermal zones exposed; publish without a temperature.
Expand Down
69 changes: 68 additions & 1 deletion packages/user-intent-kit/test/host-telemetry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { collectHostTelemetry, modelFromCommandLine } from '../src/host-telemetr
*/
function sources({ platform = 'linux', load = [1.8, 1.7, 1.6], cpuCount = 4, zones = {},
totalMem = 16e9, freeMem = 4e9, availMem = 12e9, commands = {},
processes = [] } = {}) {
processes = [], trips = {} } = {}) {
return {
platform: () => platform,
loadavg: () => load,
Expand All @@ -34,6 +34,11 @@ function sources({ platform = 'linux', load = [1.8, 1.7, 1.6], cpuCount = 4, zon
if (value instanceof Error) throw value;
return value;
},
readThermalCriticalMilli: (zone) => {
const value = trips[zone];
if (value instanceof Error) throw value;
return value ?? '';
},
};
}

Expand Down Expand Up @@ -327,3 +332,65 @@ test('a voice stack with lower pids does not become the served model (VTA layout
// and with the model server gone, the voice stack still is not "the model"
assert.ok(!('model' in collectHostTelemetry({ sources: sources({ processes: table.slice(0, 3) }) })));
});
// --- the machine's own limit, so a dashboard is not guessing a scale ---

test('publishes the critical trip point of the SAME zone the reading came from', () => {
const host = collectHostTelemetry({
sources: sources({
zones: { thermal_zone0: '49000', thermal_zone1: '72000' },
// zone1 is the hottest, so zone1's limit is the one that applies.
trips: { thermal_zone0: '104000', thermal_zone1: '95000' },
}),
});
assert.equal(host.temp_c, 72);
assert.equal(host.temp_limit_c, 95, "paired the reading with another zone's limit");
});

test('a host that declares no critical point publishes a temperature and no limit', () => {
const host = collectHostTelemetry({
sources: sources({ zones: { thermal_zone0: '49000' }, trips: {} }),
});
assert.equal(host.temp_c, 49);
assert.ok(!('temp_limit_c' in host), 'invented a limit the machine never declared');
});

test('an unreadable trip table costs the limit, never the temperature', () => {
const host = collectHostTelemetry({
sources: sources({
zones: { thermal_zone0: '49000' },
trips: { thermal_zone0: new Error('EACCES') },
}),
});
assert.equal(host.temp_c, 49);
assert.ok(!('temp_limit_c' in host));
});

test('a critical point is kept when the machine has REACHED it', () => {
// The moment the limit matters most. An earlier cut dropped any critical
// point at or below the reading as "a broken table", so a box sitting on
// its trip published no limit at all and a reader fell back to a calmer
// default. Crossing the critical trip is a protection event, not bad data.
const at = collectHostTelemetry({
sources: sources({ zones: { thermal_zone0: '100000' }, trips: { thermal_zone0: '100000' } }),
});
assert.equal(at.temp_c, 100);
assert.equal(at.temp_limit_c, 100, 'dropped the limit at exactly the trip point');

const over = collectHostTelemetry({
sources: sources({ zones: { thermal_zone0: '105000' }, trips: { thermal_zone0: '100000' } }),
});
assert.equal(over.temp_c, 105);
assert.equal(over.temp_limit_c, 100, 'dropped the limit on a box that is OVER it');
});

test('a nonsense critical point is dropped rather than published', () => {
// Judged on the value alone: unparseable, empty, or hotter than any real
// die. Deliberately NOT "below the current reading" - see the test above.
for (const bad of ['900000', 'not-a-number', '']) {
const host = collectHostTelemetry({
sources: sources({ zones: { thermal_zone0: '49000' }, trips: { thermal_zone0: bad } }),
});
assert.equal(host.temp_c, 49, `temp lost for trip=${bad}`);
assert.ok(!('temp_limit_c' in host), `published nonsense limit ${bad}`);
}
});
Loading