From 00c1b8268591eb853a655e2f00d2ba5feb1744a8 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:12:35 +0000 Subject: [PATCH 1/7] refactor: remove unused `formatValue` export from toolUtils The function was exported but never imported anywhere in the codebase. Confirmed dead via ts-prune and exhaustive grep. --- src/browser/features/Tools/Shared/toolUtils.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/browser/features/Tools/Shared/toolUtils.tsx b/src/browser/features/Tools/Shared/toolUtils.tsx index 19b35f0c65..152b61797e 100644 --- a/src/browser/features/Tools/Shared/toolUtils.tsx +++ b/src/browser/features/Tools/Shared/toolUtils.tsx @@ -78,21 +78,6 @@ export function getStatusDisplay(status: ToolStatus): React.ReactNode { } } -/** - * Format a value for display (JSON or string) - */ -export function formatValue(value: unknown): string { - if (value === null || value === undefined) return "None"; - if (typeof value === "string") return value; - if (typeof value === "number" || typeof value === "boolean") return String(value); - try { - return JSON.stringify(value, null, 2); - } catch { - // If JSON.stringify fails (e.g., circular reference), return a safe fallback - return "[Complex Object - Cannot Stringify]"; - } -} - /** * Type guard for ToolErrorResult shape: { success: false, error: string }. * Use this when you need type narrowing to access error. From a0f9e541c4dd7132f90eb55ec6ccc864c4e08c7d Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:18:59 +0000 Subject: [PATCH 2/7] ci: retrigger after flaky SigningService timeout From 403d092ccece68fa663d0327d873958ecfe2cfed Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:12:40 +0000 Subject: [PATCH 3/7] refactor: deduplicate secret type guards in Config class Remove private static isSecretReferenceValue() and isOpSecretValue() from Config class, replacing them with the identical shared exports from @/common/types/secrets. Both methods had byte-identical logic to their shared counterparts and config.ts already imported from that module. --- src/node/config.ts | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index ffda84f6c1..4294972d78 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -5,7 +5,12 @@ import * as jsonc from "jsonc-parser"; import writeFileAtomic from "write-file-atomic"; import { log } from "@/node/services/log"; import type { WorkspaceMetadata, FrontendWorkspaceMetadata } from "@/common/types/workspace"; -import { type Secret, type SecretsConfig } from "@/common/types/secrets"; +import { + isSecretReferenceValue, + isOpSecretValue, + type Secret, + type SecretsConfig, +} from "@/common/types/secrets"; import type { Workspace, ProjectConfig, @@ -1203,30 +1208,12 @@ ${jsonString}`; return stripTrailingSlashes(projectPath); } - private static isSecretReferenceValue(value: unknown): value is { secret: string } { - return ( - typeof value === "object" && - value !== null && - "secret" in value && - typeof (value as { secret?: unknown }).secret === "string" - ); - } - - private static isOpSecretValue(value: unknown): value is { op: string } { - return ( - typeof value === "object" && - value !== null && - "op" in value && - typeof (value as { op?: unknown }).op === "string" - ); - } - private static isSecretValue(value: unknown): value is Secret["value"] { if (typeof value === "string") { return true; } - return Config.isSecretReferenceValue(value) || Config.isOpSecretValue(value); + return isSecretReferenceValue(value) || isOpSecretValue(value); } private static isSecret(value: unknown): value is Secret { @@ -1383,12 +1370,12 @@ ${jsonString}`; try { const raw = globalRawByKey.get(key); - if (typeof raw === "string" || Config.isOpSecretValue(raw)) { + if (typeof raw === "string" || isOpSecretValue(raw)) { globalResolved.set(key, raw); return raw; } - if (Config.isSecretReferenceValue(raw)) { + if (isSecretReferenceValue(raw)) { const target = raw.secret.trim(); if (!target) { globalResolved.set(key, undefined); @@ -1416,7 +1403,7 @@ ${jsonString}`; } return projectSecrets.map((secret) => { - if (!Config.isSecretReferenceValue(secret.value)) { + if (!isSecretReferenceValue(secret.value)) { return secret; } From c9f31ffecfa40322b3375058fffb7515ab332192 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:24:14 +0000 Subject: [PATCH 4/7] refactor: extract shared runAnalyticsEffect helper in useAnalytics All 9 reactive analytics hooks repeated the same ~45-line boilerplate: API readiness check, analytics namespace validation, ignore-flag cancellation, and error handling. Extract this into a single runAnalyticsEffect() helper that each hook delegates to. Behavior-preserving: identical logic, same exported signatures. -401 lines, +95 lines (net -306 lines). --- src/browser/hooks/useAnalytics.ts | 496 ++++++------------------------ 1 file changed, 95 insertions(+), 401 deletions(-) diff --git a/src/browser/hooks/useAnalytics.ts b/src/browser/hooks/useAnalytics.ts index 09e1f26468..62e006b424 100644 --- a/src/browser/hooks/useAnalytics.ts +++ b/src/browser/hooks/useAnalytics.ts @@ -1,4 +1,5 @@ import assert from "@/common/utils/assert"; +import type React from "react"; import { useEffect, useState } from "react"; import type { z } from "zod"; import type { APIClient } from "@/browser/contexts/API"; @@ -85,6 +86,61 @@ function getAnalyticsNamespace(api: APIClient): AnalyticsNamespace | null { return maybeNamespace as AnalyticsNamespace; } +/** + * Shared effect body for analytics data-fetching hooks. + * Handles API readiness check, cancellation on unmount/re-render, and error handling. + * Returns the effect cleanup function (or undefined for early-exit paths). + */ +function runAnalyticsEffect( + api: APIClient | null, + setState: React.Dispatch>>, + fetcher: (analyticsApi: AnalyticsNamespace) => Promise +): (() => void) | undefined { + if (!api) { + setState((previousState) => ({ + data: previousState.data, + loading: true, + error: null, + })); + return; + } + + const analyticsApi = getAnalyticsNamespace(api); + if (!analyticsApi) { + setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); + return; + } + + let ignore = false; + setState((previousState) => ({ + data: previousState.data, + loading: true, + error: null, + })); + + void fetcher(analyticsApi) + .then((data) => { + if (ignore) { + return; + } + setState({ data, loading: false, error: null }); + }) + .catch((error: unknown) => { + if (ignore) { + return; + } + setState((previousState) => ({ + data: previousState.data, + loading: false, + error: getErrorMessage(error), + })); + }); + + return () => { + ignore = true; + }; +} + export function useAnalyticsSummary( projectPath?: string | null, dateFilters?: DateFilterParams @@ -100,53 +156,11 @@ export function useAnalyticsSummary( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getSummary({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); - }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getSummary({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) + ); }, [api, projectPath, fromMs, toMs]); return state; @@ -174,58 +188,16 @@ export function useAnalyticsSpendOverTime(params: { }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getSpendOverTime({ + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getSpendOverTime({ projectPath: params.projectPath ?? null, granularity: params.granularity, from: fromDate, to: toDate, }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); - }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + ); }, [api, params.projectPath, params.granularity, fromMs, toMs]); return state; @@ -245,53 +217,11 @@ export function useAnalyticsSpendByProject( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getSpendByProject({ from: fromDate, to: toDate }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); - }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getSpendByProject({ from: fromDate, to: toDate }) + ); }, [api, fromMs, toMs]); return state; @@ -312,53 +242,11 @@ export function useAnalyticsSpendByModel( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getSpendByModel({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); - }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getSpendByModel({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) + ); }, [api, projectPath, fromMs, toMs]); return state; @@ -379,53 +267,15 @@ export function useAnalyticsTokensByModel( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getTokensByModel({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getTokensByModel({ + projectPath: projectPath ?? null, + from: fromDate, + to: toDate, }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + ); }, [api, projectPath, fromMs, toMs]); return state; @@ -452,58 +302,16 @@ export function useAnalyticsTimingDistribution( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getTimingDistribution({ + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getTimingDistribution({ metric, projectPath: projectPath ?? null, from: fromDate, to: toDate, }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); - }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + ); }, [api, metric, projectPath, fromMs, toMs]); return state; @@ -524,53 +332,15 @@ export function useAnalyticsProviderCacheHitRatio( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getCacheHitRatioByProvider({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getCacheHitRatioByProvider({ + projectPath: projectPath ?? null, + from: fromDate, + to: toDate, }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + ); }, [api, projectPath, fromMs, toMs]); return state; @@ -591,53 +361,15 @@ export function useAnalyticsAgentCostBreakdown( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getAgentCostBreakdown({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getAgentCostBreakdown({ + projectPath: projectPath ?? null, + from: fromDate, + to: toDate, }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + ); }, [api, projectPath, fromMs, toMs]); return state; @@ -658,53 +390,15 @@ export function useAnalyticsDelegationSummary( }); useEffect(() => { - if (!api) { - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - return; - } - - const analyticsApi = getAnalyticsNamespace(api); - if (!analyticsApi) { - setState({ data: null, loading: false, error: ANALYTICS_UNAVAILABLE_MESSAGE }); - return; - } - - let ignore = false; - setState((previousState) => ({ - data: previousState.data, - loading: true, - error: null, - })); - const fromDate = fromMs == null ? null : new Date(fromMs); const toDate = toMs == null ? null : new Date(toMs); - - void analyticsApi - .getDelegationSummary({ projectPath: projectPath ?? null, from: fromDate, to: toDate }) - .then((data) => { - if (ignore) { - return; - } - setState({ data, loading: false, error: null }); + return runAnalyticsEffect(api, setState, (analyticsApi) => + analyticsApi.getDelegationSummary({ + projectPath: projectPath ?? null, + from: fromDate, + to: toDate, }) - .catch((error: unknown) => { - if (ignore) { - return; - } - setState((previousState) => ({ - data: previousState.data, - loading: false, - error: getErrorMessage(error), - })); - }); - - return () => { - ignore = true; - }; + ); }, [api, projectPath, fromMs, toMs]); return state; From b6a0fff38b00903e97bfb8ae93bc16f8ae7fa878 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:14:05 +0000 Subject: [PATCH 5/7] refactor: use CARD_CLASS constant for project cards in LandingPage The ProjectsSection component had a hardcoded className string identical to the CARD_CLASS constant already defined at the top of the file and used by three other components (SessionStatsRow, SpendGraph loading/main). Replace with the shared constant for consistency. --- src/browser/features/LandingPage/LandingPage.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/browser/features/LandingPage/LandingPage.tsx b/src/browser/features/LandingPage/LandingPage.tsx index d8b0ec2411..85b78b9cac 100644 --- a/src/browser/features/LandingPage/LandingPage.tsx +++ b/src/browser/features/LandingPage/LandingPage.tsx @@ -328,10 +328,7 @@ function ProjectsSection(props: { dateFilters: DateFilters }) {

Projects

{projects.map((project) => ( -
+
{project.projectName} From eac3a073330ad11e406aafa230ee711dce39e8b0 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:14:42 +0000 Subject: [PATCH 6/7] refactor: remove redundant getTokenTotalOrUndefined wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getTokenTotalOrUndefined was a thin wrapper around getTokenTotal (from @/common/types/devtools) that added a null check — but getTokenTotal already handles null/undefined inputs and returns undefined for them. Replace the two call sites with direct getTokenTotal calls and remove the dead function. --- .../RightSidebar/DevToolsTab/DevToolsStepCard.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/browser/features/RightSidebar/DevToolsTab/DevToolsStepCard.tsx b/src/browser/features/RightSidebar/DevToolsTab/DevToolsStepCard.tsx index 7628732d0f..de507c6410 100644 --- a/src/browser/features/RightSidebar/DevToolsTab/DevToolsStepCard.tsx +++ b/src/browser/features/RightSidebar/DevToolsTab/DevToolsStepCard.tsx @@ -273,8 +273,8 @@ function ProviderOptionsSection(props: { providerOptions: unknown }) { } function TokenUsageSection(props: { usage: DevToolsUsage }) { - const inputTotal = getTokenTotalOrUndefined(props.usage.inputTokens); - const outputTotal = getTokenTotalOrUndefined(props.usage.outputTokens); + const inputTotal = getTokenTotal(props.usage.inputTokens); + const outputTotal = getTokenTotal(props.usage.outputTokens); const inputBreakdown = isInputTokenBreakdown(props.usage.inputTokens) ? props.usage.inputTokens : null; @@ -700,16 +700,6 @@ function getCombinedTotal( return (inputTotal ?? 0) + (outputTotal ?? 0); } -function getTokenTotalOrUndefined( - value: number | { total: number } | undefined -): number | undefined { - if (value == null) { - return undefined; - } - - return getTokenTotal(value); -} - function formatTokenCount(value: number | undefined): string { return value == null ? "—" : value.toLocaleString(); } From efccc16aa5d10cebcbacde62b61a5c6e6414a7ea Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 04:36:23 +0000 Subject: [PATCH 7/7] refactor: use createStallWatchdog in onChat subscription The onChat subscription had an inline stall watchdog (manual setInterval + lastChatEventAt tracking + cleanup) that duplicated the createStallWatchdog helper already used by runActivitySubscription and runTerminalActivitySubscription. Replace the inline implementation with the shared helper for consistency and to reduce duplication (-19/+6 lines). --- src/browser/stores/WorkspaceStore.ts | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/browser/stores/WorkspaceStore.ts b/src/browser/stores/WorkspaceStore.ts index 42217b29fd..43b2b0f45e 100644 --- a/src/browser/stores/WorkspaceStore.ts +++ b/src/browser/stores/WorkspaceStore.ts @@ -2982,8 +2982,7 @@ export class WorkspaceStore { const onClientChange = () => attemptController.abort(); clientChangeSignal.addEventListener("abort", onClientChange, { once: true }); - let stallInterval: ReturnType | null = null; - let lastChatEventAt = Date.now(); + const watchdog = this.createStallWatchdog(attemptController, `onChat(${workspaceId})`); try { // Always reset caughtUp at subscription start so historical events are @@ -3049,26 +3048,16 @@ export class WorkspaceStore { this.resetChatStateForReplay(workspaceId); } - // Stall watchdog: server sends heartbeats every 5s, so if we don't receive ANY events - // (including heartbeats) for 10s, the connection is likely dead. - stallInterval = setInterval(() => { - if (attemptController.signal.aborted) return; - - const elapsedMs = Date.now() - lastChatEventAt; - if (elapsedMs < SUBSCRIPTION_STALL_TIMEOUT_MS) return; - - console.warn( - `[WorkspaceStore] onChat appears stalled for ${workspaceId} (no events for ${elapsedMs}ms); retrying...` - ); - attemptController.abort(); - }, SUBSCRIPTION_STALL_CHECK_INTERVAL_MS); + // Start watchdog after subscribe connects so timeout measures + // post-connect silence, not handshake latency. + watchdog.start(); for await (const data of iterator) { if (signal.aborted) { return; } - lastChatEventAt = Date.now(); + watchdog.markEvent(); // Connection is alive again - don't carry old backoff into the next failure. attempt = 0; @@ -3134,9 +3123,7 @@ export class WorkspaceStore { } finally { signal.removeEventListener("abort", onAbort); clientChangeSignal.removeEventListener("abort", onClientChange); - if (stallInterval) { - clearInterval(stallInterval); - } + watchdog.stop(); } if (this.isWorkspaceRegistered(workspaceId)) {