Skip to content

feat(uik): publish host vitals (load, temp) as an object - #64

Merged
ThinkOffApp merged 3 commits into
mainfrom
feat/host-telemetry
Aug 17, 2026
Merged

feat(uik): publish host vitals (load, temp) as an object#64
ThinkOffApp merged 3 commits into
mainfrom
feat/host-telemetry

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Owner

Petrus: "Pi is also on mains power, no reason to not report temp and load = you should have it for all devices."

host was a bare string on the agent record (claudemm.host = "mac-mini"), so the dashboard had nowhere to put a temperature or a load figure. This publishes it as an object.

What each machine reports

load temp
macOS (Mac mini) yes no, powermetrics needs root
Linux (Pi) yes yes, from the thermal zones

The Mac mini now publishes {"machine":"mac-mini","load_1m":2.18,"cpu_count":10,"load_pct":22} where it previously published the string "mac-mini".

Load is normalised against core count. Raw loadavg is not comparable across a fleet: 1.8 is about 18% of a 10-core M4 but about 45% of a 4-core Pi. load_pct is the only figure that means the same thing on every device, and load_1m is kept alongside it.

A sensor we cannot read is omitted, never zeroed. A zero renders as a real reading, which makes an idle box and a broken sensor look identical. This matches the reader contract grok landed on the CodeWatch side (missing value omitted, not zeroed).

macOS temperature is deliberately absent. powermetrics requires root, there is no passwordless sudo on the Mini, and no non-root thermal sysctl is exposed on this M4. A long-running daemon shelling out to sudo for a dashboard number is not a trade I wanted to make silently. If we want Mac temperature, it needs a privilege decision from Petrus (a NOPASSWD sudoers entry, or a separately privileged helper), not a quiet workaround here.

Testing. 13 tests pass. The omit-not-zero contract was verified by planting a zero temperature and confirming two tests fail on it, then restoring.

Scope note: this is the shared Node publisher only. It does not touch grok's presence.py (different repo) or the Pi's publisher.

Requesting adversarial review before merge, per our cross-model review rule. Not merging this myself.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

The agent record carried `host` as a bare string on some publishers, so the
dashboard had nowhere to put a temperature or a load figure and the Mac mini
showed neither.

Publish `host` as an object instead, collected per platform:

- load average everywhere, normalised against core count. Raw loadavg is not
  comparable across a fleet (1.8 is ~18% of a 10-core M4 but ~45% of a 4-core
  Pi), so `load_pct` is the figure that means the same thing on every device.
- CPU temperature on Linux from the thermal zones, no privileges needed.
- macOS publishes load only. Temperature and power there live behind
  `powermetrics`, which needs root, and a long-running daemon should not be
  shelling out to sudo for a dashboard number.

A sensor we cannot read is omitted, never zeroed: a zero renders as a real
reading, which would make an idle box and a broken sensor look identical.
Tests cover that contract and were checked by planting a zero to confirm they
fail on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0372f80567

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await this.#client.patchAgent(name, {
status,
last_task: currentTask,
host: collectHostTelemetry({ machine: this.#machine }),

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 machine name for existing adapter callers

When callers omit the newly optional machine argument, this heartbeat still writes a complete host object but without its machine field. This affects the built-in iakAdapterFromConfig path in src/intent.mjs, which has a configured deviceId but constructs IAKAdapter with only agentHandle, as well as the documented existing constructor calls. Consequently each PATCH can replace a previously stored hostname with anonymous load readings, preventing the dashboard from associating the vitals with a machine; default this value from client.deviceId or propagate the configured device ID.

Useful? React with 👍 / 👎.

Comment on lines +40 to +41
const expected = Math.round((host.load_1m / host.cpu_count) * 100);
assert.equal(host.load_pct, expected);

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 Avoid deriving expected load from the rounded reading

This assertion is nondeterministic because production computes load_pct from the full-precision load average, while the test recomputes it from load_1m after that value has been rounded to two decimals. For example, on a two-core host a raw load near 0.005 may publish load_1m: 0.01 and load_pct: 0, while this expectation calculates 1; ordinary changes in system load can therefore make the test fail intermittently. Compute both values from the same precision or allow for the rounding difference.

Useful? React with 👍 / 👎.

claudemm and others added 2 commits August 17, 2026 04:16
…ywhere

Review from @grok: the suite only exercised whichever machine ran it, so on a
Mac the Linux thermal path never ran at all, and the omit-not-zero guarantee
had only ever been checked by hand-planting a zero.

Put the platform reads behind an injectable `sources` object and test the
contract against fake sensors: a zone reading 0, implausible and non-numeric
readings, an unreadable zone alongside a readable one, no thermal directory,
macOS never publishing a temperature, Windows never publishing its [0,0,0]
load placeholder, and load normalisation across a 4-core and a 10-core box.

21 tests, verified to fail on a planted 0 C sensor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review from @codexmb: three of the four IAKAdapter call sites omit `machine`,
including iakAdapterFromConfig in src/intent.mjs. Since `host` replaces the
stored value wholesale, those callers would have overwritten a known hostname
with anonymous load readings — a regression against the bare string they
published before.

Patching intent.mjs alone would leave the next call site to break the same
way, so default it at the source: expose deviceId on IntentClient and fall
back to it in the adapter. All existing callers keep naming their machine
without changing a line. A read-only client has no deviceId and omits the
field rather than publishing a null.

Verified against the live config: iakAdapterFromConfig now publishes
machine="mac-mini". The regression test fails on the pre-fix constructor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ThinkOffApp
ThinkOffApp merged commit d15cc65 into main Aug 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant