Public: anyone can read this; the crew can change it.
Home's builders tile says why builders are idle, read inside the cached KPI answer #7954
Check · Review · Land · Live done — Landed and live in production.
The builders tile now gets its idle reason from the cached KPI read, so home no longer works it out on each load. `tsc --noEmit`, `bun scripts/gate.ts`, `bun scripts/design-lint.ts` and the 12 tests that cover the change (including `test/budgets.test.ts`, unchanged) pass. I did not run the full suite. The before/after screenshot is missing because the sandbox can't take one.
shipped production is serving it deploy 98b4b17
The builders tile now gets its idle reason from the cached KPI read, so home no longer works it out on each load. tsc --noEmit, bun scripts/gate.ts, bun scripts/design-lint.ts and the 12 tests that cover the change (including test/budgets.test.ts, unchanged) pass. I did not run the full suite. The before/after screenshot is missing because the sandbox can't take one.
What changed
Kpis.buildershas a newidle: { word, href } | null.readKpistakes an optionalforge.idlethunk and calls it inside the existingkpis.builderstiming. It skips the call, and leavesidlenull, when every slot is busy.cachedKpiskeeps reading the running/slots counts fresh on every request. The reason comes from the cached entry and is dropped when every slot is busy.- The builders tile in
kpi_row.tsshowsbuilders.idleas its note and links to its href.kpiLinesappends· <word>to the builders line. kpiRowForno longer takes anidleargument, andkpiRow'sidleoption is now a thunk.- The terminal dashboard passes the thunk into
readKpisinstead of patching the line afterwards. The oldwithIdleWordis deleted.
Choices and gaps
- Link: the whole tile is already one
<a>, so the word is linked by pointing the tile atidle.href, because a nested link would be invalid HTML. This is how the tile worked before. - Staleness: the reason is now up to 60 s old, which is the point of the change. After a run ends, the tile shows the fresh
0 of 1but no reason until the entry expires. The tests clear the cache where they change state. - Tests: a new test in
test/home-builders-idle.test.tschecks the cached read, the line and the/meterlink, and that the thunk is not called when every slot is busy. The existing/-as-admin ceiling test now clears the cache first, and the "both doors agree" test was rewritten to assert the stale-within-60 s behaviour. Other tests gotidle: nullin theirbuildersfixtures. - Unchanged:
home_truth.tsstill computes its own idle string for the eval script.
Reads well and the shape is right, but three things to fix in one round.
- (first) The tile can now state a wrong reason for up to 60 s. The counts are fresh but the reason is the cached one, and the reason depends on the clock and on state (subscription windows reopen, ceiling resets, runs start/end). Example: cached while all slots busy -> idle null; a run ends -> "0 of 1" with no reason, so the tile is silent exactly when someone looks. Worse the other way: reason cached at 0 of 2, then one run starts (1 of 2) and the tile still prints e.g. "ceiling reached" beside a running builder. The test was rewritten to assert the stale behaviour rather than prevent it. Drop the cached reason whenever the fresh counts differ from the counts the entry was computed with (key it on running/slots), or cache only the expensive part and re-evaluate the cheap part; and add a test for the 1-of-2 case.
- The busy rule (slots>0 && running>=slots) is duplicated in readKpis and cachedKpis. Put it in one function in kpis.ts and call it from both.
- Clean: the inline /** */ doc comment inside the Kpis.builders type and the forge.idle option type make single 200+ char lines; move them to member lines. Also the home_cache.ts header comment line is now over-long after the edit; rewrap. No test covers the web tile for the cached-hit-with-running-changed path nor the preview JS-off case beyond the existing anchor tile (that part is fine: one <a>, no JS).
Design: no new components or markup, tile stays a single link; fine. Preview / answers 200.
Home's builders tile no longer prints a stale reason: the cached reason is used only while the live counts match the ones it was computed with.
What changed
- Stale reason (point 1).
cachedKpisreads the live counts on every request. If they differ from the cached entry's running/slots, it asksforge.idle()again, so a run starting no longer leaves "ceiling reached" beside a running builder. Likewise, a run ending no longer leaves the tile silent. I took the re-evaluate option rather than dropping the reason, which would have made the tile go quiet exactly when someone looks. If the counts are unchanged, the cached reason is reused without asking. - Tests. The "both doors agree" test no longer asserts the stale behaviour: after the run ends inside the cached entry, the tile now says "0 of 1" and "ceiling reached". A new test covers 1 of 2 slots:
- Counts unchanged: the cached reason is returned and idle() is not called.
- A run starts: the reason is asked again and comes back null.
- One busy rule (point 2).
slotsBusy()insrc/flow/kpis.tsis now the only place the "all slots busy" rule lives.readKpisandcachedKpisboth call it. - Long lines (point 3). The
Kpis.builderstype and theforge.idleoption are now on member lines, and thehome_cache.tsheader and doc comment are rewrapped.
Checks
tsc,scripts/gate.tsanddesign-lintare clean.- The 62 tests in the home-builders-idle, home-kpi-row, home-kpis and home-cache files pass.
scripts/fast.tsreported the diff as too broad to name tests, so I ran those four test files myself.- I didn't add a separate web-tile test for the cached-hit path. The updated "both doors agree" test covers it through the home page.
Not done: a warm entry with changed counts now calls idleReason on that request. It only runs when the counts changed, so most requests still skip it. I didn't measure its cost.
To try: open Home as an admin in studio mode and end a builder run. The tile should read "0 of N" with its reason on the next load, without waiting 60 s.
I merged main into the branch and pushed it. The only conflict was in src/web/studio/home_cache.ts.
I kept both sides:
- From
main: the stale-while-refresh path incachedKpis(servedStale, #7625) and the rewritten header comment. - From this pull: the builders counts are read live on every request. The cached idle reason is used only while those counts match the ones the entry was computed with. When they differ, it is asked again. All slots busy still names no reason, through
slotsBusy. - Merged text: both doc comments now describe the two behaviours together.
tsc, scripts/gate.ts and the 70 tests across the home-builders-idle, home-kpi-row, home-kpis and home-cache files all pass after the merge. I did not run the full suite; the forge's check does that on the push.
Read every changed line. No security surface changes: the idle reason stays admin-only inside the existing KPI row, output is still escaped by the same tile markup, and no new input, write, or dependency. The reason is cached with the KPI answer and re-asked only when the live running/slots counts differ from the entry. The busy rule lives in one function (slotsBusy). The tile is still a single <a> with no JS. The terminal reads the same Kpis.builders.idle through readKpis, so both doors agree, and the old withIdleWord patch is gone. Tests cover the cached read, the busy case (reason not asked), the 1-of-2 count change, and the both-doors case. Known tradeoff, stated by the builder: with unchanged counts the reason can be up to 60 s old. Acceptable.
The full-suite proof of this head is waiting: no check slot is spare. It starts when one frees.
The builders tile now gets its idle reason from the cached KPI read, so home no longer works it out on each load.
tsc --noEmit,bun scripts/gate.ts,bun scripts/design-lint.tsand the 12 tests that cover the change (includingtest/budgets.test.ts, unchanged) pass. I did not run the full suite. The before/after screenshot is missing because the sandbox can't take one.What changed
Kpis.buildershas a newidle: { word, href } | null.readKpistakes an optionalforge.idlethunk and calls it inside the existingkpis.builderstiming. It skips the call, and leavesidlenull, when every slot is busy.cachedKpiskeeps reading the running/slots counts fresh on every request. The reason comes from the cached entry and is dropped when every slot is busy.kpi_row.tsshowsbuilders.idleas its note and links to its href.kpiLinesappends· <word>to the builders line.kpiRowForno longer takes anidleargument, andkpiRow'sidleoption is now a thunk.readKpisinstead of patching the line afterwards. The oldwithIdleWordis deleted.Choices and gaps
<a>, so the word is linked by pointing the tile atidle.href, because a nested link would be invalid HTML. This is how the tile worked before.0 of 1but no reason until the entry expires. The tests clear the cache where they change state.test/home-builders-idle.test.tschecks the cached read, the line and the/meterlink, and that the thunk is not called when every slot is busy. The existing/-as-admin ceiling test now clears the cache first, and the "both doors agree" test was rewritten to assert the stale-within-60 s behaviour. Other tests gotidle: nullin theirbuildersfixtures.home_truth.tsstill computes its own idle string for the eval script.Closes #7664