Summary
Distortion windows are currently hardwired to a time-of-day anchor: TimeWindow resolves its bounds via ClockTime.forTodayInMillis() / forTomorrowInMillis(), re-resolving to "today" on every read. This is elegant for the canonical bedside-clock use case, but it blocks three things people will reasonably want — multi-day spans, a stopwatch, and a countdown — all of which turn out to be the same missing abstraction.
This issue proposes generalizing the anchor of a TimeWindow so a window can be pinned to one of:
- time-of-day (today's behavior, unchanged) — e.g. "1:00am–7:00am"
- absolute instant — e.g. "starting 2026-12-25T01:00Z, for 6h"
- elapsed-since-start — e.g. "from 0:00 to 0:30 of this run"
One seam, three payoffs.
Motivation
a) Multi-day
"Multi-day" is really two features, and the current model is ambiguous about both:
- Windows are times-of-day and re-anchor to today on every read (
windowStartInMillis → forTodayInMillis), which implicitly means they recur daily.
- But
Clock._offset accumulates monotonically and is never reset, which implies a single continuous timeline.
These conflict across midnight. Concretely:
- Cross-midnight integration bug. When
Date.now() and Clock._lastCheck straddle a midnight re-anchoring boundary, the offset integration in Clock.offset misbehaves. (The companion clockblocker-web demo only avoids this by pinning the whole simulation to a single calendar day.)
- Sparse-read correctness gap.
Clock.offset clamps each window's contribution with Math.min(window.durationInMillis, now - windowStart, timeSinceLastCheck, windowEnd - lastCheck) against today's re-anchored window. So if the clock is read sparsely (read → idle 3 days → read), it integrates only today's instance of a daily window and silently skips the intervening days.
- No true multi-day span.
windowEndInMillis only rolls forward to tomorrow when end < start; it cannot express a window longer than ~24h (e.g. "dilate continuously for 3 days").
b) Stopwatch mode
A stopwatch is counting up with warped elapsed time: relativeTimeInMillis - startInMillis. The Clock already computes exactly the warped-elapsed-time offset this needs — a stopwatch is a thinner abstraction than the wall clock, not a heavier one. The only blocker is that windows are time-of-day; a stopwatch wants windows anchored to its own zero ("from 0:00–0:30 of the run, dilate").
c) Countdown mode
A countdown is targetInMillis - relativeTimeInMillis, counting down to zero. This is highly compelling: distortions can make a countdown crawl early and sprint at the end while still hitting zero on the real deadline (when dilation debt is fully repaid by later compression). The clockblocker-web "Escape Room" scenario demonstrates this behavior today using the wall-clock API; a first-class countdown mode would express it directly. Same blocker: windows need an elapsed/relative anchor instead of time-of-day.
Proposed direction
Abstract the anchor out of TimeWindow (today it implicitly calls ClockTime.forTodayInMillis). Sketch:
type WindowAnchor =
| { kind: 'timeOfDay'; start: ClockTime; /* recurrence: 'none' | 'daily' */ }
| { kind: 'absolute'; startInstant: Temporal.ZonedDateTime }
| { kind: 'elapsed'; startOffsetMs: number }; // relative to a run's t=0
time-of-day → preserves current behavior. Consider making recurrence explicit (none | daily) so "just tonight" vs "every night" is a choice rather than a side effect of re-anchoring — this also resolves the cross-midnight ambiguity.
absolute → enables multi-day spans and unambiguous one-shot windows.
elapsed → enables stopwatch and countdown, where windows are relative to the run's start.
Clock keeps its offset integrator largely as-is; what changes is how a window resolves windowStartInMillis / windowEndInMillis and how compareWithinWindow is evaluated. Wall clock / stopwatch / countdown then become thin presentation layers over the same engine:
- wall clock:
now + offset
- stopwatch:
(now + offset) - runStart
- countdown:
target - (now + offset)
Suggested sequencing
- Fix cross-midnight integration + sparse-read gap (worth doing on its own; latent correctness bug regardless of new features).
- Introduce the anchor abstraction with
timeOfDay (current behavior) + elapsed.
- Add
Stopwatch and Countdown facades over the elapsed anchor.
- Add
absolute anchor for true multi-day spans; extend windowEndInMillis beyond the single overnight wrap.
Out of scope / open questions
- Recurrence semantics for time-of-day windows (
none | daily, and whether _offset resets per recurrence).
- Whether
absolute windows should also support recurrence (e.g. weekly), or stay one-shot.
- Backward-compatibility: the current
ClockTimeDescriptor constructor signature should keep working (default to timeOfDay).
Summary
Distortion windows are currently hardwired to a time-of-day anchor:
TimeWindowresolves its bounds viaClockTime.forTodayInMillis()/forTomorrowInMillis(), re-resolving to "today" on every read. This is elegant for the canonical bedside-clock use case, but it blocks three things people will reasonably want — multi-day spans, a stopwatch, and a countdown — all of which turn out to be the same missing abstraction.This issue proposes generalizing the anchor of a
TimeWindowso a window can be pinned to one of:One seam, three payoffs.
Motivation
a) Multi-day
"Multi-day" is really two features, and the current model is ambiguous about both:
windowStartInMillis→forTodayInMillis), which implicitly means they recur daily.Clock._offsetaccumulates monotonically and is never reset, which implies a single continuous timeline.These conflict across midnight. Concretely:
Date.now()andClock._lastCheckstraddle a midnight re-anchoring boundary, the offset integration inClock.offsetmisbehaves. (The companionclockblocker-webdemo only avoids this by pinning the whole simulation to a single calendar day.)Clock.offsetclamps each window's contribution withMath.min(window.durationInMillis, now - windowStart, timeSinceLastCheck, windowEnd - lastCheck)against today's re-anchored window. So if the clock is read sparsely (read → idle 3 days → read), it integrates only today's instance of a daily window and silently skips the intervening days.windowEndInMillisonly rolls forward to tomorrow whenend < start; it cannot express a window longer than ~24h (e.g. "dilate continuously for 3 days").b) Stopwatch mode
A stopwatch is counting up with warped elapsed time:
relativeTimeInMillis - startInMillis. TheClockalready computes exactly the warped-elapsed-timeoffsetthis needs — a stopwatch is a thinner abstraction than the wall clock, not a heavier one. The only blocker is that windows are time-of-day; a stopwatch wants windows anchored to its own zero ("from 0:00–0:30 of the run, dilate").c) Countdown mode
A countdown is
targetInMillis - relativeTimeInMillis, counting down to zero. This is highly compelling: distortions can make a countdown crawl early and sprint at the end while still hitting zero on the real deadline (when dilation debt is fully repaid by later compression). Theclockblocker-web"Escape Room" scenario demonstrates this behavior today using the wall-clock API; a first-class countdown mode would express it directly. Same blocker: windows need an elapsed/relative anchor instead of time-of-day.Proposed direction
Abstract the anchor out of
TimeWindow(today it implicitly callsClockTime.forTodayInMillis). Sketch:time-of-day→ preserves current behavior. Consider making recurrence explicit (none | daily) so "just tonight" vs "every night" is a choice rather than a side effect of re-anchoring — this also resolves the cross-midnight ambiguity.absolute→ enables multi-day spans and unambiguous one-shot windows.elapsed→ enables stopwatch and countdown, where windows are relative to the run's start.Clockkeeps its offset integrator largely as-is; what changes is how a window resolveswindowStartInMillis/windowEndInMillisand howcompareWithinWindowis evaluated. Wall clock / stopwatch / countdown then become thin presentation layers over the same engine:now + offset(now + offset) - runStarttarget - (now + offset)Suggested sequencing
timeOfDay(current behavior) +elapsed.StopwatchandCountdownfacades over the elapsed anchor.absoluteanchor for true multi-day spans; extendwindowEndInMillisbeyond the single overnight wrap.Out of scope / open questions
none | daily, and whether_offsetresets per recurrence).absolutewindows should also support recurrence (e.g. weekly), or stay one-shot.ClockTimeDescriptorconstructor signature should keep working (default totimeOfDay).