From aa3d7fcdf7ecd72f85e14ae933a5511b7e1c3c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 21 Jun 2026 20:54:33 +0200 Subject: [PATCH 1/2] docs(sleep): correct the sleep-rhythm peer-card window comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment claimed the average-per-night, sleep-debt, and chronotype cards share one window and cannot disagree. They each read their own sub-window — the average over the full scorable span, sleep-debt over its trailing 14 nights, chronotype over CHRONOTYPE_WINDOW_NIGHTS — so the figures are each internally honest rather than identical, and every card's caption discloses its own nightsCounted. State that plainly; no computation changes. --- src/lib/insights/derived/sleep-rhythm.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/lib/insights/derived/sleep-rhythm.ts b/src/lib/insights/derived/sleep-rhythm.ts index ec3cbbaff..e9a2eade5 100644 --- a/src/lib/insights/derived/sleep-rhythm.ts +++ b/src/lib/insights/derived/sleep-rhythm.ts @@ -75,10 +75,14 @@ export interface SleepDebtDto { * Wire DTO for the average sleep per night over the rhythm window. * * v1.19.1 — a third peer card next to sleep-debt + chronotype. The mean of the - * canonical per-night asleep totals over the same scorable-night set the debt - * and chronotype read, so the three cards can never contradict one another. - * Carries the same calm `partial` state under a minimum-night threshold so it - * never asserts an average off one or two thin nights. + * canonical per-night asleep totals over the full scorable span (up to + * `DEFAULT_WINDOW_DAYS`). Each of the three cards reads its OWN sub-window — + * sleep-debt caps to 14 nights, chronotype to `CHRONOTYPE_WINDOW_NIGHTS`, this + * average over every scorable night — so the figures are each internally honest + * rather than identical; the card caption discloses its `nightsCounted` so the + * reader sees exactly which span produced the number. Carries the same calm + * `partial` state under a minimum-night threshold so it never asserts an average + * off one or two thin nights. */ export interface AverageSleepDto { state: "partial" | "ready"; @@ -261,8 +265,11 @@ export function computeSleepRhythmFromNights( })); const chronotype = computeChronotype(chronoNights); - // Average sleep per night over the same scorable set the debt + chronotype - // read, so the three peer cards share one window and can't disagree. + // Average sleep per night over the full scorable span. Each of the three peer + // cards reads its OWN window — debt slices its trailing 14 nights, chronotype + // its trailing CHRONOTYPE_WINDOW_NIGHTS, this average every scorable night — + // so the figures are each internally honest rather than identical; each card's + // caption shows its own nightsCounted so the reader sees the span behind it. const averagePerNight = computeAverageSleep(scorable); return { From 1bf8f3d6619e2aa141ff257febfd313488579db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 21 Jun 2026 20:54:39 +0200 Subject: [PATCH 2/2] refactor(coach): drop the unread streaming startedAt and reasoning state The thinking disclosure was removed, so the coach streaming bubble no longer reads startedAt or reasoning. Both fields stayed populated by the send hook but nothing consumed them. Remove them from CoachStreamingMessage and every writer, accept the additive reasoning SSE frame as an explicit no-op so the wire type stays recognised, and update the thread-test fixture. Streaming is otherwise unchanged. --- .../__tests__/message-thread.test.tsx | 10 ++-- .../insights/coach-panel/use-coach.ts | 53 +++---------------- 2 files changed, 10 insertions(+), 53 deletions(-) diff --git a/src/components/insights/coach-panel/__tests__/message-thread.test.tsx b/src/components/insights/coach-panel/__tests__/message-thread.test.tsx index aa20016d2..dd9356ed3 100644 --- a/src/components/insights/coach-panel/__tests__/message-thread.test.tsx +++ b/src/components/insights/coach-panel/__tests__/message-thread.test.tsx @@ -62,10 +62,10 @@ import { import type { CoachConversationDetailDTO } from "@/lib/ai/coach/types"; import type { CoachStreamingMessage } from "../use-coach"; -// v1.18.9 — `CoachStreamingMessage` gained `usage` / `startedAt` / -// `reasoning` (per-message token footer + thinking disclosure). The -// fixtures below predate those fields; this builder fills the defaults so -// each test only spells out the fields it cares about. +// v1.18.9 — `CoachStreamingMessage` gained a `usage` field for the +// per-message token footer. The fixtures below predate it; this builder +// fills the defaults so each test only spells out the fields it cares +// about. function streaming( partial: Partial, ): CoachStreamingMessage { @@ -77,8 +77,6 @@ function streaming( messageId: null, errorCode: null, usage: null, - startedAt: null, - reasoning: "", ...partial, }; } diff --git a/src/components/insights/coach-panel/use-coach.ts b/src/components/insights/coach-panel/use-coach.ts index 5517d5237..db101a0d6 100644 --- a/src/components/insights/coach-panel/use-coach.ts +++ b/src/components/insights/coach-panel/use-coach.ts @@ -199,19 +199,6 @@ export interface CoachStreamingMessage { * bubble; persisted bubbles read `CoachMessageDTO.tokensUsed` instead. */ usage: CoachUsage | null; - /** - * v1.18.9 — epoch-ms timestamp the send fired, so the thinking - * disclosure can show a live "thinking for N s" counter that freezes - * to a past-tense summary once the first token lands. Null before a - * turn starts. - */ - startedAt: number | null; - /** - * v1.18.9 — optional reasoning-summary text from the additive - * `reasoning` frame, rendered inside the thinking disclosure when a - * reasoning-capable provider emits it. Empty string when none. - */ - reasoning: string; } /** @@ -253,8 +240,6 @@ const EMPTY_STREAMING: CoachStreamingMessage = { messageId: null, errorCode: null, usage: null, - startedAt: null, - reasoning: "", }; export interface SendCoachMessageParams { @@ -349,10 +334,6 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { messageId: null, errorCode: null, usage: null, - // v1.18.9 — anchor the thinking-disclosure elapsed timer to the - // moment the send fired. - startedAt: Date.now(), - reasoning: "", }); // v1.4.47 W8 — pre-check navigator.onLine so an airplane-mode @@ -367,8 +348,6 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { messageId: null, errorCode: "coach.network", usage: null, - startedAt: null, - reasoning: "", }); return null; } @@ -403,8 +382,6 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { messageId: null, errorCode: "coach.network", usage: null, - startedAt: null, - reasoning: "", }); return null; } @@ -418,8 +395,6 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { messageId: null, errorCode: `coach.http.${response.status}`, usage: null, - startedAt: null, - reasoning: "", }); return null; } @@ -458,8 +433,6 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { messageId: null, errorCode: structured ?? `coach.http.${response.status}`, usage: null, - startedAt: null, - reasoning: "", }); // v1.16.4 — KEEP the optimistic user bubble: a rejected turn // (budget gate, 4xx) has no persisted twin coming, and dropping @@ -479,7 +452,6 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { let lastError: string | null = null; let messageId: string | null = null; let collectedUsage: CoachUsage | null = null; - let collectedReasoning = ""; try { while (true) { @@ -512,16 +484,10 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { })); break; case "reasoning": - // v1.18.9 — additive reasoning-summary frame. Append so - // multiple chunks accumulate; the disclosure renders it - // when present, else falls back to the elapsed-time label. - // Guard the text: a `reasoning` frame with no `text` must not - // append a literal "undefined". - collectedReasoning += evt.text ?? ""; - setStreaming((prev) => ({ - ...prev, - reasoning: prev.reasoning + (evt.text ?? ""), - })); + // v1.19.1 — the thinking disclosure was removed, so the + // additive `reasoning` frame is accepted off the wire and + // dropped. Kept as an explicit no-op case so the wire frame + // stays a recognised type rather than slipping to a default. break; case "done": resolvedConversationId = evt.conversationId; @@ -544,8 +510,6 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { collectedUsage = evt.usage ?? null; } else if (evt.type === "error") { lastError = evt.code; - } else if (evt.type === "reasoning") { - collectedReasoning += evt.text ?? ""; } } } catch (err) { @@ -554,7 +518,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { } } - setStreaming((prev) => ({ + setStreaming({ content: collectedContent, metricSource: collectedProvenance, suggestion: collectedSuggestion, @@ -562,12 +526,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) { messageId, errorCode: lastError, usage: collectedUsage, - // v1.18.9 — keep the send-anchored timestamp so the disclosure can - // freeze the elapsed time to a past-tense "thought for N s" once - // the turn settles. - startedAt: prev.startedAt, - reasoning: collectedReasoning, - })); + }); if (resolvedConversationId) { // Invalidate the freshly-persisted conversation + the rail so