// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0 // Browser-side Workbench journey probe. Prometheus label shaping is enforced again by cloud-api. import type { ChatMessage, TraceEvent } from "@/types"; type WorkbenchEventKind = "workbench_journey" | "workbench_event_phase" | "workbench_backend_event_visible" | "workbench_ui_event"; type WorkbenchOutcome = "ok" | "timeout" | "error" | "dropped" | "stale" | "partial" | "empty" | "network" | "denied" | "unknown"; type WorkbenchLoadingScope = "workbench" | "session_list" | "session_detail" | "api" | "sse" | "page"; type WorkbenchUiState = "enter" | "exit" | "request" | "connect" | "open" | "message" | "close" | "error" | "sample"; type WorkbenchUiReason = "hydrate" | "select_session" | "create_session" | "api_request" | "sse_connect" | "sse_open" | "sse_error" | "sse_close" | "sse_flush" | "pagehide" | "visibility_hidden" | "unknown"; interface WorkbenchPerformanceEvent { kind: WorkbenchEventKind; valueMs: number; route?: string; journey?: string; phase?: string; eventType?: string; backend?: string; transport?: string; entry?: string; source?: string; targetState?: string; cache?: string; authState?: string; visibility?: string; outcome?: WorkbenchOutcome; eventName?: string; uiTraceId?: string; otelTraceId?: string; spanId?: string; parentSpanId?: string; loadingScope?: WorkbenchLoadingScope; state?: WorkbenchUiState; reason?: WorkbenchUiReason; sessionHash?: string; traceHash?: string; method?: string; status?: number; statusClass?: string; startedAtEpochMs?: number; endedAtEpochMs?: number; errorName?: string; timeoutName?: string; activityWaitingFor?: string | null; activityLastEventLabel?: string | null; activityIdleMs?: number; resourceDurationMs?: number; resourceFetchToRequestMs?: number; resourceRequestWaitMs?: number; resourceResponseTransferMs?: number; resourceTransferSize?: number; resourceEncodedBodySize?: number; resourceDecodedBodySize?: number; resourceNextHopProtocol?: string; resourceServerTiming?: string; module?: string; diagnosticCode?: string; rootCause?: string; recoveryAction?: string; contractVersion?: string; realtimeAuthority?: string; sinceOutboxSeq?: number; syncCursorOutboxSeq?: number; entityFamilyCount?: number; entityFamilies?: string; maxEntityVersion?: number; projectionRevision?: string; terminalSeal?: boolean; detailProjection?: boolean; transportState?: string; scopedKey?: string; eventCount?: number; deliveredCount?: number; droppedCount?: number; chunkCount?: number; replacedByKey?: number; flushDurationMs?: number; maxItemsPerChunk?: number; maxChunkMs?: number; } interface TraceEventTimingInput { traceId: string | null | undefined; events: TraceEvent[]; transport: "sse" | "sync_replay" | "detail_history" | "poll"; serverSentAt?: string | null | undefined; eventCreatedAt?: string | null | undefined; traceSeq?: number | string | null | undefined; } interface OpenJourneyState { startAt: number; route: string; cache: string; authState: string; firstVisibleReported: boolean; fullLoadReported: boolean; } interface SessionSwitchState { sessionId: string; startAt: number; route: string; source: string; targetState: string; cache: string; firstVisibleReported: boolean; fullLoadReported: boolean; } interface SubmitJourneyState { traceId: string; sessionId: string; startAt: number; route: string; entry: string; backend: string; transport: string; firstVisibleReported: boolean; apiAcceptedReported: boolean; } interface TraceEventState { traceId: string; receivedAt: number; projectedAt: number | null; eventCreatedAt: number | null; appendedAt: number | null; serverSentAt: number | null; traceSeq: number | null; eventType: string; backend: string; transport: string; visibleOutcome: WorkbenchOutcome; visibleReported: boolean; } interface WorkbenchUiTrace { uiTraceId: string; otelTraceId: string; traceparent: string; startedAtEpochMs: number; valuesPrinted: false; } interface LoadingStateRecord { active: true; startAt: number; startEpochMs: number; reason: WorkbenchUiReason; route: string; sessionHash?: string; traceHash?: string; } const SCHEMA_VERSION = "hwlab-web-performance-v2"; const FLUSH_INTERVAL_MS = 10_000; const PRIORITY_FLUSH_DELAY_MS = 250; const FLUSH_BATCH_THRESHOLD = 120; const MAX_QUEUE = 300; const submitJourneys = new Map(); const sessionSwitches = new Map(); const traceEvents = new Map(); const loadingStates = new Map(); let openJourney: OpenJourneyState | null = null; let queue: WorkbenchPerformanceEvent[] = []; let installed = false; let resourceTimingBufferConfigured = false; let flushTimer: number | null = null; let priorityFlushTimer: number | null = null; let uiTrace: WorkbenchUiTrace | null = null; export function currentWorkbenchUiTrace(): WorkbenchUiTrace | null { if (typeof window === "undefined") return null; if (!uiTrace) { const otelTraceId = randomHex(16); const parentSpanId = randomHex(8); uiTrace = { uiTraceId: `ui_${Date.now().toString(36)}_${randomHex(6)}`, otelTraceId, traceparent: `00-${otelTraceId}-${parentSpanId}-01`, startedAtEpochMs: Date.now(), valuesPrinted: false }; } publishWorkbenchUiTrace(uiTrace); return uiTrace; } export function recordWorkbenchLoadingState(input: { scope: WorkbenchLoadingScope; active: boolean; reason?: WorkbenchUiReason; sessionId?: string | null; traceId?: string | null }): void { ensureInstalled(); const reason = input.reason ?? "unknown"; const now = monotonicNow(); const nowEpoch = wallNow(); const sessionHash = hashIdentifier(input.sessionId, "ses"); const traceHash = hashIdentifier(input.traceId, "trc"); const previous = loadingStates.get(input.scope); if (input.active) { if (previous?.active) return; const state: LoadingStateRecord = { active: true, startAt: now, startEpochMs: nowEpoch, reason, route: pageRoute(), sessionHash, traceHash }; loadingStates.set(input.scope, state); enqueueWorkbenchUiEvent({ eventType: "loading_state", loadingScope: input.scope, state: "enter", reason, route: state.route, valueMs: 0, startedAtEpochMs: nowEpoch, endedAtEpochMs: nowEpoch, sessionHash, traceHash, outcome: "ok" }); return; } if (!previous?.active) return; loadingStates.delete(input.scope); const valueMs = Math.max(0, now - previous.startAt); enqueueWorkbenchUiEvent({ eventType: "loading_state", loadingScope: input.scope, state: "exit", reason: previous.reason, route: previous.route, valueMs, startedAtEpochMs: previous.startEpochMs, endedAtEpochMs: nowEpoch, sessionHash: previous.sessionHash, traceHash: previous.traceHash, outcome: "ok" }); } export function recordWorkbenchApiRequest(input: { route: string; method?: string; status?: number; startedAtEpochMs: number; endedAtEpochMs?: number; outcome?: string | null; timeoutName?: string | null; errorName?: string | null; activityWaitingFor?: string | null; activityLastEventLabel?: string | null; activityIdleMs?: number | null }): void { if (!isWorkbenchPage()) return; const route = safeText(input.route); if (!route || route.startsWith("/v1/web-performance")) return; ensureInstalled(); const endedAtEpochMs = input.endedAtEpochMs ?? wallNow(); const resourceTiming = resourceTimingForApiRequest(route, input.startedAtEpochMs, endedAtEpochMs); const routeLabel = routeTemplate(route); enqueueWorkbenchUiEvent({ eventType: "api_request", loadingScope: "api", state: "request", reason: "api_request", route: routeLabel, method: input.method ?? "GET", status: input.status, statusClass: statusClass(input.status), outcome: normalizeUiOutcome(input.outcome), timeoutName: input.timeoutName ? safeText(input.timeoutName).slice(0, 80) : undefined, errorName: input.errorName ? safeText(input.errorName).slice(0, 80) : undefined, activityWaitingFor: input.activityWaitingFor ? safeText(input.activityWaitingFor).slice(0, 80) : undefined, activityLastEventLabel: input.activityLastEventLabel ? safeText(input.activityLastEventLabel).slice(0, 120) : undefined, activityIdleMs: Number.isFinite(input.activityIdleMs ?? NaN) ? Math.max(0, Math.round(Number(input.activityIdleMs))) : undefined, valueMs: Math.max(0, endedAtEpochMs - input.startedAtEpochMs), startedAtEpochMs: input.startedAtEpochMs, endedAtEpochMs, ...resourceTiming }); } function resourceTimingForApiRequest(route: string, startedAtEpochMs: number, endedAtEpochMs: number): Partial { if (typeof window === "undefined" || typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") return {}; let target: URL; try { target = new URL(route, window.location.href); } catch { return {}; } const timeOrigin = Number.isFinite(performance.timeOrigin) ? performance.timeOrigin : Date.now() - monotonicNow(); const entries = performance.getEntriesByType("resource") as PerformanceResourceTiming[]; const lowerBound = startedAtEpochMs - 2000; const upperBound = endedAtEpochMs + 2000; const entry = bestResourceTimingEntry(entries, target, timeOrigin, lowerBound, upperBound, startedAtEpochMs); clearCollectedResourceTimings(entries.length); if (!entry) return {}; const result: Partial = { resourceDurationMs: roundedNonNegative(entry.duration), resourceFetchToRequestMs: roundedDelta(entry.requestStart, entry.fetchStart), resourceRequestWaitMs: roundedDelta(entry.responseStart, entry.requestStart), resourceResponseTransferMs: roundedDelta(entry.responseEnd, entry.responseStart), resourceTransferSize: roundedNonNegative(entry.transferSize), resourceEncodedBodySize: roundedNonNegative(entry.encodedBodySize), resourceDecodedBodySize: roundedNonNegative(entry.decodedBodySize), resourceNextHopProtocol: safeText(entry.nextHopProtocol).slice(0, 40) || undefined, resourceServerTiming: serverTimingSummary(entry.serverTiming) }; return Object.fromEntries(Object.entries(result).filter(([, value]) => value !== undefined)) as Partial; } interface ResourceTimingCandidate { entry: PerformanceResourceTiming; queryExact: boolean; initiatorRank: number; startDistanceMs: number; } function bestResourceTimingEntry(entries: PerformanceResourceTiming[], target: URL, timeOrigin: number, lowerBound: number, upperBound: number, startedAtEpochMs: number): PerformanceResourceTiming | undefined { let best: ResourceTimingCandidate | null = null; for (const entry of entries) { const candidate = resourceTimingCandidate(entry, target, timeOrigin, lowerBound, upperBound, startedAtEpochMs); if (!candidate) continue; if (!best || resourceTimingCandidateRank(candidate, best) < 0) best = candidate; } return best?.entry; } function resourceTimingCandidateRank(left: ResourceTimingCandidate, right: ResourceTimingCandidate): number { return Number(right.queryExact) - Number(left.queryExact) || left.initiatorRank - right.initiatorRank || left.startDistanceMs - right.startDistanceMs; } function resourceTimingCandidate(entry: PerformanceResourceTiming, target: URL, timeOrigin: number, lowerBound: number, upperBound: number, startedAtEpochMs: number): ResourceTimingCandidate | null { if (!entry) return null; let url: URL; try { url = new URL(entry.name); } catch { return null; } if (url.origin !== target.origin) return null; if (url.pathname !== target.pathname) return null; const started = timeOrigin + entry.startTime; if (started < lowerBound || started > upperBound) return null; return { entry, queryExact: url.search === target.search, initiatorRank: resourceInitiatorRank(entry.initiatorType), startDistanceMs: Math.abs(started - startedAtEpochMs) }; } function clearCollectedResourceTimings(entryCount: number): void { if (entryCount <= 0 || typeof performance === "undefined" || typeof performance.clearResourceTimings !== "function") return; performance.clearResourceTimings(); } function resourceInitiatorRank(value: string): number { const initiator = safeText(value).toLowerCase(); if (initiator === "fetch") return 0; if (initiator === "xmlhttprequest") return 1; if (!initiator) return 2; return 3; } function roundedDelta(end: number, start: number): number | undefined { if (!Number.isFinite(end) || !Number.isFinite(start) || end < start) return undefined; return Math.round(end - start); } function roundedNonNegative(value: number): number | undefined { if (!Number.isFinite(value) || value < 0) return undefined; return Math.round(value); } function serverTimingSummary(entries: readonly PerformanceServerTiming[] | undefined): string | undefined { if (!Array.isArray(entries) || entries.length === 0) return undefined; const summary = entries.slice(0, 8).map((entry) => { const name = safeText(entry.name).replace(/[^A-Za-z0-9_.-]/gu, "_").slice(0, 40) || "metric"; const duration = Number.isFinite(entry.duration) ? Math.round(entry.duration) : null; return duration === null ? name : `${name}:${duration}`; }).join(","); return summary.slice(0, 240) || undefined; } export function recordWorkbenchSseLifecycle(input: { state: "connect" | "open" | "error" | "close"; route: string; sessionId?: string | null; traceId?: string | null; errorName?: string | null; valueMs?: number | null }): void { ensureInstalled(); const state = input.state; const endedAt = wallNow(); const valueMs = roundedNonNegative(Number(input.valueMs ?? 0)) ?? 0; enqueueWorkbenchUiEvent({ eventType: "sse_lifecycle", loadingScope: "sse", state, reason: state === "connect" ? "sse_connect" : state === "open" ? "sse_open" : state === "close" ? "sse_close" : "sse_error", route: input.route, valueMs, startedAtEpochMs: Math.max(0, endedAt - valueMs), endedAtEpochMs: endedAt, sessionHash: hashIdentifier(input.sessionId, "ses"), traceHash: hashIdentifier(input.traceId, "trc"), outcome: state === "error" ? "network" : "ok", errorName: input.errorName ? safeText(input.errorName).slice(0, 80) : undefined }); } export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null; diagnostic?: Record | null; sessionId?: string | null; traceId?: string | null; outcome?: WorkbenchOutcome | null }): void { ensureInstalled(); const diagnostic = recordValue(input.diagnostic); const endedAt = wallNow(); const code = diagnosticText(diagnostic, "code") ?? "runtime_diagnostic"; const flushDurationMs = diagnosticNumber(diagnostic, "flushDurationMs"); const isSseFlush = code === "workbench_sse_flush"; enqueueWorkbenchUiEvent({ eventType: "runtime_diagnostic", loadingScope: "sse", state: isSseFlush ? "sample" : "error", reason: isSseFlush ? "sse_flush" : "sse_error", route: pageRoute(), valueMs: flushDurationMs ?? 0, startedAtEpochMs: Math.max(0, endedAt - (flushDurationMs ?? 0)), endedAtEpochMs: endedAt, sessionHash: hashIdentifier(input.sessionId ?? diagnosticText(diagnostic, "sessionId"), "ses"), traceHash: hashIdentifier(input.traceId ?? diagnosticText(diagnostic, "traceId"), "trc"), outcome: input.outcome ?? normalizeUiOutcome(diagnosticText(diagnostic, "outcome")), errorName: code.slice(0, 80), module: diagnosticLabel(input.module ?? diagnosticText(diagnostic, "module") ?? diagnosticText(diagnostic, "layer")), diagnosticCode: diagnosticLabel(code), rootCause: diagnosticText(diagnostic, "rootCause", "reason")?.slice(0, 160), recoveryAction: diagnosticText(diagnostic, "recoveryAction")?.slice(0, 160), contractVersion: diagnosticLabel(diagnosticText(diagnostic, "contractVersion")), realtimeAuthority: diagnosticLabel(diagnosticText(diagnostic, "realtimeAuthority", "authority")), sinceOutboxSeq: diagnosticNumber(diagnostic, "sinceOutboxSeq"), syncCursorOutboxSeq: diagnosticNumber(diagnostic, "syncCursorOutboxSeq", "cursorOutboxSeq"), entityFamilyCount: diagnosticNumber(diagnostic, "entityFamilyCount"), entityFamilies: diagnosticText(diagnostic, "entityFamilies")?.slice(0, 160), maxEntityVersion: diagnosticNumber(diagnostic, "maxEntityVersion", "entityVersion"), projectionRevision: diagnosticText(diagnostic, "projectionRevision")?.slice(0, 120), terminalSeal: diagnosticBoolean(diagnostic, "terminalSeal"), detailProjection: diagnosticBoolean(diagnostic, "detailProjection"), transportState: diagnosticLabel(diagnosticText(diagnostic, "transportState")), scopedKey: hashIdentifier(diagnosticText(diagnostic, "scopedKey"), "scope"), eventCount: diagnosticNumber(diagnostic, "eventCount"), deliveredCount: diagnosticNumber(diagnostic, "deliveredCount"), droppedCount: diagnosticNumber(diagnostic, "dropped", "droppedCount"), chunkCount: diagnosticNumber(diagnostic, "chunkCount"), replacedByKey: diagnosticNumber(diagnostic, "replacedByKey"), flushDurationMs, maxItemsPerChunk: diagnosticNumber(diagnostic, "maxItemsPerChunk"), maxChunkMs: diagnosticNumber(diagnostic, "maxChunkMs") }); if (isSseFlush) schedulePriorityFlush(); } export function startWorkbenchOpenJourney(input: { route?: string; cache?: "warm" | "cold" | "unknown"; authState?: "warm" | "login_redirect" | "unknown" } = {}): void { ensureInstalled(); openJourney = { startAt: monotonicNow(), route: routeTemplate(input.route ?? pageRoute()), cache: input.cache ?? "unknown", authState: input.authState ?? "unknown", firstVisibleReported: false, fullLoadReported: false }; } export function finishWorkbenchOpenFullLoad(outcome: WorkbenchOutcome = "ok"): void { if (!openJourney || openJourney.fullLoadReported) return; openJourney.fullLoadReported = true; enqueue({ kind: "workbench_journey", journey: "workbench_open_full_load", route: openJourney.route, cache: openJourney.cache, authState: openJourney.authState, visibility: visibilityState(), outcome, valueMs: elapsedSince(openJourney.startAt) }); } export function startWorkbenchSessionSwitch(input: { sessionId: string | null | undefined; source: "rail" | "deeplink" | "history" | "direct"; targetState?: string | null; cache?: "warm" | "cold" | "unknown" }): void { const sessionId = safeText(input.sessionId); if (!sessionId) return; ensureInstalled(); sessionSwitches.set(sessionId, { sessionId, startAt: monotonicNow(), route: pageRoute(), source: input.source, targetState: normalizeTargetState(input.targetState), cache: input.cache ?? "unknown", firstVisibleReported: false, fullLoadReported: false }); } export function finishWorkbenchSessionSwitchFullLoad(sessionId: string | null | undefined, outcome: WorkbenchOutcome = "ok"): void { const state = sessionSwitches.get(safeText(sessionId)); if (!state || state.fullLoadReported) return; state.fullLoadReported = true; enqueue({ kind: "workbench_journey", journey: "session_switch_full_load", route: state.route, source: state.source, targetState: state.targetState, cache: state.cache, visibility: visibilityState(), outcome, valueMs: elapsedSince(state.startAt) }); } export function failWorkbenchSessionSwitch(sessionId: string | null | undefined): void { finishWorkbenchSessionSwitchFullLoad(sessionId, "error"); } export function startWorkbenchSubmitJourney(input: { traceId: string; sessionId: string; entry: "new" | "existing" | "steer" | "retry"; backend?: string | null; transport?: string | null }): void { if (!input.traceId || !input.sessionId) return; ensureInstalled(); submitJourneys.set(input.traceId, { traceId: input.traceId, sessionId: input.sessionId, startAt: monotonicNow(), route: pageRoute(), entry: input.entry, backend: normalizeBackend(input.backend), transport: normalizeTransport(input.transport ?? "sse"), firstVisibleReported: false, apiAcceptedReported: false }); } export function markWorkbenchSubmitApiAccepted(traceId: string | null | undefined): void { const state = submitJourneys.get(safeText(traceId)); if (!state || state.apiAcceptedReported) return; state.apiAcceptedReported = true; enqueue({ kind: "workbench_event_phase", phase: "user_submit_to_api_accepted", eventType: "request", backend: state.backend, transport: state.transport, outcome: "ok", valueMs: elapsedSince(state.startAt) }); } export function failWorkbenchSubmitJourney(traceId: string | null | undefined, outcome: WorkbenchOutcome = "error"): void { const state = submitJourneys.get(safeText(traceId)); if (!state || state.firstVisibleReported) return; state.firstVisibleReported = true; enqueue({ kind: "workbench_journey", journey: "submit_to_failure", route: state.route, entry: state.entry, backend: state.backend, transport: state.transport, visibility: visibilityState(), outcome, valueMs: elapsedSince(state.startAt) }); submitJourneys.delete(state.traceId); } export function markWorkbenchTraceEventsReceived(input: TraceEventTimingInput): void { const traceId = safeText(input.traceId); if (!traceId) return; const event = input.events.find(isUserVisibleTraceEvent) ?? input.events[0]; if (!event) return; const eventType = eventTypeFromTraceEvent(event); const backend = backendFromTraceEvent(event); const eventCreatedAt = traceEventCreatedAt(event, input.eventCreatedAt); const appendedAt = timestampMs(event.appendedAt); const serverSentAt = timestampMs(input.serverSentAt); const receivedAt = monotonicNow(); const receivedWallAt = wallNow(); const traceSeq = Number(input.traceSeq ?? event.seq ?? event.sourceSeq ?? NaN); const visibleOutcome: WorkbenchOutcome = input.transport === "sse" && serverSentAt !== null ? "ok" : "stale"; traceEvents.set(traceId, { traceId, receivedAt, projectedAt: null, eventCreatedAt, appendedAt, serverSentAt, traceSeq: Number.isFinite(traceSeq) ? Math.trunc(traceSeq) : null, eventType, backend, transport: input.transport, visibleOutcome, visibleReported: false }); if (eventCreatedAt !== null && appendedAt !== null) enqueue({ kind: "workbench_event_phase", phase: "created_to_append", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, appendedAt - eventCreatedAt) }); if (appendedAt !== null && serverSentAt !== null) enqueue({ kind: "workbench_event_phase", phase: "append_to_sse", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, serverSentAt - appendedAt) }); if (serverSentAt !== null) enqueue({ kind: "workbench_event_phase", phase: "sse_to_receive", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, receivedWallAt - serverSentAt) }); } export function markWorkbenchTraceProjected(traceId: string | null | undefined): void { const state = traceEvents.get(safeText(traceId)); if (!state || state.projectedAt !== null) return; const projectedAt = monotonicNow(); state.projectedAt = projectedAt; enqueue({ kind: "workbench_event_phase", phase: "receive_to_project", eventType: state.eventType, backend: state.backend, transport: state.transport, outcome: "ok", valueMs: Math.max(0, projectedAt - state.receivedAt) }); } export function acknowledgeWorkbenchVisibleAfterPaint(input: { messages: ChatMessage[]; activeSessionId?: string | null; detailLoading?: boolean }): void { afterNextPaint(() => acknowledgeWorkbenchVisible(input)); } export function acknowledgeWorkbenchVisible(input: { messages: ChatMessage[]; activeSessionId?: string | null; detailLoading?: boolean }): void { const now = monotonicNow(); const route = pageRoute(); if (openJourney && !openJourney.firstVisibleReported) { openJourney.firstVisibleReported = true; enqueue({ kind: "workbench_journey", journey: "workbench_open_first_visible", route: openJourney.route || route, cache: openJourney.cache, authState: openJourney.authState, visibility: visibilityState(), outcome: "ok", valueMs: Math.max(0, now - openJourney.startAt) }); } const activeSessionId = safeText(input.activeSessionId); const session = activeSessionId ? sessionSwitches.get(activeSessionId) : null; if (session && !session.firstVisibleReported && !input.detailLoading) { session.firstVisibleReported = true; enqueue({ kind: "workbench_journey", journey: "session_switch_first_visible", route: session.route, source: session.source, targetState: session.targetState, cache: session.cache, visibility: visibilityState(), outcome: "ok", valueMs: Math.max(0, now - session.startAt) }); } for (const message of input.messages) { const traceId = safeText(message.traceId ?? message.runnerTrace?.traceId); if (!traceId || message.role !== "agent") continue; const submit = submitJourneys.get(traceId); const outputVisible = isVisibleAgentOutputMessage(message); if (submit && !submit.firstVisibleReported && outputVisible) { submit.firstVisibleReported = true; enqueue({ kind: "workbench_journey", journey: "submit_to_first_visible", route: submit.route, entry: submit.entry, backend: submit.backend, transport: submit.transport, visibility: visibilityState(), outcome: "ok", valueMs: Math.max(0, now - submit.startAt) }); submitJourneys.delete(traceId); } const event = traceEvents.get(traceId); if (event && !event.visibleReported && isVisibleTraceEventMessage(message)) { event.visibleReported = true; if (event.projectedAt !== null) enqueue({ kind: "workbench_event_phase", phase: "project_to_paint", eventType: event.eventType, backend: event.backend, transport: event.transport, outcome: "ok", valueMs: Math.max(0, now - event.projectedAt) }); if (event.eventCreatedAt !== null && event.visibleOutcome === "ok") enqueue({ kind: "workbench_backend_event_visible", eventType: event.eventType, backend: event.backend, transport: event.transport, outcome: event.visibleOutcome, valueMs: Math.max(0, wallNow() - event.eventCreatedAt) }); traceEvents.delete(traceId); } } } export function resetWorkbenchPerformanceForTest(): void { submitJourneys.clear(); sessionSwitches.clear(); traceEvents.clear(); loadingStates.clear(); openJourney = null; queue = []; uiTrace = null; if (flushTimer !== null && hasBrowserTimerRuntime()) window.clearTimeout(flushTimer); flushTimer = null; if (priorityFlushTimer !== null && hasBrowserTimerRuntime()) window.clearTimeout(priorityFlushTimer); priorityFlushTimer = null; installed = false; resourceTimingBufferConfigured = false; } export function drainWorkbenchPerformanceEventsForTest(): WorkbenchPerformanceEvent[] { const events = queue; queue = []; return events; } function enqueueWorkbenchUiEvent(event: Omit): void { const trace = currentWorkbenchUiTrace(); if (!trace) return; enqueue({ ...event, kind: "workbench_ui_event", uiTraceId: trace.uiTraceId, otelTraceId: trace.otelTraceId, spanId: randomHex(8), visibility: visibilityState() }); } function enqueue(event: WorkbenchPerformanceEvent): void { if (!Number.isFinite(event.valueMs) || event.valueMs < 0) return; queue.push({ ...event, valueMs: Math.round(event.valueMs * 1000) / 1000 }); if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE); if (queue.length >= FLUSH_BATCH_THRESHOLD) { flushWorkbenchPerformance(false); return; } scheduleFlush(); } function scheduleFlush(): void { if (!hasBrowserTimerRuntime() || flushTimer !== null) return; flushTimer = window.setTimeout(() => flushWorkbenchPerformance(false), FLUSH_INTERVAL_MS); } function schedulePriorityFlush(): void { if (!hasBrowserTimerRuntime() || priorityFlushTimer !== null) return; priorityFlushTimer = window.setTimeout(() => { priorityFlushTimer = null; flushWorkbenchPerformance(false); }, PRIORITY_FLUSH_DELAY_MS); } function flushWorkbenchPerformance(useBeacon: boolean): void { if (flushTimer !== null && hasBrowserTimerRuntime()) { window.clearTimeout(flushTimer); flushTimer = null; } if (priorityFlushTimer !== null && hasBrowserTimerRuntime()) { window.clearTimeout(priorityFlushTimer); priorityFlushTimer = null; } if (!queue.length || typeof window === "undefined") return; const events = queue.splice(0, queue.length).map(enrichWorkbenchEventForFlush); const body = JSON.stringify({ schemaVersion: SCHEMA_VERSION, serviceId: "hwlab-cloud-web", page: pageRoute(), events }); if (useBeacon && navigator.sendBeacon) { navigator.sendBeacon("/v1/web-performance", new Blob([body], { type: "application/json" })); return; } void fetch("/v1/web-performance", { method: "POST", headers: { "content-type": "application/json" }, body, credentials: "same-origin", keepalive: true }).catch(() => undefined); } function enrichWorkbenchEventForFlush(event: WorkbenchPerformanceEvent): WorkbenchPerformanceEvent { if (event.kind !== "workbench_ui_event" || event.eventType !== "api_request") return event; if (event.resourceDurationMs !== undefined) return event; const route = safeText(event.route); if (!route || !Number.isFinite(event.startedAtEpochMs) || !Number.isFinite(event.endedAtEpochMs)) return event; const timing = resourceTimingForApiRequest(route, Number(event.startedAtEpochMs), Number(event.endedAtEpochMs)); return Object.keys(timing).length > 0 ? { ...event, ...timing } : event; } function ensureInstalled(): void { if (installed || typeof document === "undefined") return; installed = true; configureResourceTimingBuffer(); currentWorkbenchUiTrace(); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { enqueueWorkbenchUiEvent({ eventType: "page_lifecycle", loadingScope: "page", state: "close", reason: "visibility_hidden", route: pageRoute(), valueMs: 0, startedAtEpochMs: wallNow(), endedAtEpochMs: wallNow(), outcome: "ok" }); flushWorkbenchPerformance(true); } }); window.addEventListener("pagehide", () => { enqueueWorkbenchUiEvent({ eventType: "page_lifecycle", loadingScope: "page", state: "close", reason: "pagehide", route: pageRoute(), valueMs: 0, startedAtEpochMs: wallNow(), endedAtEpochMs: wallNow(), outcome: "ok" }); flushWorkbenchPerformance(true); }); } function configureResourceTimingBuffer(): void { if (resourceTimingBufferConfigured || typeof performance === "undefined" || typeof performance.setResourceTimingBufferSize !== "function") return; resourceTimingBufferConfigured = true; performance.setResourceTimingBufferSize(2000); } function afterNextPaint(callback: () => void): void { if (typeof requestAnimationFrame !== "function") { callback(); return; } requestAnimationFrame(() => requestAnimationFrame(callback)); } function isVisibleAgentOutputMessage(message: ChatMessage): boolean { if (isTerminalStatusText(message.status) && String(message.text ?? "").trim()) return true; return traceEventsFromMessage(message).some(isSubmitFirstVisibleTraceEvent); } function isVisibleTraceEventMessage(message: ChatMessage): boolean { return traceEventsFromMessage(message).some(isUserVisibleTraceEvent); } function traceEventsFromMessage(message: ChatMessage): TraceEvent[] { return Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : []; } function isUserVisibleTraceEvent(event: TraceEvent): boolean { const type = eventTypeFromTraceEvent(event); return type === "assistant" || type === "tool_call" || type === "backend" || type === "terminal" || type === "error"; } function isSubmitFirstVisibleTraceEvent(event: TraceEvent): boolean { const type = eventTypeFromTraceEvent(event); if (type === "tool_call") return true; return type === "assistant" && isTerminalTraceEvent(event) && traceEventHasDisplayText(event); } function isTerminalTraceEvent(event: TraceEvent): boolean { return event.terminal === true || event.final === true || isTerminalStatusText(event.status); } function isTerminalStatusText(value: unknown): boolean { return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(safeText(value).toLowerCase().replace(/_/gu, "-")); } function traceEventHasDisplayText(event: TraceEvent): boolean { return [event.message, event.text, event.outputSummary, event.stdoutSummary, event.stderrSummary, event.command].some((value) => Boolean(safeText(value))); } function eventTypeFromTraceEvent(event: TraceEvent | null | undefined): string { const raw = safeText(event?.eventType ?? event?.type ?? event?.kind ?? event?.label ?? event?.status).toLowerCase(); if (/tool/u.test(raw)) return "tool_call"; if (/assistant|message|delta|reply/u.test(raw)) return "assistant"; if (/terminal|complete|final/u.test(raw)) return "terminal"; if (/error|fail|timeout/u.test(raw)) return "error"; if (/backend|runner|agentrun/u.test(raw)) return "backend"; if (/request|accepted/u.test(raw)) return "request"; return "status"; } function backendFromTraceEvent(event: TraceEvent | null | undefined): string { const direct = safeText(event?.backend ?? event?.backendProfile ?? event?.provider ?? event?.runnerKind); return normalizeBackend(direct); } function normalizeBackend(value: unknown): string { const text = safeText(value).toLowerCase(); if (!text) return "unknown"; if (text.includes("/")) return text; if (/^(?:codex|deepseek|openai|claude|hyue)/u.test(text)) return `agentrun-v01/${text}`; return text; } function normalizeTransport(value: unknown): string { const text = safeText(value).toLowerCase(); if (text === "rest_gap") return "detail_history"; return text === "detail_history" || text === "sync_replay" || text === "poll" || text === "sse" ? text : "unknown"; } function normalizeTargetState(value: unknown): string { const text = safeText(value).toLowerCase(); if (text === "running" || text === "terminal" || text === "empty") return text; if (["completed", "failed", "timeout", "canceled"].includes(text)) return "terminal"; return "unknown"; } function traceEventCreatedAt(event: TraceEvent, fallback?: unknown): number | null { const raw = safeText(event.createdAt ?? fallback ?? event.ts ?? event.timestamp); const parsed = raw ? Date.parse(raw) : NaN; return Number.isFinite(parsed) ? parsed : null; } function timestampMs(value: unknown): number | null { const raw = safeText(value); const parsed = raw ? Date.parse(raw) : NaN; return Number.isFinite(parsed) ? parsed : null; } function pageRoute(): string { if (typeof window === "undefined" || !window.location) return "/workbench"; return routeTemplate(`${window.location.pathname}${window.location.hash || ""}` || "/workbench"); } function hasBrowserTimerRuntime(): boolean { return typeof window !== "undefined" && typeof window.setTimeout === "function" && typeof window.clearTimeout === "function"; } function routeTemplate(input: string): string { const raw = safeText(input) || "/workbench"; const pathOnly = raw.startsWith("#") ? raw.slice(1) || "/" : raw.split(/[?#]/u, 1)[0] || "/"; const segments = pathOnly.split("/").filter(Boolean).map((part) => routeSegmentTemplate(part)); const path = `/${segments.join("/")}`; return `${path}${routeQueryTemplate(raw)}`; } function routeQueryTemplate(input: string): string { try { const url = new URL(input.startsWith("#") ? input.slice(1) || "/" : input, "https://hwlab.local"); if (!/^\/v1\/workbench\/sessions\/[^/]+$/u.test(url.pathname)) return ""; if (url.searchParams.get("includeMessages") === "false") return "?includeMessages=false"; if (url.searchParams.get("messages") === "0") return "?messages=0"; } catch { // Keep route templates low-cardinality when the URL is not parseable. } return ""; } function routeSegmentTemplate(segment: string): string { const decoded = safeDecode(segment); if (/^(?:trc|trace|thread|run|cmd|job|ses|cnv|conv|gws|box|res|pod|dp|usr)[_-]/iu.test(decoded)) return ":id"; if (/^[0-9a-f]{8,}(?:-[0-9a-f]{4,}){2,}$/iu.test(decoded)) return ":uuid"; if (/^[A-Za-z0-9_-]{20,}$/u.test(decoded)) return ":id"; if (/^\d{5,}$/u.test(decoded)) return ":id"; return decoded.replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 80) || "_"; } function safeDecode(value: string): string { try { return decodeURIComponent(value); } catch { return value; } } function visibilityState(): string { if (typeof document === "undefined") return "unknown"; return document.visibilityState === "hidden" ? "background" : "foreground"; } function publishWorkbenchUiTrace(trace: WorkbenchUiTrace): void { if (typeof window === "undefined") return; window.__HWLAB_WORKBENCH_UI_TRACE__ = { ...trace }; } function randomHex(bytes: number): string { const values = new Uint8Array(Math.max(1, bytes)); const cryptoSource = typeof crypto !== "undefined" ? crypto : typeof window !== "undefined" ? window.crypto : null; if (cryptoSource?.getRandomValues) cryptoSource.getRandomValues(values); else for (let index = 0; index < values.length; index += 1) values[index] = Math.floor(Math.random() * 256); return [...values].map((value) => value.toString(16).padStart(2, "0")).join(""); } function hashIdentifier(value: unknown, prefix: string): string | undefined { const text = safeText(value); if (!text) return undefined; let hash = 0x811c9dc5; for (let index = 0; index < text.length; index += 1) { hash ^= text.charCodeAt(index); hash = Math.imul(hash, 0x01000193) >>> 0; } return `${prefix}_${hash.toString(16).padStart(8, "0")}`; } function recordValue(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function diagnosticText(record: Record | null, ...keys: string[]): string | undefined { if (!record) return undefined; for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) return value.trim(); if (typeof value === "number" && Number.isFinite(value)) return String(value); } return undefined; } function diagnosticNumber(record: Record | null, ...keys: string[]): number | undefined { if (!record) return undefined; for (const key of keys) { const value = record[key]; const number = Number(value); if (Number.isFinite(number) && number >= 0) return Math.round(number); } return undefined; } function diagnosticBoolean(record: Record | null, ...keys: string[]): boolean | undefined { if (!record) return undefined; for (const key of keys) { const value = record[key]; if (typeof value === "boolean") return value; if (typeof value === "string") { const text = value.trim().toLowerCase(); if (text === "true") return true; if (text === "false") return false; } } return undefined; } function diagnosticLabel(value: unknown): string | undefined { const text = safeText(value).replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 80); return text || undefined; } function isWorkbenchPage(): boolean { return pageRoute().startsWith("/workbench"); } function statusClass(status: unknown): string { const code = typeof status === "number" ? status : Number(status); if (Number.isFinite(code) && code >= 100 && code < 600) return `${Math.floor(code / 100)}xx`; if (code === 0) return "network"; return "unknown"; } function normalizeUiOutcome(value: unknown): WorkbenchOutcome { const text = safeText(value).toLowerCase(); if (text === "ok" || text === "timeout" || text === "error" || text === "dropped" || text === "stale" || text === "partial" || text === "empty" || text === "network" || text === "unknown") return text; if (text === "http_error") return "error"; if (text === "network_error") return "network"; return "unknown"; } function monotonicNow(): number { return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); } function wallNow(): number { return Date.now(); } function elapsedSince(startAt: number): number { return Math.max(0, monotonicNow() - startAt); } function safeText(value: unknown): string { return typeof value === "string" ? value.trim() : ""; }