From 4d8a7852fd6b761903216a1acb92fa2f662c2f71 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 5 Sep 2026 22:43:07 -0700 Subject: [PATCH 1/2] fix(website): pin the stage hold one millisecond inside it so the frame reports pause Verified against production after #1032 deployed: the frame went ready, applied exactly through the interrupt (stream:930), but reported phase `stream` at the hold because the website pinned t at hold.startMs and the frame's phaseReachedAt treats a boundary instant as the outgoing run's. Co-Authored-By: Claude Fable 5.1 --- apps/website/src/lib/stage-beats.spec.ts | 7 ++++--- apps/website/src/lib/stage-beats.ts | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/website/src/lib/stage-beats.spec.ts b/apps/website/src/lib/stage-beats.spec.ts index 47e4e84e6..c3bf5a303 100644 --- a/apps/website/src/lib/stage-beats.spec.ts +++ b/apps/website/src/lib/stage-beats.spec.ts @@ -80,9 +80,10 @@ describe('timeAt', () => { it('pins time at the interrupt through the hold and resumes past the threshold', () => { const a = beatWindows()[2]; const at = (f: number) => timeAt(a.from + (a.to - a.from) * f, READY); - expect(at(APPROVE_HOLD.from)).toBe(READY.hold.startMs); - expect(at(0.5)).toBe(READY.hold.startMs); - expect(at(APPROVE_HOLD.to - 1e-6)).toBe(READY.hold.startMs); + // Pinned one millisecond inside the hold, so the frame reports `pause`. + expect(at(APPROVE_HOLD.from)).toBe(READY.hold.startMs + 1); + expect(at(0.5)).toBe(READY.hold.startMs + 1); + expect(at(APPROVE_HOLD.to - 1e-6)).toBe(READY.hold.startMs + 1); expect(at(APPROVE_HOLD.to)).toBe(READY.hold.endMs); expect(at(1)).toBe(READY.beats[2].endMs); }); diff --git a/apps/website/src/lib/stage-beats.ts b/apps/website/src/lib/stage-beats.ts index 7afeaa852..32048de36 100644 --- a/apps/website/src/lib/stage-beats.ts +++ b/apps/website/src/lib/stage-beats.ts @@ -114,7 +114,10 @@ export function timeAt(p: number, ready: StageReadyMessage): number { return Math.round( lerp(b.startMs, ready.hold.startMs, f / APPROVE_HOLD.from) ); - if (f < APPROVE_HOLD.to) return ready.hold.startMs; + // One millisecond INSIDE the hold: the frame treats a boundary instant as + // belonging to the outgoing run (phaseReachedAt subtracts an epsilon), so + // pinning at hold.startMs exactly would report `stream`, not `pause`. + if (f < APPROVE_HOLD.to) return Math.min(ready.hold.startMs + 1, ready.hold.endMs); return Math.round( lerp( ready.hold.endMs, From 709b4581e060829efd41ff4c3fe7e08f90361aad Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 5 Sep 2026 22:43:40 -0700 Subject: [PATCH 2/2] fix(website): the pre-hold ramp ends on the pinned hold value so the seam is continuous Co-Authored-By: Claude Fable 5.1 --- apps/website/src/lib/stage-beats.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/website/src/lib/stage-beats.ts b/apps/website/src/lib/stage-beats.ts index 32048de36..e661945e2 100644 --- a/apps/website/src/lib/stage-beats.ts +++ b/apps/website/src/lib/stage-beats.ts @@ -110,14 +110,13 @@ export function timeAt(p: number, ready: StageReadyMessage): number { : Math.round(lerp(mid, b.endMs, (f - 0.5) / 0.5)); } case 'approve': { + const holdPin = Math.min(ready.hold.startMs + 1, ready.hold.endMs); if (f < APPROVE_HOLD.from) - return Math.round( - lerp(b.startMs, ready.hold.startMs, f / APPROVE_HOLD.from) - ); + return Math.round(lerp(b.startMs, holdPin, f / APPROVE_HOLD.from)); // One millisecond INSIDE the hold: the frame treats a boundary instant as // belonging to the outgoing run (phaseReachedAt subtracts an epsilon), so // pinning at hold.startMs exactly would report `stream`, not `pause`. - if (f < APPROVE_HOLD.to) return Math.min(ready.hold.startMs + 1, ready.hold.endMs); + if (f < APPROVE_HOLD.to) return holdPin; return Math.round( lerp( ready.hold.endMs,