What happened
Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.
Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.
Why it happens
CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.
apps/web/src/components/coil/UpdateToast.tsx:65 — const [now, setNow] = useState(() => Date.now()), evaluated once at mount.apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.
selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.
Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.
Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.
The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.
Expected
The age advances while the toast is on screen: just now → 12 min ago → 1h ago → 8h ago.
Suggested fix
Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:
- Run the interval when
autoRestart !== undefinedorstate.status.kind === "ready". - One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and
formatBuiltAgo's finest bucket is a minute. - Re-seed
now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.
Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.
Surfaces
Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.
Tests
updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.
What happened
Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.
Toast contents at the time of the screenshot:
20 changes ready to run/ "Restart to update T3 Coil." /built just nowin the top-right /Restart when idle+Restart.Why it happens
CoilUpdateToastseeds its clock once and only ticks it while an auto-restart is armed.apps/web/src/components/coil/UpdateToast.tsx:65—const [now, setNow] = useState(() => Date.now()), evaluated once at mount.apps/web/src/components/coil/UpdateToast.tsx:71— theCEILING_TICK_MSinterval bails out withif (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.selectUpdateToastViewthreads thatnowintoformatBuiltAgo(builtAt, now)(updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over
bridge.onState, andbuiltAgogets computed against anowthat predatesbuiltAt. That's a negative age, whichformatBuiltAgodeliberately clamps to"just now"(updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.
The
hours/daysbranches offormatBuiltAgoare, as far as I can tell, unreachable in practice today.Expected
The age advances while the toast is on screen:
just now→12 min ago→1h ago→8h ago.Suggested fix
Tick
nowwhenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule inAGENTS.md:autoRestart !== undefinedorstate.status.kind === "ready".formatBuiltAgo's finest bucket is a minute.nowon thereadytransition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.Cost check:
viewKeyisJSON.stringify(view), so a tick that doesn't change the bucket produces notoastManager.update— a minute-tickingsetNowcosts one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.Surfaces
Desktop only. The feature is gated on
isElectronand there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.Tests
updateToast.logic.test.tscoversformatBuiltAgoat fixednowvalues and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs onreadywithout an arm.