v1.0.0
[1.0.0] - 2026-08-01
The stability promise. Four surfaces are frozen and each has a machine guard behind it.
On the way there the sensors came off the one-size-fits-all schedule, and a reading
carried over from an earlier tick now says how old it is instead of being republished as
though it had just been measured.
Added
Sensors read on their own cadence, and a carried-over reading is dated
- Temperatures and the battery are read every 30 seconds instead of every 5 — and
every 5 seconds while the Battery screen is visible, plus immediately when you open it.
They used to ride the 5-second medium tier whatever was on screen. On macOS one such
read costs about 85 ms of wall clock, which is a long time to spend on a number nobody
is looking at. It did not fix the budget it was aimed at, and Known limitations
below says so rather than leaving you to find out. - A reading carried over from an earlier tick is published as stale, carrying its age.
Until now a value read four seconds ago was republished as freshly measured, and nothing
downstream could tell the difference. At a 30-second cadence that would have become a
half-minute-old temperature presented as current, which is the one thing this project
will not do. The rule reaches the interface, the JSON export, and a library consumer
readingSensorSnapshotalike. - The header dates its hottest reading:
temp 62.5C ~00:28. The alternative was a
field that vanished for most of every 30 seconds, which is what filtering to freshly
measured readings would have done — a dated number beats a blank. - The Battery screen states the age once per panel, in the panel's own trailing label
—82% discharging ~00:28,2 sensors ~00:28. A dozen fields unwrapped from one
retained snapshot are all exactly the same age, and~00:28printed beside each of them
would be one fact rendered twelve times; the fields themselves are drawn in the stale
style rather than as measurements. The charge meter is the exception, because every
meter annotates its own value where it draws it.
The Time Lens caret says what changed, not only when
- A caret parked on a historical sample now carries the sample's wall clock and how its
CPU compares with two baselines, then how far back it is:22:14:07Z, then
cpu prev +41 points, then30s -6 points, then-00:37 selected. §2.5's comparisons
have been in the core since0.1.0with nothing calling them; this is where they reach
a screen. A baseline the history cannot reach reads30s no baselinerather than+0,
because a zero delta would say the metric did not change when the truth is that there
was nothing to compare it against. - The segments are ordered by what nothing else carries, and the note drops from the
end when the caret's side of the row runs out of room. The wall clock leads: between 80
and 99 columns this note is the only place the selected sample's clock appears at all,
because the compact header's one-line strip reads the live snapshot. The relative offset
goes last, since the[<HISTORY -MM:SS]badge repeats it at every width.
What 1.0.0 froze, and the four guards behind it
- Four surfaces are frozen: the public API of
monitrs-core,monitrs-collectorsand
monitrs-tui; the JSON export; the configuration keys; and the default keymap.
CONTRIBUTING.md's What 1.0.0 froze is the reference rather than
this list — it states the terms of each, what is deliberately not frozen (layout,
wording, colour, glyph choice and panel arrangement are presentation, not API), and why
no one of the four guards can see another's blind spot. - Each surface has a mechanism, not a paragraph. The API is checked by
cargo-semver-checksin CI; the export bydocs/schema/v2.json, an inventory of all 292
field paths version 2 produces, andcrates/monitrs/tests/schema_contract.rs; the
configuration keys bydocs/schema/config-v1.json, all 32 of them, and
crates/monitrs/tests/config_contract.rs; the keymap by exact key→action assertions in
crates/monitrs-tui/src/keymap.rs. Three of the four fail a build today. The semver job
reports without blocking until the commit that tagsv1.0.0, which is where it becomes
the gate.
Changed
Breaking for library users, one at a time:
- Six public enums are now
#[non_exhaustive]:Effect,Action,ViewIdand
SortField(monitrs-tui), the paletteCommand(monitrs-tui), andHistoryMetric
(monitrs-core). Matching one of them from another crate now needs a wildcard arm,
which is itself the break — and this is the last moment it can be taken, because the
attribute is what buys the whole of1.xa new screen, a new effect, a new sortable
column, a new palette command or a new retained metric without a major bump.
MetricStateis deliberately excluded: there a consumer's exhaustive match is the
protection rather than an inconvenience, and a new availability state should cost a
major bump, because every one of them has to be handled deliberately. (FilterPattern
inmonitrs-corecarried the attribute already, so seven public enums carry it in
total;CONTRIBUTING.mdlists all seven with their file paths.) SparklineCaret::with_noteis replaced bywith_note_segments, which takes the note
as priority-ordered segments instead of one finished string. Only the widget knows which
side of the caret the note lands on and how much room that side has, so only the widget
can decide what fits; a caller sizing one string against the row's full width produced
notes that vanished outright for most scrub positions on a 160-column Overview.Effect::SetSensorInterest(bool)is a new variant — how the visible screen tells the
sampler to read the sensors every 5 seconds instead of every 30.SensorSnapshot::hottestis deprecated rather than removed. Itsfresh()filter
returnsNonefor exactly the retained readings that became normal in this release, so
it is now a trap rather than a convenience; the header's own use of it was the first
casualty. It still compiles and still means what it always meant, and its deprecation
note names the replacement, which yields the value together with its age. Removal
waits for a later major version, per the deprecation policy inCONTRIBUTING.md.
And two changes that break nothing and still need saying:
- The JSON export's usual shape changed, and
schema_versionis still2. No field
was removed and none was renamed, so the version does not move — but the variant
frequency inverted. Sensors are stale most of the time now, so a consumer of the command
palette'sexport snapshot <path>that readssensors.temperatures.availableused to
find it on every tick and will now usually findsensors.temperatures.staleinstead,
with the reading under.valueand its age beside it. The same goes for
sensors.battery. Both shapes are recorded indocs/schema/v2.jsonand both are
guarded. Themonitrs snapshot --format jsonsubcommand is unaffected: it takes a single
sample with every tier due, so its sensors are always freshly measured. sampling.slow_intervalandsampling.medium_intervalgovern different things now,
with no key added, removed or renamed.slow_interval(30 s) sets the sensor read while
nobody is looking at a sensor panel;medium_interval(5 s) sets it only while the
Battery screen is open, where it used to set it always. A configuration file written
before1.0.0still loads and still means what it says about the tiers.
docs/configuration.mdis the record, because a frozen key
whose meaning moves is precisely the thing none of the four guards can see.
Fixed
- Below 92 columns the tab strip lost every screen name, including the active one's —
a regression0.2.0listed in its own Known limitations. The active screen keeps its
title, bracketed, and only the other six condense to bare digits, so the one piece of
chrome that says where you are survives the narrow bands. The threshold is computed from
the room the footer's key hints leave rather than compared against a constant, so it
moves with them: 92 columns with the two hints a live view shows, 100 while the timeline
is frozen and a third appears.
Known limitations
Compiled by re-reading 0.2.0's list and checking each item against the code and
docs/benchmarks.md, rather than by copying it forward — plus what
this release itself leaves open. Every figure below comes from that file.
- The idle self-CPU p95 still misses §16.1's budget, and closing it is what this release
was built for. Median 0.60–0.85% against 1%, which passes; p95 4.30–9.50%
against 2%, which does not. The previous figures were 0.5–1.1% and 6–11%, so the p95
improved and the two median ranges overlap — "barely moved" is the honest reading of the
median. With the Battery screen visible, where the sensors return to five seconds, the
median is clearly worse (1.20–1.70%) while the p95 (6.00–8.30%) does not separate
from the Overview row's at all. §16.1's gate is about idle, and idle is the Overview row. - The cause this release was designed around turned out to be the wrong one, which is
the most useful thing this section can tell you. The plan assumed the ~85 mssysinfo
temperature read on the 5-second tier was what the budget was paying for. A thread- and
process-CPU instrument built afterwards bounded that read at about 4 ms of CPU: the
85 ms is a blocking wait on in-kernel IOKit calls, and §16.1 budgets CPU. What is
measured to cost CPU is the medium tier's remaining work — its two filesystem-capacity
reads — at 13.2–35.0 ms beyond a fast-only tick, positive in 15 of 15 runs, against
a whole-tick budget of roughly 16 ms. Which of the two reads carries it was never
separated, and where the sensor read's 85 ms actually goes is a hypothesis rather than a
finding. The sensor work stands on its own merits regardless: the wall-clock cost is
real, a 136 ms tick occupies the sampler thread whether or not it burns CPU, and a
retained reading that says its age is right whatever the budget does. - Every figure in this release was measured against 981–1007 processes, five times
§16.1's 200-process, 8-CPU reference workload, and the dominant costs scale with process
count. The budget has never been read on the workload it was written for.
scripts/measure-overhead.pycan now take that reading, and prints the host's real
process count beside its verdict so it cannot call a mismatched host a reference
measurement. The run is owed;docs/benchmarks.mdsays in advance what each outcome
means. - The twelve-hour soak was deferred until after this release, by decision. It is a
blocking gate, and it did not lapse: on 2026-08-01 the maintainer decided to tag1.0.0
first and run the soak after, anddocs/release-checklist.md's step 5 records the
decision, its reasoning, and the deadline — all three runs within seven days of the tag.
What exists instead is a 30-minute run with the shipped collector, which shows no growth:
resident size fell, retained history stayed bounded, file descriptors were flat at 3, and
nothing was dropped. §16.1 asks for twelve hours, so that is the right shape rather than
the gate. The trade being made is that a leak is a1.0.1patch and cannot touch what
this release froze. What the trade does not cover is that nothing has been soaked
on Linux at all, which is also the only configuration in which the descriptor budget is
exercised — so the Linux run is the one carrying real unknown. If the soak finds
something, it will appear in1.0.1's notes and indocs/soak-testing.md. - Two of the seven screens have never been seen on Linux. The Battery screen's Linux
collector is tested from captured/sys/class/power_supplyfixtures and the inode reader
fromstatfserrno cases, both on macOS; no Linux machine has drawn either. Every frame
indocs/screenshots/is a macOS frame. - Four of the six published archives have never been executed. Every archive's checksum
and build attestation verifies, and the two macOS ones have been run from the published
tarball — the x86_64 one only under Rosetta, where it reports temperatures as
unsupported. Nobody has run the four Linux archives on their own hardware. Unchanged
since0.1.0. - A slow terminal can still block a frame for an unbounded time, and no instrument here can
see it: frame time is measured through ratatui'sTestBackend, which stops short of the
write to the terminal, and the soak harness has no renderer. Whether anything constitutes
a redraw busy loop, as opposed to the idle-redraw interval the reducer enforces, is
likewise unmeasured. - Device busy time remains unsupported on macOS: there is no documented API for it. On
macOS the Battery screen's cycles, capacity, wear, pack temperature and watts readn/a
for the related reason — they live in the undocumentedAppleSmartBatteryregistry
properties §9.3 forbids this build from reading — and are real on Linux. - PSI is Linux-only, and says
n/aon macOS rather than promising a value that will never
arrive. - Timestamps are UTC and labelled
Z: no time-zone database is bundled.
Verifying a download
sha256sum --check --ignore-missing SHA256SUMS
gh attestation verify monitrs-*.tar.gz --repo gaborini/monitrs