From addbac100943f83b0135be4fbecc856decb38085 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 14:47:06 -0600 Subject: [PATCH] fix: validate mode at stats schema boundaries MuxMetadata.mode is typed as string (for future custom modes), but the stats Zod schemas only accept "plan" | "exec" | undefined. The code was casting mode without validation, which TypeScript allowed but Zod rejected at runtime. This caused ZodError spam on every stream delta event, since each delta triggers getSnapshot() which parses the schema. Fix: validate mode before storing/emitting - treat unknown modes as undefined for stats purposes. --- src/common/types/message.ts | 2 +- src/node/services/sessionTimingService.ts | 6 +++++- src/node/services/streamManager.ts | 9 +++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/common/types/message.ts b/src/common/types/message.ts index 1b2853849b..7d9c1661cb 100644 --- a/src/common/types/message.ts +++ b/src/common/types/message.ts @@ -124,7 +124,7 @@ export interface MuxMetadata { // Readers should use helper: isCompacted = compacted !== undefined && compacted !== false compacted?: "user" | "idle" | boolean; toolPolicy?: ToolPolicy; // Tool policy active when this message was sent (user messages only) - mode?: string; // The mode (plan/exec/etc) active when this message was sent (assistant messages only) + mode?: string; // The mode active when this message was sent (assistant messages only) - plan/exec today, custom modes in future cmuxMetadata?: MuxFrontendMetadata; // Frontend-defined metadata, backend treats as black-box muxMetadata?: MuxFrontendMetadata; // Frontend-defined metadata, backend treats as black-box } diff --git a/src/node/services/sessionTimingService.ts b/src/node/services/sessionTimingService.ts index a456f2e838..6ebcdf510a 100644 --- a/src/node/services/sessionTimingService.ts +++ b/src/node/services/sessionTimingService.ts @@ -482,11 +482,15 @@ export class SessionTimingService { const model = normalizeGatewayModel(data.model); + // Validate mode: stats schema only accepts "plan" | "exec" for now. + // Custom modes will need schema updates when supported. + const mode = data.mode === "plan" || data.mode === "exec" ? data.mode : undefined; + const state: ActiveStreamState = { workspaceId: data.workspaceId, messageId: data.messageId, model, - mode: data.mode, + mode, startTimeMs: data.startTime, firstTokenTimeMs: null, completedToolExecutionMs: 0, diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index fbe1966daa..7e13b07ff7 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -865,7 +865,9 @@ export class StreamManager extends EventEmitter { streamInfo.state = StreamState.STREAMING; // Emit stream start event (include mode from initialMetadata if available) - const streamStartMode = streamInfo.initialMetadata?.mode as "plan" | "exec" | undefined; + // Validate mode - stats schema only accepts "plan" | "exec" for now + const rawMode = streamInfo.initialMetadata?.mode; + const streamStartMode = rawMode === "plan" || rawMode === "exec" ? rawMode : undefined; this.emit("stream-start", { type: "stream-start", workspaceId: workspaceId as string, @@ -1776,7 +1778,10 @@ export class StreamManager extends EventEmitter { await this.tokenTracker.setModel(streamInfo.model); // Emit stream-start event (include mode from initialMetadata if available) - const replayMode = streamInfo.initialMetadata?.mode as "plan" | "exec" | undefined; + // Validate mode - stats schema only accepts "plan" | "exec" for now + const rawReplayMode = streamInfo.initialMetadata?.mode; + const replayMode = + rawReplayMode === "plan" || rawReplayMode === "exec" ? rawReplayMode : undefined; this.emit("stream-start", { type: "stream-start", workspaceId,