From 558c39cc140580762be8dbba2140e23c209aae24 Mon Sep 17 00:00:00 2001 From: claudeMB Date: Thu, 17 Sep 2026 12:40:03 +0200 Subject: [PATCH 1/2] telemetry: publish the machine's own critical temperature petrus wants temperature coloured green / amber / fuchsia by how close a box is to ITS limit, not to a guessed scale. He is right that the scale has to be per-machine: 49 C is idle on a box that trips at 104 and alarming on one that trips at 60, and a 0-100 C bar would render a healthy 72 C box as "72% of something" that does not exist. Linux declares the number itself. Every ACPI zone on the DGX Sparks carries a critical trip point of 104.8 C, so a reader can colour 49.7 C as 47% of limit instead of inventing a ceiling. - New source readThermalCriticalMilli(zone) reads the zone's trip table and returns the 'critical' trip only. A zone also advertises passive and hot trips; those are throttle hints, not the number a reading is judged against. - readLinuxTempC now returns {tempC, limitC} and the limit comes from THE SAME zone as the reading. Pairing the hottest zone's temperature with a different zone's critical point would describe neither, and a test fails if it does. - A critical point below the current reading, or above the plausible-die ceiling, is treated as a broken table and dropped. Publishing it would paint a cool box fuchsia. - Unreadable trip table costs the limit, never the temperature. A host that declares no critical point simply omits temp_limit_c, and the reader falls back to its own default: the same omit-never-zero contract the rest of this module keeps. Macs omit it, as they already omit temp_c, because that sensor is behind root. 32 tests pass. Verified against a known failure: pointing the limit read at the wrong zone makes the same-zone test fail, and only that one. Co-Authored-By: Claude Opus 5 --- .../user-intent-kit/src/host-telemetry.js | 51 +++++++++++++++++-- .../test/host-telemetry.test.js | 51 ++++++++++++++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/user-intent-kit/src/host-telemetry.js b/packages/user-intent-kit/src/host-telemetry.js index 5757944..57ef303 100644 --- a/packages/user-intent-kit/src/host-telemetry.js +++ b/packages/user-intent-kit/src/host-telemetry.js @@ -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 ''; + }, }; /** @@ -318,17 +331,39 @@ 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; + // A critical point below the current reading, or outside plausible die + // temperatures, is a broken table rather than an emergency. + if (raw !== '' && Number.isFinite(c) && c > hottest && 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 }; } /** @@ -369,8 +404,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. diff --git a/packages/user-intent-kit/test/host-telemetry.test.js b/packages/user-intent-kit/test/host-telemetry.test.js index 70584cd..ae85934 100644 --- a/packages/user-intent-kit/test/host-telemetry.test.js +++ b/packages/user-intent-kit/test/host-telemetry.test.js @@ -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, @@ -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 ?? ''; + }, }; } @@ -327,3 +332,47 @@ 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 nonsense critical point is dropped rather than published', () => { + // Below the current reading, or hotter than any real die: a broken table, + // not an emergency. Publishing it would paint a cool box fuchsia. + for (const bad of ['30000', '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}`); + } +}); From 52cd078e2238d4fb3c9f938de7587b449db9e6af Mon Sep 17 00:00:00 2001 From: claudeMB Date: Thu, 17 Sep 2026 12:42:48 +0200 Subject: [PATCH 2/2] Keep the limit when a box has reached it: my guard inverted the alarm codexmb, reviewing #105: `c > hottest` dropped a valid critical point exactly when the temperature reached or passed it. A box reading 105 C against a 100 C critical published NO limit, and a reader falling back to a default would have painted a thermally emergency machine calmer than it actually was. My reasoning was that a critical point below the current reading must be a broken table. It is the opposite. The Linux thermal API defines crossing the critical trip as a protection event the kernel acts on, so that reading is the real state of a box in trouble - the one case the whole per-machine colour scheme exists to show. Plausibility is now judged on the value alone, TEMP_MIN_C..TEMP_MAX_C, with no reference to the reading. Unparseable, empty and hotter-than-any-real-die are still dropped; at-the-trip and over-the-trip are kept. New test covers 100/100 and 105/100. Verified against the known failure: restoring `c > hottest` fails that test and only that test. 33 tests pass. Co-Authored-By: Claude Opus 5 --- .../user-intent-kit/src/host-telemetry.js | 13 +++++++--- .../test/host-telemetry.test.js | 24 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/user-intent-kit/src/host-telemetry.js b/packages/user-intent-kit/src/host-telemetry.js index 57ef303..1a741d8 100644 --- a/packages/user-intent-kit/src/host-telemetry.js +++ b/packages/user-intent-kit/src/host-telemetry.js @@ -354,9 +354,16 @@ function readLinuxTempC(sources) { try { const raw = String(sources.readThermalCriticalMilli(hottestZone) ?? '').trim(); const c = Number(raw) / 1000; - // A critical point below the current reading, or outside plausible die - // temperatures, is a broken table rather than an emergency. - if (raw !== '' && Number.isFinite(c) && c > hottest && c <= TEMP_MAX_C) { + // 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 { diff --git a/packages/user-intent-kit/test/host-telemetry.test.js b/packages/user-intent-kit/test/host-telemetry.test.js index ae85934..2b9fa82 100644 --- a/packages/user-intent-kit/test/host-telemetry.test.js +++ b/packages/user-intent-kit/test/host-telemetry.test.js @@ -365,10 +365,28 @@ test('an unreadable trip table costs the limit, never the temperature', () => { 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', () => { - // Below the current reading, or hotter than any real die: a broken table, - // not an emergency. Publishing it would paint a cool box fuchsia. - for (const bad of ['30000', '900000', 'not-a-number', '']) { + // 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 } }), });