// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first; PJ2026-010403 API契约 draft-2026-06-18-r1; PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-18-p0-unique-projection; draft-2026-06-28-p0-d518-session-timeline-consistency; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0; PJ2026-010401080313 Workbench实时权威 draft-2026-07-20-p0-kafka-sse-single-projection.
// Responsibility: Session-first Workbench state orchestration for selection, turn admission, and trace lifecycle rendering.

import { computed, nextTick, ref } from "vue";
import { defineStore } from "pinia";
import { api } from "@/api";
import { workbenchDebugCapabilities, workbenchRealtimeCapabilities } from "@/config/runtime";
import { workbenchRuntimePolicy } from "@/config/workbench-runtime-policy";
import { createKeyedSingleflight } from "@/utils/scheduler/keyed-singleflight";
import { createWorkbenchHealthProbeCache } from "@/utils/workbench-health";
import { agentErrorFromProjection, normalizeApiErrorRecord, normalizeErrorDiagnostic, normalizeProjectionDiagnostic, projectionDiagnosticFromApiFailure, projectionDiagnosticFromFailure } from "@/utils/workbench-error-runtime";
import { readWorkbenchJson, readWorkbenchNumber, readWorkbenchString, removeWorkbenchStorageKey, writeWorkbenchJson, writeWorkbenchString } from "@/utils/workbench-storage-runtime";
import { createWorkbenchStreamTransportRuntime, workbenchRealtimeTraceIdForCapabilities, type WorkbenchRealtimeEvent, type WorkbenchStreamTransportRecovery } from "@/utils/workbench-realtime-runtime";
import { createWorkbenchReplayVisibilityRuntime } from "@/utils/workbench-replay-visibility";
import { mergeRunnerTrace, type TraceSnapshot } from "@/composables/workbench-trace-snapshot";
import type { AgentChatResponse, AgentChatResultResponse, ApiError, ApiResult, ChatMessage, ErrorDiagnostic, LiveSurface, ProjectionBlocker, ProjectionDiagnostic, ProviderProfile, TraceEvent, WorkbenchSessionRecord, WorkbenchTurnTimingProjection } from "@/types";
import { firstNonEmptyString, nextProtocolId, normalizeWorkbenchSessionId, normalizeWorkbenchSessionRouteId } from "@/utils";
import { composeWorkbenchScopedKey, workbenchRealtimeScopeKey } from "@/utils/workbench-key";
import { failWorkbenchSessionSwitch, failWorkbenchSubmitJourney, finishWorkbenchSessionSwitchFullLoad, markWorkbenchSubmitApiAccepted, markWorkbenchTraceEventsReceived, markWorkbenchTraceProjected, recordWorkbenchLoadingState, recordWorkbenchRuntimeDiagnostic, startWorkbenchSessionSwitch, startWorkbenchSubmitJourney } from "@/utils/workbench-performance";
import { RECENT_DRAFTS_STORAGE_KEY, appendSessionPage, defaultProviderProfileOptions, isArchivedSession, mergeSessionIntoList, normalizeChatMessageStatus, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, preferredProviderProfile, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, sessionDetailLoadingProgress as projectSessionDetailLoadingProgress, shouldReadTerminalTraceDetail, shouldShowSessionDetailLoading, shouldShowSessionListLoading, sortSessionTabs, stableSessionList, type DraftEntry, type ProviderProfileOption, type TurnStatusAuthority } from "./workbench-session";
import { initialWorkbenchSessionIdFromLocation } from "./workbench-projection";
import { cleanupWorkbenchServerStateSessions, selectActiveMessages, selectActiveSession, selectSessionList, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority, type WorkbenchServerAction } from "./workbench-server-state";
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "./workbench-session-cache";
import { reduceWorkbenchRealtimeEvent, workbenchRealtimeEventIsBusinessActivity, type WorkbenchRealtimeAction } from "./workbench-event-reducer";
import { projectWorkbenchLiveKafkaMessage, projectWorkbenchLiveKafkaUserMessage, projectWorkbenchLiveKafkaUserSession, workbenchLiveKafkaEnvelope, workbenchLiveKafkaProjectionTarget, workbenchLiveKafkaSessionSummaryChanged } from "./workbench-live-kafka-event";
import { projectRejectedWorkbenchAdmission } from "./workbench-admission-failure";
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed } from "./workbench-terminal-authority";
import {
  agentErrorDisplayText,
  agentErrorFromApiFailure,
  agentRunFromMessage,
  asAgentRun,
  finalResponseText,
  firstFiniteNumber,
  isTerminalMessageStatus,
  isTraceActiveStatus,
  messageHasTerminalResponse,
  messageNeedsTraceDetailRead,
  messageText,
  messageTimingPatch,
  messageTimingPatchFromProjection,
  normalizeAgentError,
  normalizeMessageRunnerTrace,
  normalizedStatusText,
  optionalString,
  positiveDurationBetween,
  projectedAgentMessageText,
  projectionFromMessage,
  projectionFromResult,
  recordValue,
  shouldSuppressTransientWorkbenchReadFailure,
  terminalMessageTimingPatchForNormalize,
  turnResultIsTerminalForMerge,
  turnResultStatusForMerge,
  traceHasEvents,
} from "./workbench-message-projection-runtime";
import { assessWorkbenchRealtimeConnected, planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type WorkbenchRealtimeApplyStep, type WorkbenchRealtimeRecoveryStep } from "./workbench-realtime-plan";
import { useWorkbenchColadaMutations } from "./workbench-colada-mutations";
import { useWorkbenchColadaQueries } from "./workbench-colada-queries";
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
import { traceEventsAutoReadDecision, workbenchTraceEventsReadKey, type TraceEventsReadRangeRecord } from "./workbench-session-messages-read-budget";
import { traceDetailProjectedSeq, traceNextProjectedSeq } from "./workbench-trace-detail";
import { appendRawHwlabIngressFrame, createRawHwlabIngressState } from "./workbench-raw-hwlab-ingress";

const WORKBENCH_SESSION_PROJECTION_SIGNAL_CHANNEL = "hwlab.workbench.sessionProjection.v1";
const WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY = "hwlab.workbench.sessionProjectionSignal.v1";

interface HydrateOptions {
  sessionId?: string | null;
  invalidRouteId?: string | null;
}

type SessionSelectionSource = "route" | "user" | "system";

interface SelectSessionOptions {
  source?: SessionSelectionSource;
}

function workbenchMessageIdForTrace(traceId: string, role: "user" | "agent"): string {
  const suffix = firstNonEmptyString(traceId)
    ?.replace(/^trc_/u, "")
    .replace(/[^A-Za-z0-9_.:-]/gu, "_")
    .slice(0, 48);
  if (!suffix) throw new Error("traceId must produce a stable Workbench message identity");
  return `msg_${suffix}_${role}`;
}

export const useWorkbenchStore = defineStore("workbench", () => {
  const runtimePolicy = workbenchRuntimePolicy();
  const realtimeCapabilities = workbenchRealtimeCapabilities();
  const debugCapabilities = workbenchDebugCapabilities();
  const workbenchColadaReducer = useWorkbenchColadaReducer();
  const workbenchColadaQueries = useWorkbenchColadaQueries();
  const workbenchColadaMutations = useWorkbenchColadaMutations();
  const traceDetailReadSingleflight = createKeyedSingleflight<void>();
  const traceEventsReadRanges = new Map<string, TraceEventsReadRangeRecord>();
  const providerProfile = ref<ProviderProfile>(readString("hwlab.workbench.providerProfile.v1", "codex"));
  const providerOptions = ref<ProviderProfileOption[]>(defaultProviderProfileOptions(providerProfile.value));
  const recentDrafts = ref<DraftEntry[]>(readRecentDrafts());
  const codeAgentTimeoutMs = ref(readNumber("hwlab.workbench.codeAgentTimeoutMs.v1", runtimePolicy.defaultCodeAgentTimeoutMs));
  const gatewayShellTimeoutMs = ref(readNumber("hwlab.workbench.gatewayShellTimeoutMs.v1", runtimePolicy.defaultGatewayTimeoutMs));
  const live = ref<LiveSurface | null>(null);
  const loading = ref(false);
  const sessionsReady = ref(false);
  const switchingSessionId = ref<string | null>(null);
  const sessionDetailLoadingId = ref<string | null>(null);
  const sessionListHasMore = ref(false);
  const sessionListNextCursor = ref<string | null>(null);
  const sessionListLoadingMore = ref(false);
  const sessionListLoadMoreError = ref<string | null>(null);
  const chatPending = ref(false);
  const rawHwlabIngress = ref(createRawHwlabIngressState());
  const error = ref<string | null>(null);
  const activityRef = ref({ lastActivityAt: Date.now(), lastActivityIso: new Date().toISOString(), waitingFor: "idle", lastEventLabel: null as string | null });
  const currentRequest = ref<{ traceId: string; sessionId: string | null; threadId: string | null; status?: string | null } | null>(null);
  const liveRealtimeReadySessionId = ref<string | null>(null);
  const realtimeReplayLoadingSessionId = ref<string | null>(null);
  const realtimeTransportPhase = ref("idle");
  const explicitSessionId = ref<string | null>(initialWorkbenchSessionIdFromLocation());
  const activeSelectionSource = ref<SessionSelectionSource>(explicitSessionId.value ? "route" : "system");
  const selectionEpoch = ref(0);
  const serverState = workbenchColadaReducer.serverState;
  const sessions = computed(() => selectSessionList(serverState.value));
  const sessionStatusAuthority = computed(() => selectSessionStatusAuthority(serverState.value));
  const turnStatusAuthority = computed(() => selectTurnStatusAuthority(serverState.value));
  const traceAuthorityById = computed(() => selectTraceAuthorityById(serverState.value));
  const realtimeTransport = createWorkbenchStreamTransportRuntime();
  const replayVisibility = createWorkbenchReplayVisibilityRuntime();
  const workbenchProjectionSignalSourceId = nextProtocolId("wbtab");
  let workbenchProjectionSignalChannel: BroadcastChannel | null = null;
  const workbenchHealthProbeCache = createWorkbenchHealthProbeCache({ cacheMs: runtimePolicy.workbenchReadFailureCooldownMs });

  const projectedActiveSession = computed(() => selectActiveSession(serverState.value, explicitSessionId.value));
  const routeActiveSession = computed(() => routeSelectedSessionRecord(explicitSessionId.value, sessionDetailLoadingId.value, error.value, projectedActiveSession.value));
  const activeSession = computed(() => projectedActiveSession.value ?? routeActiveSession.value);
  const activeSessionSelectionSource = computed(() => activeSelectionSource.value);
  const selectedSessionId = computed(() => firstNonEmptyString(activeSession.value?.sessionId, explicitSessionId.value));
  const activeSessionId = computed(() => selectedSessionId.value);
  const activeMessages = computed(() => selectActiveMessages(serverState.value, activeSessionId.value));
  const messages = activeMessages;
  const selectedThreadId = computed(() => firstNonEmptyString(activeSession.value?.threadId));
  const sessionTabs = computed(() => sortSessionTabs(routeActiveSession.value ? mergeSessionIntoList(sessions.value, routeActiveSession.value) : sessions.value, activeSessionId.value, sessionStatusAuthority.value));
  const sessionListLoadedCount = computed(() => sessionTabs.value.length);
  const sessionListLoading = computed(() => shouldShowSessionListLoading({ loading: loading.value, sessionsReady: sessionsReady.value }));
  const sessionDetailLoading = computed(() => shouldShowSessionDetailLoading({
    loading: loading.value,
    messageCount: messages.value.length,
    sessionDetailLoadingId: sessionDetailLoadingId.value,
    switchingSessionId: switchingSessionId.value,
    replayLoadingSessionId: realtimeReplayLoadingSessionId.value,
    activeSessionId: activeSessionId.value
  }));
  const sessionDetailLoadingProgress = computed(() => projectSessionDetailLoadingProgress({
    loading: loading.value,
    messageCount: messages.value.length,
    sessionDetailLoadingId: sessionDetailLoadingId.value,
    switchingSessionId: switchingSessionId.value,
    replayLoadingSessionId: realtimeReplayLoadingSessionId.value,
    activeSessionId: activeSessionId.value,
    transportPhase: realtimeTransportPhase.value
  }));
  const composer = computed(() => resolveComposerState({ messages: messages.value, sessions: sessions.value, activeSessionId: activeSessionId.value, chatPending: chatPending.value, realtimeReady: !realtimeCapabilities.liveKafkaSse || liveRealtimeReadySessionId.value === activeSessionId.value, currentRequest: currentRequest.value, turnStatusAuthority: turnStatusAuthority.value }));

  function recordActivity(label = "user-activity"): void {
    const now = Date.now();
    activityRef.value = { lastActivityAt: now, lastActivityIso: new Date(now).toISOString(), waitingFor: "code-agent", lastEventLabel: label };
  }

  async function hydrate(options: HydrateOptions = {}): Promise<void> {
    const routeRequestId = normalizeWorkbenchSessionRouteId(options.sessionId);
    const routeSessionId = normalizeWorkbenchSessionId(options.sessionId);
    const requestEpoch = beginSessionSelection();
    sessionsReady.value = sessions.value.length > 0;
    const includeSessionId = routeSessionId ?? activeSessionId.value;
    loading.value = true;
    recordWorkbenchLoadingState({ scope: "workbench", active: true, reason: "hydrate", sessionId: includeSessionId });
    recordWorkbenchLoadingState({ scope: "session_list", active: true, reason: "hydrate", sessionId: includeSessionId });
    error.value = null;
    void refreshProviderOptions();
    if (routeSessionId) {
      sessionDetailLoadingId.value = routeSessionId;
      recordWorkbenchLoadingState({ scope: "session_detail", active: true, reason: "hydrate", sessionId: routeSessionId });
      setActiveSessionSelection(routeSessionId, "route");
      rememberSessionDetail(workbenchSessionNavigationSeed(routeSessionId));
      restartRealtime("hydrate-route");
    }
    const sessionsResult = await workbenchColadaQueries.fetchSessions({ includeSessionId, limit: runtimePolicy.sessionListPageLimit, minIntervalMs: runtimePolicy.sessionListMinRefreshIntervalMs });
    const listedSessions = sessionsResult.ok ? workbenchSessionsFromPayload(sessionsResult.data) : [];
    if (sessionsResult.ok) {
      applySessionPagination(sessionsResult.data);
    } else {
      error.value = sessionsResult.error ?? "session list unavailable";
    }
    const targetSessionId = routeRequestId ?? includeSessionId ?? activeSessionId.value ?? listedSessions[0]?.sessionId ?? null;
    if (!sessionsResult.ok && !targetSessionId) {
      clearSessionDetailLoading(includeSessionId);
      loading.value = false;
      recordWorkbenchLoadingState({ scope: "session_detail", active: false, reason: "hydrate", sessionId: includeSessionId });
      recordWorkbenchLoadingState({ scope: "session_list", active: false, reason: "hydrate", sessionId: includeSessionId });
      recordWorkbenchLoadingState({ scope: "workbench", active: false, reason: "hydrate", sessionId: includeSessionId });
      return;
    }
    if (targetSessionId && (routeSessionId || !routeRequestId)) {
      setActiveSessionSelection(targetSessionId, routeSessionId ? "route" : "system");
    }
    if (targetSessionId) {
      sessionDetailLoadingId.value = targetSessionId;
      recordWorkbenchLoadingState({ scope: "session_detail", active: true, reason: "hydrate", sessionId: targetSessionId });
    }
    const selected = targetSessionId ? loadWorkbenchSession(targetSessionId, listedSessions.find((item) => item.sessionId === targetSessionId) ?? null) : null;
    if (selected && !isArchivedSession(selected) && routeRequestId && !routeSessionId && requestEpoch === selectionEpoch.value) setActiveSessionSelection(selected.sessionId, "route");
    const selectedIsCurrent = Boolean(selected && !isArchivedSession(selected) && isCurrentSessionSelection(requestEpoch, selected.sessionId));
    if (sessionsResult.ok || (selectedIsCurrent && selected)) error.value = null;
    if (selectedIsCurrent && selected) applySelectedSessionDetail(selected, "route");
    if (selected && !isArchivedSession(selected) && routeRequestId && !isCurrentSessionSelection(requestEpoch, selected.sessionId) && selected.sessionId !== activeSessionId.value) rememberSessionDetail(selected);
    if (selected && isArchivedSession(selected)) applyCanonicalSessionArchive(selected.sessionId, "session archived");
    if (options.invalidRouteId) markSessionReadUnavailable(options.invalidRouteId, "invalid session URL");
    if (targetSessionId && !selected) markSessionReadUnavailable(targetSessionId, "session detail unavailable");
    const stableSelected = selectedIsCurrent && selected ? selected : activeSessionListSeed();
    const nextSessions = stableSessionList(sessions.value, listedSessions, stableSelected?.sessionId ?? selected?.sessionId ?? routeSessionId ?? includeSessionId, stableSelected);
    rememberSessionList(nextSessions);
    sessionsReady.value = sessionsResult.ok || nextSessions.length > 0;
    clearSessionDetailLoading(targetSessionId);
    loading.value = false;
    recordWorkbenchLoadingState({ scope: "session_detail", active: false, reason: "hydrate", sessionId: targetSessionId });
    recordWorkbenchLoadingState({ scope: "session_list", active: false, reason: "hydrate", sessionId: includeSessionId });
    recordWorkbenchLoadingState({ scope: "workbench", active: false, reason: "hydrate", sessionId: targetSessionId ?? includeSessionId });
    await refreshProviderOptions();
    restartRealtime("hydrate");
  }

  async function refreshLive(): Promise<void> {
    const [healthLive, health, restIndex, hwpodSpecs, hwpodNodeOps, liveBuilds] = await Promise.all([
      liveHealthCall("health/live", api.healthLive),
      liveHealthCall("health", api.health),
      liveHealthCall("REST index", api.restIndex),
      liveHealthCall("hwpod specs", api.hwpod.specs),
      liveHealthCall("hwpod node ops", api.hwpod.nodeOpsHealth),
      liveHealthCall("live builds", api.liveBuilds)
    ]);
    live.value = { healthLive, health, restIndex, hwpodSpecs, hwpodNodeOps, liveBuilds, loadedAt: new Date().toISOString() };
  }

  async function liveHealthCall<T>(label: string, call: () => Promise<ApiResult<T>>): Promise<ApiResult<T>> {
    const snapshot = await workbenchHealthProbeCache.probe<ApiResult<T>>({
      key: label,
      fetcher: () => liveCall(label, call),
      classify: (value) => value.ok ? "ok" : "degraded"
    });
    if (snapshot.value) return snapshot.value;
    const diagnostic: ErrorDiagnostic = { contractVersion: "hwlab-error-diagnostic-v1", code: "workbench_health_probe_unavailable", category: "health", layer: "workbench-health-runtime", source: "workbench-web", retryable: true, valuesPrinted: false };
    return { ok: false, status: 0, data: null, error: `${label}: health probe unavailable`, diagnostic } as ApiResult<T>;
  }

  async function createSession(): Promise<void> {
    beginSessionSelection();
    loading.value = true;
    recordWorkbenchLoadingState({ scope: "workbench", active: true, reason: "create_session", sessionId: activeSessionId.value });
    error.value = null;
    void refreshProviderOptions();
    const response = await workbenchColadaMutations.createAgentSession({ providerProfile: providerProfile.value });
    loading.value = false;
    recordWorkbenchLoadingState({ scope: "workbench", active: false, reason: "create_session", sessionId: activeSessionId.value });
    const createdResponse = response.ok ? sessionFromWorkbenchSession(response.data?.session, { includeMessages: false }) : null;
    const created = createdResponse ? workbenchSessionNavigationSeed(createdResponse.sessionId, createdResponse) : null;
    if (!response.ok || !created) {
      error.value = response.error ?? "session create failed";
      return;
    }
    rememberSessionList(mergeSessionIntoList(sessions.value, created));
    sessionsReady.value = true;
    setActiveSessionSelection(created.sessionId, "user");
    await selectSession(created);
  }

  async function selectSession(session: WorkbenchSessionRecord, options: SelectSessionOptions = {}): Promise<void> {
    await selectSessionById(session.sessionId, session, options);
  }

  async function selectSessionById(sessionId: string, seed: WorkbenchSessionRecord | null = null, options: SelectSessionOptions = {}): Promise<boolean> {
    const source = options.source ?? "user";
    const requestId = normalizeWorkbenchSessionRouteId(sessionId);
    const normalized = normalizeWorkbenchSessionId(sessionId);
    if (!requestId) {
      error.value = "invalid session URL";
      return false;
    }
    const existing = seed ?? sessions.value.find((session) => session.sessionId === normalized) ?? null;
    const requestEpoch = beginSessionSelection();
    startWorkbenchSessionSwitch({ sessionId: normalized ?? requestId, source: existing ? "rail" : "deeplink", targetState: existing?.status ?? "unknown", cache: (existing?.messages?.length ?? 0) > 0 ? "warm" : "cold" });
    switchingSessionId.value = requestId;
    sessionDetailLoadingId.value = requestId;
    loading.value = true;
    recordWorkbenchLoadingState({ scope: "workbench", active: true, reason: "select_session", sessionId: normalized ?? requestId });
    recordWorkbenchLoadingState({ scope: "session_detail", active: true, reason: "select_session", sessionId: normalized ?? requestId });
    if (normalized) setActiveSessionSelection(normalized, source);
    if (existing) {
      rememberSessionList(mergeSessionIntoList(sessions.value, existing));
      sessionsReady.value = true;
    }
    const selected = loadWorkbenchSession(requestId, existing);
    if (selected && !isArchivedSession(selected) && !normalized && requestEpoch === selectionEpoch.value) setActiveSessionSelection(selected.sessionId, source);
    if (!isCurrentSessionRequest(requestEpoch, requestId, selected?.sessionId ?? normalized)) {
      clearSessionDetailLoading(requestId);
      clearSwitchingSession(requestId);
      recordWorkbenchLoadingState({ scope: "session_detail", active: false, reason: "select_session", sessionId: normalized ?? requestId });
      recordWorkbenchLoadingState({ scope: "workbench", active: false, reason: "select_session", sessionId: normalized ?? requestId });
      return true;
    }
    if (selected && !isArchivedSession(selected)) {
      applySelectedSessionDetail(selected, source);
      await nextTick();
      loading.value = false;
      clearSessionDetailLoading(requestId);
      clearSwitchingSession(requestId);
      recordWorkbenchLoadingState({ scope: "session_detail", active: false, reason: "select_session", sessionId: selected.sessionId });
      recordWorkbenchLoadingState({ scope: "workbench", active: false, reason: "select_session", sessionId: selected.sessionId });
      error.value = null;
      await refreshSessions(selected.sessionId, { force: true });
      finishWorkbenchSessionSwitchFullLoad(selected.sessionId, "ok");
      return activeSessionId.value === selected.sessionId;
    }
    loading.value = false;
    clearSessionDetailLoading(requestId);
    clearSwitchingSession(requestId);
    recordWorkbenchLoadingState({ scope: "session_detail", active: false, reason: "select_session", sessionId: normalized ?? requestId });
    recordWorkbenchLoadingState({ scope: "workbench", active: false, reason: "select_session", sessionId: normalized ?? requestId });
    if (selected && isArchivedSession(selected)) applyCanonicalSessionArchive(selected.sessionId, "session archived");
    else markSessionReadUnavailable(requestId, "session detail unavailable");
    failWorkbenchSessionSwitch(normalized ?? requestId);
    return false;
  }

  async function deleteCurrentSession(): Promise<void> {
    const sessionId = activeSessionId.value;
    if (!sessionId) return;
    error.value = null;
    const response = await workbenchColadaMutations.deleteAgentSession({ sessionId });
    if (!response.ok) {
      error.value = response.error ?? "session delete failed";
      return;
    }
    forgetSession(sessionId);
    await refreshSessions(null, { force: true });
    const next = sessions.value.find((session) => !isArchivedSession(session)) ?? null;
    replaceActiveSessionSelection(next?.sessionId ?? null);
    currentRequest.value = null;
    if (next) {
      await selectSession(next, { source: "system" });
      return;
    }
    restartRealtime("delete-current-session");
  }

  async function refreshSessions(includeSessionId: string | null = activeSessionId.value, options: { force?: boolean } = {}): Promise<void> {
    const requestIncludeSessionId = firstNonEmptyString(includeSessionId);
    const requestLimit = currentSessionListLimit();
    await refreshSessionsNow(requestIncludeSessionId, requestLimit, { force: options.force });
  }

  async function refreshSessionsNow(includeSessionId: string | null, limit: number, options: { force?: boolean } = {}): Promise<void> {
    const response = await workbenchColadaQueries.fetchSessions({ includeSessionId, limit, force: options.force, minIntervalMs: runtimePolicy.sessionListMinRefreshIntervalMs });
    if (response.ok) {
      const listed = workbenchSessionsFromPayload(response.data);
      const activeSeed = activeSessionListSeed();
      const refreshed = stableSessionList(sessions.value, listed, activeSeed?.sessionId ?? includeSessionId, activeSeed);
      const next = sessions.value.length > refreshed.length ? appendSessionPage(sessions.value, refreshed) : refreshed;
      rememberSessionList(next);
      applySessionPagination(response.data);
      sessionsReady.value = true;
      return;
    }
    sessionsReady.value = sessions.value.length > 0;
    if (sessions.value.length === 0) error.value = response.error ?? "session list unavailable";
  }

  async function loadMoreSessions(): Promise<void> {
    if (sessionListLoadingMore.value || !sessionListHasMore.value) return;
    const cursor = sessionListNextCursor.value;
    if (!cursor) {
      sessionListHasMore.value = false;
      return;
    }
    sessionListLoadingMore.value = true;
    sessionListLoadMoreError.value = null;
    const response = await workbenchColadaQueries.fetchSessions({ includeSessionId: activeSessionId.value, limit: runtimePolicy.sessionListPageLimit, cursor, force: true });
    sessionListLoadingMore.value = false;
    if (!response.ok) {
      sessionListLoadMoreError.value = response.error ?? "session list load more failed";
      return;
    }
    const next = appendSessionPage(sessions.value, workbenchSessionsFromPayload(response.data));
    rememberSessionList(next);
    applySessionPagination(response.data);
    sessionsReady.value = true;
  }

  function currentSessionListLimit(): number {
    return Math.max(runtimePolicy.sessionListPageLimit, sessionListLoadedCount.value || 0);
  }

  function applySessionPagination(payload: unknown): void {
    const record = recordValue(payload);
    sessionListHasMore.value = record?.hasMore === true;
    sessionListNextCursor.value = sessionListHasMore.value ? firstNonEmptyString(record?.nextCursor) : null;
    if (!sessionListHasMore.value) sessionListLoadMoreError.value = null;
  }

  function reduceServerState(action: WorkbenchServerAction): void {
    workbenchColadaReducer.reduceServerState(action);
  }

  function rememberSessionDetail(session: WorkbenchSessionRecord | null | undefined): void {
    reduceServerState({ type: "session.detail", session });
  }

  function rememberSessionList(source: WorkbenchSessionRecord[]): void {
    reduceServerState({ type: "session.list", sessions: source });
    const beforeTrim = serverState.value;
    const trimmed = trimWorkbenchSessionCache(beforeTrim, { retainSessionIds: [activeSessionId.value, explicitSessionId.value], maxSessions: currentSessionListLimit() });
    const cleaned = cleanupDroppedWorkbenchSessionCaches(trimmed.state, trimmed.state.sessionOrder);
    const authorityCleaned = cleanupWorkbenchServerStateSessions(beforeTrim, [...trimmed.evictedSessionIds, ...cleaned.droppedSessionIds]);
    workbenchColadaReducer.replaceServerState(() => ({
      ...authorityCleaned,
      sessionOrder: cleaned.state.sessionOrder,
      sessionsById: cleaned.state.sessionsById,
      messagesBySessionId: cleaned.state.messagesBySessionId
    }));
  }

  function forgetSession(sessionId: string): void {
    reduceServerState({ type: "session.forget", sessionId });
  }

  function rememberSessionMessages(sessionId: string | null | undefined, source: ChatMessage[]): void {
    reduceServerState({ type: "session.messages", sessionId, messages: source });
  }

  function replaceActiveMessages(source: ChatMessage[]): void {
    rememberSessionMessages(activeSessionId.value, source);
  }

  function updateActiveMessages(project: (source: ChatMessage[]) => ChatMessage[]): void {
    replaceActiveMessages(project(messages.value));
  }

  function updateSessionMessages(sessionId: string | null | undefined, project: (source: ChatMessage[]) => ChatMessage[]): void {
    const id = normalizeWorkbenchSessionId(sessionId);
    if (!id) return;
    const current = serverState.value.messagesBySessionId[id] ?? serverState.value.sessionsById[id]?.messages ?? [];
    rememberSessionMessages(id, project(current));
  }

  function updateTraceMessages(traceId: string, authoritySessionId: string | null | undefined, project: (message: ChatMessage) => ChatMessage): void {
    const ownerSessionId = traceOwnerSessionId(traceId, authoritySessionId);
    if (!ownerSessionId) return;
    updateSessionMessages(ownerSessionId, (source) => source.map((message) => messageMatchesTraceAuthority(message, traceId, authoritySessionId, ownerSessionId) ? project(message) : message));
  }

  function latestMessageForTrace(traceId: string | null | undefined, source: ChatMessage[] = messages.value): ChatMessage | null {
    const id = firstNonEmptyString(traceId);
    if (!id) return null;
    return [...source].reverse().find((message) => firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) === id) ?? null;
  }

  function traceTerminalBodyIsVisible(traceId: string | null | undefined, sessionId: string | null | undefined = null): boolean {
    const id = firstNonEmptyString(traceId);
    if (!id) return false;
    const ownerSessionId = normalizeWorkbenchSessionId(sessionId) ?? traceOwnerSessionId(id, null);
    const source = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value;
    return !shouldReadTerminalTraceDetail({ traceId: id, messages: source, turnStatusAuthority: turnStatusAuthority.value });
  }

  function appendActiveMessages(...items: ChatMessage[]): void {
    replaceActiveMessages([...messages.value, ...items]);
  }

  function setActiveSessionSelection(sessionId: string | null | undefined, source: SessionSelectionSource = "system"): void {
    const normalized = normalizeWorkbenchSessionId(sessionId);
    if (!normalized) return;
    explicitSessionId.value = normalized;
    activeSelectionSource.value = source;
  }

  function replaceActiveSessionSelection(sessionId: string | null | undefined, source: SessionSelectionSource = "system"): void {
    explicitSessionId.value = normalizeWorkbenchSessionId(sessionId);
    activeSelectionSource.value = source;
  }

  function activeSessionListSeed(): WorkbenchSessionRecord | null {
    return projectedActiveSession.value ?? null;
  }

  function fetchWorkbenchTraceEvents(traceId: string, afterProjectedSeq: number, useActivityTimeout = shouldUseActivityTimeoutForTrace(traceId), options: { force?: boolean } = {}): Promise<ApiResult<AgentChatResultResponse>> {
    const activitySource = useActivityTimeout ? () => activityRef.value : null;
    return workbenchColadaQueries.fetchTraceEvents(traceId, { timeoutMs: runtimePolicy.workbenchTraceEventsTimeoutMs, activityRef: activitySource, afterProjectedSeq, limit: runtimePolicy.traceDetailPageLimit, minIntervalMs: runtimePolicy.workbenchTraceEventsMinRefreshMs, force: options.force });
  }

  function shouldUseActivityTimeoutForTrace(traceId: string | null | undefined): boolean {
    const id = firstNonEmptyString(traceId);
    if (!id) return false;
    if (currentRequest.value?.traceId === id) return true;
    const turn = turnStatusAuthority.value[id];
    const message = [...messages.value].reverse().find((item) => firstNonEmptyString(item.traceId, item.runnerTrace?.traceId) === id) ?? null;
    if (traceAuthorityIsSealed(turn, message)) return false;
    if (messageIsSealedTerminal(message)) return false;
    return isTraceActiveStatus(turn?.status) || isTraceActiveStatus(message?.status);
  }

  function rememberTurnStatus(traceId: string, result: AgentChatResultResponse | TraceSnapshot): void {
    const id = firstNonEmptyString(result.traceId, traceId);
    if (!id) return;
    const status = turnResultStatusForMerge(result as AgentChatResultResponse);
    const running = (result as AgentChatResultResponse).running === true || isTraceActiveStatus(status);
    const terminal = turnResultIsTerminalForMerge(result as AgentChatResultResponse);
    reduceServerState({
      type: "turn.status",
      turn: {
        traceId: id,
        status,
        running,
        terminal,
        sessionId: firstNonEmptyString(result.sessionId) ?? null,
        threadId: firstNonEmptyString(result.threadId) ?? null,
        updatedAt: firstNonEmptyString((result as AgentChatResultResponse).updatedAt) ?? null,
        loadedAt: new Date().toISOString()
      }
    });
  }

  async function submitMessage(text: string): Promise<boolean> {
    const value = text.trim();
    if (!value) return false;
    beginSessionSelection();
    if (!composer.value.sessionId) {
      error.value = "session_required：请先显式新建或选择 Code Agent session。";
      return false;
    }
    const steerMode = composer.value.submitMode === "steer";
    const submitEntry = steerMode ? "steer" : "existing";
    const traceId = steerMode && composer.value.targetTraceId ? composer.value.targetTraceId : nextProtocolId("trc");
    const steerTraceId = steerMode ? nextProtocolId("trc_steer") : null;
    const userMessageId = workbenchMessageIdForTrace(steerTraceId ?? traceId, "user");
    const agentMessageId = workbenchMessageIdForTrace(traceId, "agent");
    const sessionId = composer.value.sessionId;
    const threadId = composer.value.threadId;
    const providerThreadId = providerThreadIdForRequest(threadId);
    const submittedAt = new Date().toISOString();
    const previousChatPending = chatPending.value;
    if (!steerMode) {
      const user = makeMessage("user", value, "sent", { id: userMessageId, messageId: userMessageId, traceId, sessionId, threadId, title: "用户", createdAt: submittedAt, updatedAt: submittedAt });
      const pending = makeMessage("agent", "", "running", { id: agentMessageId, messageId: agentMessageId, traceId, sessionId, threadId, title: "Code Agent", retryInput: value, traceAutoLifecycle: "running", createdAt: submittedAt, updatedAt: submittedAt, ...optimisticRunningTimingPatch(submittedAt) });
      appendActiveMessages(user, pending);
      currentRequest.value = { traceId, sessionId, threadId, status: "running" };
    }
    startWorkbenchSubmitJourney({ traceId, sessionId, entry: submitEntry, backend: providerProfile.value, transport: "sse" });
    chatPending.value = true;
    rememberRecentDraft(value);
    recordActivity("submit");
    const payload = { message: value, prompt: value, submittedAt, sessionId, threadId: providerThreadId, traceId, userMessageId, providerProfile: providerProfile.value, gatewayShellTimeoutMs: gatewayShellTimeoutMs.value, targetTraceId: composer.value.targetTraceId, steerTraceId };
    const response = steerMode
      ? await workbenchColadaMutations.steerAgentMessage({ payload, timeoutMs: codeAgentTimeoutMs.value, activityRef: () => activityRef.value, sessionId, traceId })
      : await workbenchColadaMutations.submitAgentMessage({ payload, timeoutMs: codeAgentTimeoutMs.value, activityRef: () => activityRef.value, sessionId, traceId });
    if (!response.ok || !response.data) {
      const projection = projectionDiagnosticFromApiFailure(response, { code: "code_agent_admission_failed", message: response.error ?? "Code Agent 请求失败", health: response.status === 0 ? "unavailable" : "degraded" });
      const failureError = agentErrorFromApiFailure(response, projection, "Code Agent 请求失败");
      const failureMessage = agentErrorDisplayText(failureError) ?? "Code Agent 请求失败";
      if (steerMode) {
        error.value = failureMessage;
        applyProjectionDiagnostic(traceId, projection);
        failWorkbenchSubmitJourney(traceId, response.status === 0 ? "network" : "error");
        chatPending.value = previousChatPending;
        restartRealtime("steer-not-admitted");
        return false;
      }
      projectRejectedAdmissionFailure({ traceId, message: failureMessage, error: failureError, projection, submittedAt });
      failWorkbenchSubmitJourney(traceId, response.status === 0 ? "network" : "error");
      await clearActiveTrace(traceId, steerMode && response.status === 404 ? "steer-trace-not-found" : "submit-not-admitted");
      chatPending.value = false;
      currentRequest.value = null;
      restartRealtime("submit-not-admitted");
      return false;
    }
    markWorkbenchSubmitApiAccepted(traceId);
    if (steerMode) {
      recordActivity("steer:accepted");
      chatPending.value = previousChatPending;
      currentRequest.value = {
        traceId,
        sessionId,
        threadId,
        status: "running"
      };
      void readTraceEventsForMessages(messages.value);
      restartRealtime("steer");
      return true;
    }
    projectOptimisticRunningTurn({ sessionId, threadId, traceId, userText: value });
    const canonicalTraceId = alignOptimisticTurnMessages(traceId, response.data);
    if (canonicalTraceId !== traceId) {
      reduceServerState({ type: "turn.forget", traceId });
    }
    if (traceTerminalBodyIsVisible(canonicalTraceId, sessionId)) {
      chatPending.value = false;
      currentRequest.value = null;
      return true;
    }
    currentRequest.value = {
      traceId: canonicalTraceId,
      sessionId: firstNonEmptyString(response.data.sessionId, sessionId),
      threadId: firstNonEmptyString(response.data.threadId, threadId),
      status: response.data.status ?? "running"
    };
    publishWorkbenchProjectionSignal(sessionId, canonicalTraceId, "submit-admitted");
    restartRealtime("submit");
    return true;
  }

  async function cancelAgentMessage(message: ChatMessage): Promise<void> {
    const traceId = message.traceId ?? message.runnerTrace?.traceId;
    if (!traceId) return;
    const sessionId = message.sessionId ?? selectedSessionId.value;
    const response = await workbenchColadaMutations.cancelAgentMessage({ traceId, sessionId: message.sessionId ?? selectedSessionId.value, threadId: message.threadId ?? selectedThreadId.value });
    if (!response.ok) {
      applyProjectionDiagnostic(traceId, projectionDiagnosticFromApiFailure(response, { code: "code_agent_cancel_failed", message: response.error ?? "Code Agent 取消请求失败，当前 turn 状态保持 canonical projection。", health: response.status === 0 ? "unavailable" : "degraded" }));
      return;
    }
    recordActivity("cancel:accepted");
    if (message.status === "running") chatPending.value = false;
    currentRequest.value = null;
    void clearActiveTrace(traceId, "cancel-agent-message");
  }

  async function cancelRunningTrace(): Promise<void> {
    const target = composer.value;
    const message = resolveCancelableAgentMessage({
      messages: messages.value,
      targetTraceId: target.targetTraceId,
      targetSessionId: target.sessionId,
      targetThreadId: target.threadId,
      turnStatusAuthority: turnStatusAuthority.value
    });
    if (message) await cancelAgentMessage(message);
  }

  async function retryAgentMessage(message: ChatMessage): Promise<void> {
    const retryInput = firstNonEmptyString(message.retryInput, messages.value.find((item) => item.role === "user" && item.traceId === message.traceId)?.text);
    if (!retryInput) return;
    await submitMessage(retryInput);
  }

  async function readTraceEventsForMessage(message: ChatMessage, options: { force?: boolean } = {}): Promise<void> {
    const traceId = message.traceId ?? message.runnerTrace?.traceId;
    if (!traceId) return;
    if (options.force !== true) {
      recordWorkbenchRuntimeDiagnostic({ module: "workbench-trace-events-read", sessionId: message.sessionId ?? message.runnerTrace?.sessionId ?? null, traceId, outcome: "ok", diagnostic: { code: "trace_events_auto_read_disabled", reason: "projection-sse-authority", source: "trace-detail-read", valuesRedacted: true } });
      return;
    }
    await readTraceEventsForMessageNow(message, options);
  }

  async function readTraceEventsForMessageNow(message: ChatMessage, options: { force?: boolean } = {}): Promise<void> {
    const traceId = message.traceId ?? message.runnerTrace?.traceId;
    if (!traceId) return;
    if (traceTerminalBodyIsVisible(traceId, message.sessionId ?? message.runnerTrace?.sessionId)) {
      recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: message.sessionId ?? null, traceId, outcome: "ok", diagnostic: { code: "terminal_low_priority_trace_skip", source: "trace-detail-read", valuesRedacted: true } });
      return;
    }
    await traceDetailReadSingleflight.run(traceId, async () => {
      if (traceTerminalBodyIsVisible(traceId, message.sessionId ?? message.runnerTrace?.sessionId)) return;
      await readTraceEventsForExplicitDetailPages(message, options);
    }, { reason: options.force ? "force-trace-detail-read" : "trace-detail-read" });
  }

  async function readTraceEventsForExplicitDetailPages(message: ChatMessage, options: { force?: boolean } = {}): Promise<void> {
    const traceId = message.traceId ?? message.runnerTrace?.traceId;
    if (!traceId) return;
    let afterProjectedSeq = traceDetailProjectedSeq(traceAuthorityById.value[traceId] ?? message.runnerTrace);
    for (let page = 0; page < runtimePolicy.traceDetailMaxPages; page += 1) {
      if (traceTerminalBodyIsVisible(traceId, message.sessionId ?? message.runnerTrace?.sessionId)) return;
      const decision = traceEventsDetailReadDecision(traceId, afterProjectedSeq, message, options);
      if (!decision.read) {
        recordTraceEventsDetailReadSkip(traceId, message, decision.reason, afterProjectedSeq);
        return;
      }
      const result = await fetchTraceDetailEventsPage(traceId, afterProjectedSeq, { force: options.force });
      if (!result.ok || !result.data) {
        if (shouldSuppressTransientWorkbenchReadFailure(result)) return;
        if (messageHasTerminalResponse(message)) return;
        applyProjectionDiagnostic(traceId, projectionDiagnosticFromApiFailure(result, { code: "trace_detail_read_failed", message: result.error ?? "Trace 更新超时，运行记录暂不可见。", health: result.status === 0 ? "unavailable" : "degraded" }));
        return;
      }
      applyTraceDetailEventsResult(traceId, result.data);
      const nextProjectedSeq = traceNextProjectedSeq(result.data, afterProjectedSeq);
      rememberTraceEventsDetailRead(traceId, afterProjectedSeq, nextProjectedSeq, result.data);
      if (result.data.hasMore !== true || nextProjectedSeq <= afterProjectedSeq) return;
      afterProjectedSeq = nextProjectedSeq;
    }
  }

  function traceEventsDetailReadDecision(traceId: string, afterProjectedSeq: number, message: ChatMessage, options: { force?: boolean }): { read: boolean; reason: string } {
    const limit = runtimePolicy.traceDetailPageLimit;
    const key = workbenchTraceEventsReadKey({ traceId, afterProjectedSeq, limit });
    return traceEventsAutoReadDecision({
      traceId,
      afterProjectedSeq,
      limit,
      force: options.force,
      terminalBodyVisible: traceTerminalBodyIsVisible(traceId, message.sessionId ?? message.runnerTrace?.sessionId),
      cachedRange: traceEventsReadRanges.get(key) ?? null
    });
  }

  function rememberTraceEventsDetailRead(traceId: string, afterProjectedSeq: number, nextProjectedSeq: number, result: AgentChatResultResponse): void {
    const limit = runtimePolicy.traceDetailPageLimit;
    traceEventsReadRanges.set(workbenchTraceEventsReadKey({ traceId, afterProjectedSeq, limit }), { traceId, afterProjectedSeq, limit, nextProjectedSeq, hasMore: result.hasMore === true });
  }

  function recordTraceEventsDetailReadSkip(traceId: string, message: ChatMessage, reason: string, afterProjectedSeq: number): void {
    recordWorkbenchRuntimeDiagnostic({
      module: "workbench-trace-events-read",
      sessionId: message.sessionId ?? message.runnerTrace?.sessionId ?? null,
      traceId,
      outcome: "ok",
      diagnostic: { code: "trace_events_auto_read_skip", reason, source: "trace-detail-read", afterProjectedSeq, limit: runtimePolicy.traceDetailPageLimit, valuesRedacted: true }
    });
  }

  async function fetchTraceDetailEventsPage(traceId: string, afterProjectedSeq: number, options: { force?: boolean } = {}): Promise<ApiResult<AgentChatResultResponse>> {
    return fetchWorkbenchTraceEvents(traceId, afterProjectedSeq, shouldUseActivityTimeoutForTrace(traceId), { force: options.force });
  }

  async function readTraceEventsForMessages(source: ChatMessage[] = messages.value, options: { force?: boolean } = {}): Promise<void> {
    const candidates = traceDetailReadCandidates(source);
    if (options.force !== true) {
      if (candidates.length > 0) {
        const traceId = firstNonEmptyString(candidates[0]?.traceId, candidates[0]?.runnerTrace?.traceId);
        recordWorkbenchRuntimeDiagnostic({ module: "workbench-trace-events-read", sessionId: candidates[0]?.sessionId ?? candidates[0]?.runnerTrace?.sessionId ?? null, traceId: traceId ?? null, outcome: "ok", diagnostic: { code: "trace_events_auto_read_disabled", reason: "projection-sse-authority", source: "trace-detail-read", candidateCount: candidates.length, valuesRedacted: true } });
      }
      return;
    }
    for (const message of candidates) void readTraceEventsForMessage(message, { force: true });
  }

  function traceDetailReadCandidates(source: ChatMessage[]): ChatMessage[] {
    return source.filter(messageNeedsTraceDetailRead).slice(-runtimePolicy.traceDetailAutoQueueLimit).reverse();
  }

  function rememberTraceAuthority(trace: ChatMessage["runnerTrace"]): void {
    const traceId = firstNonEmptyString(trace?.traceId);
    if (!traceId || !trace) return;
    const existing = traceAuthorityById.value[traceId] ?? null;
    if (trace.eventSource !== "trace-api" && !traceHasEvents(trace) && !existing) return;
    const nextTrace = mergeRunnerTrace(existing, trace as NonNullable<ChatMessage["runnerTrace"]>);
    reduceServerState({ type: "trace.snapshot", traceId, trace: nextTrace });
  }

  function applyTraceDetailEventsResult(traceId: string, result: AgentChatResultResponse): void {
    const authoritySessionId = traceResultSessionId(result);
    const ownerSessionId = traceOwnerSessionId(traceId, authoritySessionId);
    if (!ownerSessionId) return;
    const events = Array.isArray(result.events) ? result.events : Array.isArray(result.traceEvents) ? result.traceEvents : [];
    const activityLabel = firstNonEmptyString(result.lastEventLabel, result.status);
    if (ownerSessionId === activeSessionId.value && events.length > 0) recordActivity(`trace:${activityLabel ?? "detail"}`);
    markWorkbenchTraceEventsReceived({ traceId, events, transport: "detail_history" });
    updateSessionMessages(ownerSessionId, (source) => source.map((message) => {
      if (!messageMatchesTraceAuthority(message, traceId, authoritySessionId, ownerSessionId)) return message;
      const {
        traceId: _resultTraceId,
        sessionId: _resultSessionId,
        threadId: _resultThreadId,
        status: _resultStatus,
        traceStatus: _resultTraceStatus,
        timing: _resultTiming,
        terminalEvidence: _resultTerminalEvidence,
        ...resultTraceRest
      } = result.runnerTrace ?? {};
      const nextTrace = {
        ...resultTraceRest,
        traceId: optionalString(result.traceId, traceId),
        sessionId: optionalString(authoritySessionId, message.runnerTrace?.sessionId),
        threadId: optionalString(result.threadId, message.runnerTrace?.threadId),
        events,
        eventSource: "trace-api",
        eventCount: result.eventCount ?? events.length,
        eventsCompacted: result.runnerTrace?.eventsCompacted ?? false,
        fullTraceLoaded: result.fullTraceLoaded === true || result.hasMore === false,
        hasMore: result.hasMore,
        truncated: result.truncated,
        nextProjectedSeq: typeof result.nextProjectedSeq === "number" ? result.nextProjectedSeq : null,
        range: result.range,
        retention: result.retention,
        traceSummary: result.traceSummary,
        agentRun: result.agentRun,
        projection: projectionFromResult(result),
        projectionStatus: result.projectionStatus ?? result.projection?.projectionStatus,
        projectionHealth: result.projectionHealth ?? result.projection?.projectionHealth,
        staleMs: result.staleMs ?? result.projection?.staleMs,
        blocker: result.blocker ?? result.projection?.blocker,
        lastEventLabel: result.lastEventLabel ?? undefined,
        updatedAt: new Date().toISOString()
      } as NonNullable<ChatMessage["runnerTrace"]>;
      const runnerTrace = mergeRunnerTrace(message.runnerTrace, nextTrace);
      rememberTraceAuthority(runnerTrace);
      return { ...message, runnerTrace };
    }));
    markWorkbenchTraceProjected(traceId);
  }

  function setProviderProfile(value: ProviderProfile): void {
    providerProfile.value = value;
    writeWorkbenchString("hwlab.workbench.providerProfile.v1", value);
  }

  async function refreshProviderOptions(): Promise<boolean> {
    const response = await workbenchColadaQueries.fetchProviderProfiles({ minIntervalMs: runtimePolicy.workbenchReadFailureCooldownMs });
    if (response.ok) {
      providerOptions.value = providerProfileOptionsFromPayload(response.data, providerProfile.value);
      const preferred = preferredProviderProfile(providerOptions.value, providerProfile.value);
      if (preferred !== providerProfile.value) setProviderProfile(preferred);
    } else {
      providerOptions.value = defaultProviderProfileOptions(providerProfile.value);
    }
    return response.ok === true;
  }

  function rememberRecentDraft(text: string): void {
    recentDrafts.value = recordRecentDraft(recentDrafts.value, text);
    writeRecentDrafts(recentDrafts.value);
  }

  function pickDraft(draft: DraftEntry | string): string {
    return typeof draft === "string" ? draft : draft.text;
  }

  function clearRecentDrafts(): void {
    recentDrafts.value = [];
    removeWorkbenchStorageKey(RECENT_DRAFTS_STORAGE_KEY);
  }

  function clearSessionMessages(): void {
    const activeTraceId = activeTraceIdFromMessages(messages.value, turnStatusAuthority.value);
    replaceActiveMessages([]);
    currentRequest.value = null;
    if (activeTraceId) void clearActiveTrace(activeTraceId, "clear-session-messages");
    restartRealtime("clear-session-messages");
  }

  function reattachTrace(traceId: string): void {
    const initial: AgentChatResponse = { status: "running", traceId };
    if (!messages.value.some((message) => message.traceId === traceId)) appendActiveMessages(makeMessage("agent", "", "running", { traceId, sessionId: selectedSessionId.value ?? undefined, threadId: selectedThreadId.value ?? undefined, title: "Code Agent", traceAutoLifecycle: "running" }));
    currentRequest.value = { traceId, sessionId: selectedSessionId.value ?? null, threadId: selectedThreadId.value ?? null, status: initial.status };
    restartRealtime("reattach");
  }

  function restartRealtime(reason: string, forceReconnect = false): void {
    const traceId = realtimeTraceId();
    const sessionId = selectedSessionId.value ?? null;
    void reason;
    const realtimeScopeKey = workbenchRealtimeScopeKey(sessionId, traceId);
    const scopeChanged = realtimeTransport.currentKey() !== realtimeScopeKey;
    if (realtimeCapabilities.liveKafkaSse && scopeChanged) {
      liveRealtimeReadySessionId.value = null;
      realtimeReplayLoadingSessionId.value = realtimeCapabilities.kafkaRefreshReplay ? sessionId : null;
      realtimeTransportPhase.value = "connecting";
    }
    if (realtimeCapabilities.liveKafkaSse && realtimeCapabilities.kafkaRefreshReplay && (scopeChanged || forceReconnect)) {
      replayVisibility.begin(realtimeScopeKey);
    }
    if (debugCapabilities.rawHwlabEventWindow.enabled && scopeChanged) rawHwlabIngress.value = createRawHwlabIngressState(realtimeScopeKey);
    realtimeTransport.restart({
      realtimeCapabilities,
      sessionId,
      traceId,
      forceReconnect,
      errorRecoveryMinMs: runtimePolicy.workbenchRealtimeErrorSyncReplayMinMs,
      flushMaxItemsPerChunk: runtimePolicy.workbenchRealtimeFlushMaxItemsPerChunk,
      flushMaxChunkMs: runtimePolicy.workbenchRealtimeFlushMaxChunkMs,
      flushYieldMs: runtimePolicy.workbenchRealtimeFlushYieldMs,
      onIngress: debugCapabilities.rawHwlabEventWindow.enabled
        ? (frame) => { rawHwlabIngress.value = appendRawHwlabIngressFrame(rawHwlabIngress.value, frame, debugCapabilities.rawHwlabEventWindow); }
        : undefined,
      onError: () => {
        replayVisibility.discard(realtimeScopeKey);
        if (realtimeCapabilities.liveKafkaSse) liveRealtimeReadySessionId.value = null;
      },
      onState: (state) => {
        realtimeTransportPhase.value = state.phase;
        if (realtimeCapabilities.liveKafkaSse && ["connecting", "error", "closed", "blocked"].includes(state.phase)) liveRealtimeReadySessionId.value = null;
        if (["error", "closed", "blocked"].includes(state.phase)) replayVisibility.discard(realtimeScopeKey);
      },
      onRecovery: (recovery) => handleRealtimeRecovery(recovery),
      onEvent: (event, eventName) => {
        if (realtimeCapabilities.liveKafkaSse && eventName === "workbench.connected") {
          const assessment = assessWorkbenchRealtimeConnected(event, sessionId, realtimeCapabilities.kafkaRefreshReplay);
          if (assessment.ready) {
            for (const frame of replayVisibility.commit(realtimeScopeKey)) applyRealtimeEvent(frame.event, frame.eventName);
          } else {
            replayVisibility.discard(realtimeScopeKey);
          }
          liveRealtimeReadySessionId.value = assessment.ready ? assessment.connectedSessionId : null;
          if (assessment.ready && realtimeReplayLoadingSessionId.value === assessment.connectedSessionId) realtimeReplayLoadingSessionId.value = null;
          if (assessment.warning) {
            recordWorkbenchRuntimeDiagnostic({
              module: "workbench-stream-contract",
              sessionId,
              traceId,
              outcome: "partial",
              level: "warning",
              diagnostic: assessment.warning
            });
          }
          if (assessment.blocker) {
            error.value = assessment.blocker.code;
            recordWorkbenchRuntimeDiagnostic({
              module: "workbench-stream-contract",
              sessionId,
              traceId,
              outcome: "error",
              diagnostic: assessment.blocker
            });
          } else if (error.value === "workbench_live_realtime_contract_invalid" || error.value === "workbench_realtime_session_scope_mismatch") {
            error.value = null;
          }
        }
        if (replayVisibility.stage(realtimeScopeKey, { event, eventName })) return;
        applyRealtimeEvent(event, eventName);
      }
    });
  }

  function handleRealtimeRecovery(recovery: WorkbenchStreamTransportRecovery): void {
    const sessionId = normalizeWorkbenchSessionId(recovery.sessionId ?? selectedSessionId.value);
    const traceId = firstNonEmptyString(recovery.traceId, realtimeTraceId());
    const plan = planWorkbenchRealtimeRecovery(recovery, {
      selectedSessionId: selectedSessionId.value,
      activeSessionId: activeSessionId.value,
      fallbackTraceId: traceId,
      activeTraceAuthorized: Boolean(traceId && shouldApplyActiveTraceAuthority(traceId, sessionId)),
      terminalTraceSealed: Boolean(traceId && traceTerminalBodyIsVisible(traceId, sessionId))
    });
    recordWorkbenchRuntimeDiagnostic({ module: "workbench-stream-transport", diagnostic: recovery.diagnostic, sessionId: plan.sessionId, traceId: plan.traceId, outcome: "network" });
    for (const step of plan.steps) executeRealtimeRecoveryStep(step);
  }

  function executeRealtimeRecoveryStep(step: WorkbenchRealtimeRecoveryStep): void {
    switch (step.type) {
      case "events-reconnect":
        restartRealtime(step.reason, true);
        return;
    }
  }

  function stopRealtime(): void {
    liveRealtimeReadySessionId.value = null;
    realtimeTransportPhase.value = "idle";
    realtimeTransport.stop();
  }

  function realtimeTraceId(): string | null {
    return workbenchRealtimeTraceIdForCapabilities(
      realtimeCapabilities,
      currentRequest.value?.traceId,
      activeTraceIdFromMessages(messages.value, turnStatusAuthority.value)
    );
  }

  function normalizeTraceAuthoritySessionId(value: unknown): string | null {
    const sessionId = normalizeWorkbenchSessionId(value);
    const match = sessionId?.match(/^ses_agentrun_([0-9a-f]{8})_([0-9a-f]{4})_([0-9a-f]{4})_([0-9a-f]{4})_([0-9a-f]{12})$/iu);
    if (!match) return sessionId;
    return `ses_${match.slice(1).join("-").toLowerCase()}`;
  }

  function realtimeEventSessionId(event: WorkbenchRealtimeEvent): string | null {
    const turn = recordValue(event.turn);
    const snapshot = recordValue(event.snapshot);
    const traceEvent = recordValue(event.event);
    const sessionId = firstNonEmptyString(event.hwlabSessionId, event.sessionId, turn?.sessionId, snapshot?.sessionId, traceEvent?.sessionId);
    return normalizeTraceAuthoritySessionId(sessionId);
  }

  function traceResultSessionId(result: AgentChatResultResponse | TraceSnapshot | Record<string, unknown> | null | undefined): string | null {
    const value = recordValue(result);
    const runnerTrace = recordValue(value?.runnerTrace);
    const session = recordValue(value?.session);
    return normalizeTraceAuthoritySessionId(firstNonEmptyString(value?.sessionId, runnerTrace?.sessionId, session?.sessionId));
  }

  function messageSessionAuthority(message: ChatMessage | null | undefined): string | null {
    return normalizeTraceAuthoritySessionId(firstNonEmptyString(message?.sessionId, message?.runnerTrace?.sessionId));
  }

  function traceOwnerSessionId(traceId: string | null | undefined, authoritySessionId: string | null | undefined, canonicalSession = false): string | null {
    const sessionId = canonicalSession ? normalizeWorkbenchSessionId(authoritySessionId) : normalizeTraceAuthoritySessionId(authoritySessionId);
    if (sessionId) return sessionId;
    const id = firstNonEmptyString(traceId);
    if (!id) return activeSessionId.value;
    for (const [candidateSessionId, candidateMessages] of Object.entries(serverState.value.messagesBySessionId)) {
      if (candidateMessages.some((message) => firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) === id)) return normalizeTraceAuthoritySessionId(candidateSessionId);
    }
    return activeSessionId.value;
  }

  function messageMatchesTraceAuthority(message: ChatMessage, traceId: string, authoritySessionId: string | null | undefined, ownerSessionId: string | null | undefined, canonicalSession = false): boolean {
    if (message.role !== "agent" || firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) !== traceId) return false;
    const normalizeSession = canonicalSession ? normalizeWorkbenchSessionId : normalizeTraceAuthoritySessionId;
    const sessionId = normalizeSession(authoritySessionId);
    const ownerId = normalizeSession(ownerSessionId);
    const messageSessionId = canonicalSession ? normalizeWorkbenchSessionId(firstNonEmptyString(message.sessionId, message.runnerTrace?.sessionId)) : messageSessionAuthority(message);
    if (sessionId && ownerId && sessionId !== ownerId) return false;
    if (messageSessionId && ownerId && messageSessionId !== ownerId) return false;
    if (sessionId && messageSessionId && sessionId !== messageSessionId) return false;
    return true;
  }

  function shouldApplyActiveTraceAuthority(traceId: string | null | undefined, authoritySessionId: string | null | undefined, canonicalSession = false): boolean {
    const id = firstNonEmptyString(traceId);
    const activeId = activeSessionId.value;
    const sessionId = canonicalSession ? normalizeWorkbenchSessionId(authoritySessionId) : normalizeTraceAuthoritySessionId(authoritySessionId);
    if (!activeId) return false;
    if (sessionId && sessionId !== activeId) return false;
    if (!id) return Boolean(sessionId && sessionId === activeId);
    const message = messages.value.find((item) => firstNonEmptyString(item.traceId, item.runnerTrace?.traceId) === id) ?? null;
    const messageSessionId = messageSessionAuthority(message);
    if (messageSessionId && messageSessionId !== activeId) return false;
    if (sessionId && messageSessionId && sessionId !== messageSessionId) return false;
    const knownSessionId = normalizeTraceAuthoritySessionId(firstNonEmptyString(turnStatusAuthority.value[id]?.sessionId, traceAuthorityById.value[id]?.sessionId));
    if (knownSessionId && knownSessionId !== activeId) return false;
    if (sessionId && knownSessionId && sessionId !== knownSessionId) return false;
    return Boolean(sessionId || messageSessionId || knownSessionId || message);
  }

  function shouldApplyTraceToMessage(message: ChatMessage, traceId: string, authoritySessionId: string | null | undefined): boolean {
    if (message.role !== "agent" || firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) !== traceId) return false;
    const activeId = activeSessionId.value;
    const sessionId = normalizeTraceAuthoritySessionId(authoritySessionId);
    const messageSessionId = messageSessionAuthority(message);
    if (activeId && messageSessionId && messageSessionId !== activeId) return false;
    if (sessionId && activeId && sessionId !== activeId) return false;
    if (sessionId && messageSessionId && sessionId !== messageSessionId) return false;
    return true;
  }

  function applyRealtimeEvent(event: WorkbenchRealtimeEvent, eventName: string): void {
    const reduced = reduceWorkbenchRealtimeEvent(event, eventName);
    if (workbenchRealtimeEventIsBusinessActivity(event, eventName, realtimeCapabilities.liveKafkaSse)) recordActivity(reduced.activityLabel);
    applyWorkbenchRealtimeAction(reduced.action);
  }

  function applyWorkbenchRealtimeAction(action: WorkbenchRealtimeAction): void {
    const plan = planWorkbenchRealtimeApply(action);
    for (const step of plan.steps) applyWorkbenchRealtimePlanStep(step);
  }

  function applyWorkbenchRealtimePlanStep(step: WorkbenchRealtimeApplyStep): void {
    switch (step.type) {
      case "apply-trace-event":
        applyRealtimeTraceEvent(step.traceId, step.event, step.snapshot, step.realtimeEvent);
        return;
      case "apply-projection-error":
        applyRealtimeProjectionError(step.realtimeEvent);
        return;
      case "clear-active-trace":
        void clearActiveTrace(step.traceId, step.reason);
        return;
    }
  }

  function applyRealtimeTraceEvent(traceId: string | null | undefined, event: WorkbenchRealtimeEvent["event"], snapshot: WorkbenchRealtimeEvent["snapshot"], realtimeEvent?: WorkbenchRealtimeEvent | null): void {
    const id = firstNonEmptyString(traceId, event?.traceId, snapshot?.traceId);
    if (!id) return;
    const liveKafkaEnvelope = workbenchLiveKafkaEnvelope(realtimeEvent?.schema);
    const sessionId = realtimeEvent ? realtimeEventSessionId(realtimeEvent) : traceResultSessionId(snapshot ?? event ?? null);
    if (!shouldApplyActiveTraceAuthority(id, sessionId, liveKafkaEnvelope)) return;
    if (traceTerminalBodyIsVisible(id, sessionId) && !liveKafkaEnvelope) {
      recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId, traceId: id, outcome: "ok", diagnostic: { code: "terminal_low_priority_sse_trace_skip", source: "realtime-trace-event", valuesRedacted: true } });
      return;
    }
    const events = event ? [event] : Array.isArray(snapshot?.events) ? snapshot.events : [];
    markWorkbenchTraceEventsReceived({ traceId: id, events, transport: "sse", serverSentAt: realtimeEvent?.serverSentAt, eventCreatedAt: realtimeEvent?.eventCreatedAt, traceSeq: realtimeEvent?.traceSeq ?? realtimeEvent?.cursor?.traceSeq });
    if (liveKafkaEnvelope && event) {
      const context = recordValue(realtimeEvent?.context);
      applyLiveKafkaBusinessEvent(id, sessionId, { ...event, threadId: firstNonEmptyString(event.threadId, context?.threadId) }, new Date().toISOString());
      return;
    }
    recordWorkbenchRuntimeDiagnostic({ module: "workbench-realtime-authority", sessionId, traceId: id, outcome: "warning", diagnostic: { code: "non_kafka_business_projection_ignored", source: "realtime-trace-event", valuesRedacted: true } });
  }

  function applyLiveKafkaBusinessEvent(traceId: string, authoritySessionId: string | null, event: TraceEvent, receivedAt: string): void {
    const eventSessionId = firstNonEmptyString(event.sessionId);
    const ownerSessionId = traceOwnerSessionId(traceId, authoritySessionId ?? eventSessionId, true);
    if (!ownerSessionId) return;
    const threadId = firstNonEmptyString(event.threadId);
    if (workbenchLiveKafkaProjectionTarget(event) === "user") {
      const messageId = firstNonEmptyString(event.userMessageId, event.messageId);
      const previousUserMessage = messageId
        ? (serverState.value.messagesBySessionId[ownerSessionId] ?? []).find((message) => (message.messageId ?? message.id) === messageId) ?? null
        : null;
      const userMessage = projectWorkbenchLiveKafkaUserMessage({ previous: previousUserMessage, traceId, sessionId: ownerSessionId, event, receivedAt, threadId });
      if (userMessage) {
        reduceServerState({ type: "message.upsert", sessionId: ownerSessionId, message: userMessage });
        const existing = sessions.value.find((session) => session.sessionId === ownerSessionId) ?? null;
        const session = projectWorkbenchLiveKafkaUserSession({ previous: existing, message: userMessage, sessionId: ownerSessionId, threadId });
        if (session) rememberSessionList(mergeSessionIntoList(sessions.value, session));
      }
      else error.value = "workbench_live_user_message_invalid";
      return;
    }
    const sessionMessages = serverState.value.messagesBySessionId[ownerSessionId] ?? [];
    const previousMessage = sessionMessages.find((message) => messageMatchesTraceAuthority(message, traceId, authoritySessionId ?? eventSessionId, ownerSessionId, true)) ?? null;
    const turnUserMessage = [...sessionMessages].reverse().find((candidate) => candidate.role === "user" && firstNonEmptyString(candidate.traceId, candidate.targetTraceId) === traceId) ?? null;
    const turnStartedAt = firstNonEmptyString(turnUserMessage?.createdAt, turnUserMessage?.updatedAt);
    const message = projectWorkbenchLiveKafkaMessage({ previous: previousMessage, traceId, sessionId: ownerSessionId, event, receivedAt, turnStartedAt, threadId });
    reduceServerState({ type: "message.upsert", sessionId: ownerSessionId, message });
    if (message.runnerTrace) rememberTraceAuthority(message.runnerTrace);
    markWorkbenchTraceProjected(traceId);
    const terminal = message.traceAutoLifecycle === "terminal";
    const status = firstNonEmptyString(message.status) ?? "running";
    const finalResponse = terminal && message.text ? { text: message.text, status, traceId } : undefined;
    rememberTurnStatus(traceId, {
      traceId,
      sessionId: ownerSessionId,
      status,
      running: !terminal,
      terminal,
      finalResponse,
      timing: message.timing,
      startedAt: message.timing?.startedAt,
      lastEventAt: message.timing?.lastEventAt,
      finishedAt: message.timing?.finishedAt,
      durationMs: message.timing?.durationMs,
      updatedAt: message.updatedAt
    } as AgentChatResultResponse);
    const existing = sessions.value.find((session) => session.sessionId === ownerSessionId) ?? null;
    if (existing) {
      const lastUserMessageAt = firstNonEmptyString(existing.lastUserMessageAt, turnStartedAt);
      const nextSession = {
        ...existing,
        threadId: message.threadId ?? existing.threadId,
        status,
        lastTraceId: traceId,
        lastUserMessageAt,
        updatedAt: lastUserMessageAt ?? existing.updatedAt
      };
      if (workbenchLiveKafkaSessionSummaryChanged(existing, nextSession)) {
        rememberSessionList(mergeSessionIntoList(sessions.value, nextSession));
      }
    }
    if (terminal && currentRequest.value?.traceId === traceId) {
      chatPending.value = false;
      currentRequest.value = null;
    }
  }

  function realtimeEventName(event: WorkbenchRealtimeEvent): string {
    switch (event.type) {
      case "trace.snapshot":
        return "workbench.trace.snapshot";
      case "trace.event":
        return "workbench.trace.event";
      case "message.snapshot":
        return "workbench.message.snapshot";
      case "turn.snapshot":
        return "workbench.turn.snapshot";
      case "trace.unavailable":
        return "workbench.trace.unavailable";
      case "error":
        return "workbench.error";
      default:
        return "message";
    }
  }

  function installRealtimeVisibilityHandler(): void {
    if (typeof document === "undefined") return;
    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState !== "visible") return;
      restartRealtime("visibility");
    });
  }

  function installWorkbenchProjectionSignalHandler(): void {
    if (typeof window === "undefined") return;
    if (typeof BroadcastChannel !== "undefined") {
      workbenchProjectionSignalChannel = new BroadcastChannel(WORKBENCH_SESSION_PROJECTION_SIGNAL_CHANNEL);
      workbenchProjectionSignalChannel.addEventListener("message", (event: MessageEvent<unknown>) => handleWorkbenchProjectionSignal(event.data));
    }
    window.addEventListener("storage", (event: StorageEvent) => {
      if (event.key !== WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY || !event.newValue) return;
      try {
        handleWorkbenchProjectionSignal(JSON.parse(event.newValue));
      } catch {
        // Ignore malformed cross-tab payloads from older pages.
      }
    });
  }

  function publishWorkbenchProjectionSignal(sessionId: string | null | undefined, traceId: string | null | undefined, reason: string): void {
    if (typeof window === "undefined") return;
    const id = normalizeWorkbenchSessionId(sessionId);
    if (!id) return;
    const payload = { type: "session-projection", sourceId: workbenchProjectionSignalSourceId, sessionId: id, traceId: firstNonEmptyString(traceId) ?? null, reason, at: new Date().toISOString(), valuesRedacted: true };
    try { workbenchProjectionSignalChannel?.postMessage(payload); } catch { /* ignore */ }
    writeWorkbenchString(WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY, JSON.stringify(payload));
  }

  function handleWorkbenchProjectionSignal(value: unknown): void {
    const record = recordValue(value);
    if (!record || record.sourceId === workbenchProjectionSignalSourceId) return;
    if (firstNonEmptyString(record.type) !== "session-projection") return;
    const sessionId = normalizeWorkbenchSessionId(record.sessionId);
    if (!sessionId || sessionId !== activeSessionId.value) return;
    const traceId = firstNonEmptyString(record.traceId);
    recordActivity(`cross-tab-session-projection:${firstNonEmptyString(record.reason, traceId, "session") ?? "session"}`);
    restartRealtime("cross-tab-session-projection", true);
  }

  function applyRealtimeProjectionError(event: WorkbenchRealtimeEvent): void {
    const traceId = firstNonEmptyString(event.traceId, realtimeTraceId());
    const errorRecord = recordValue(event.error) ?? event;
    if (!traceId) {
      const code = firstNonEmptyString(errorRecord.code, "workbench_realtime_error") ?? "workbench_realtime_error";
      const phase = firstNonEmptyString(event.phase, errorRecord.phase, "realtime") ?? "realtime";
      const message = firstNonEmptyString(errorRecord.message, errorRecord.summary, "Workbench 实时状态更新异常，最新运行记录暂不可见。") ?? "Workbench 实时状态更新异常，最新运行记录暂不可见。";
      error.value = `${phase} · ${code} · ${message}`;
      return;
    }
    const projection = normalizeProjectionDiagnostic(event) ?? projectionDiagnosticFromFailure({ code: firstNonEmptyString(errorRecord.code, "workbench_realtime_error") ?? "workbench_realtime_error", message: firstNonEmptyString(errorRecord.message, errorRecord.summary, "Workbench 实时状态更新异常，最新运行记录暂不可见。") ?? "Workbench 实时状态更新异常，最新运行记录暂不可见。", health: "degraded", diagnostic: normalizeErrorDiagnostic(errorRecord.diagnostic, recordValue(event.projection)?.diagnostic, recordValue(event)?.diagnostic), apiError: normalizeApiErrorRecord(errorRecord, "Workbench 实时状态更新异常，最新运行记录暂不可见。") });
    applyProjectionDiagnostic(traceId, projection.projectionStatus ? projection : { ...projection, projectionStatus: "blocked", projectionHealth: projection.projectionHealth ?? "degraded" });
  }

  function applyProjectionDiagnostic(traceId: string, projection: ProjectionDiagnostic): void {
    const message = [...messages.value].reverse().find((item) => shouldApplyTraceToMessage(item, traceId, activeSessionId.value)) ?? null;
    const sessionId = normalizeWorkbenchSessionId(firstNonEmptyString(message?.sessionId, message?.runnerTrace?.sessionId, activeSessionId.value));
    const existing = turnStatusAuthority.value[traceId];
    if (existing?.terminal === true || messageHasSealedTerminalResult(message)) return;
    const status = existing?.status ?? normalizedStatusText(message?.status) ?? null;
    const running = existing?.running === true || (!isTerminalMessageStatus(message?.status) && isTraceActiveStatus(message?.status));
    const now = new Date().toISOString();
    reduceServerState({ type: "turn.status", turn: { traceId, status, running, terminal: false, sessionId, threadId: firstNonEmptyString(message?.threadId, message?.runnerTrace?.threadId) ?? existing?.threadId ?? null, updatedAt: now, projection, loadedAt: now } });
    const projectionError = agentErrorFromProjection(projection);
    updateActiveMessages((source) => source.map((item) => {
      if (!shouldApplyTraceToMessage(item, traceId, sessionId)) return item;
      if (messageHasSealedTerminalResult(item)) return item;
      const runnerTrace = mergeRunnerTrace(item.runnerTrace, { ...(item.runnerTrace ?? {}), traceId, sessionId: sessionId ?? item.runnerTrace?.sessionId, projection, projectionStatus: projection.projectionStatus ?? null, projectionHealth: projection.projectionHealth ?? null, staleMs: projection.staleMs ?? null, blocker: projection.blocker ?? null, events: item.runnerTrace?.events ?? [], eventCount: item.runnerTrace?.eventCount ?? item.runnerTrace?.events?.length ?? 0, eventSource: item.runnerTrace?.eventSource ?? "projection-diagnostic", updatedAt: now });
      rememberTraceAuthority(runnerTrace);
      return { ...item, runnerTrace, error: projectionError ? { ...(item.error ?? {}), ...projectionError } : item.error ?? null, projection, projectionStatus: projection.projectionStatus ?? null, projectionHealth: projection.projectionHealth ?? null, blocker: projection.blocker ?? null, updatedAt: now };
    }));
  }

  function failTrace(traceId: string, message: string): void {
    markMessage(traceId, { status: "failed", text: message });
    chatPending.value = false;
    currentRequest.value = null;
    void clearActiveTrace(traceId, "trace-infrastructure-error");
  }

  function projectRejectedAdmissionFailure(input: { traceId: string; message: string; error: ChatMessage["error"]; projection: ProjectionDiagnostic; submittedAt: string }): void {
    const finishedAt = new Date().toISOString();
    updateActiveMessages((source) => source.map((message) => message.traceId === input.traceId && message.role === "agent"
      ? projectRejectedWorkbenchAdmission(message, { ...input, finishedAt })
      : message));
    reduceServerState({ type: "turn.forget", traceId: input.traceId });
  }

  function projectOptimisticRunningTurn(input: { sessionId: string; threadId: string | null; traceId: string; userText: string }): void {
    const now = new Date().toISOString();
    reduceServerState({ type: "turn.status", turn: { traceId: input.traceId, status: "running", running: true, terminal: false, sessionId: input.sessionId, threadId: input.threadId, updatedAt: now, loadedAt: now } });
    const existing = sessions.value.find((session) => session.sessionId === input.sessionId) ?? null;
    rememberSessionList(mergeSessionIntoList(sessions.value, {
      ...(existing ?? {}),
      sessionId: input.sessionId,
      threadId: firstNonEmptyString(input.threadId, existing?.threadId),
      status: "running",
      updatedAt: now,
      lastTraceId: input.traceId,
      firstUserMessagePreview: firstNonEmptyString(existing?.firstUserMessagePreview, input.userText),
      messageCount: Math.max(existing?.messageCount ?? 0, messages.value.filter((message) => firstNonEmptyString(message.sessionId, message.runnerTrace?.sessionId) === input.sessionId).length),
      messages: messages.value.filter((message) => firstNonEmptyString(message.sessionId, message.runnerTrace?.sessionId) === input.sessionId)
    } as WorkbenchSessionRecord));
  }

  function markMessage(traceId: string, patch: Partial<ChatMessage>): void {
    updateActiveMessages((source) => source.map((message) => message.traceId === traceId && message.role === "agent" ? { ...message, ...patch, updatedAt: new Date().toISOString() } : message));
  }

  function alignOptimisticTurnMessages(traceId: string, lifecycle: AgentChatResponse): string {
    const canonicalTraceId = firstNonEmptyString(lifecycle.traceId, traceId) ?? traceId;
    const turnId = firstNonEmptyString(lifecycle.turnId, canonicalTraceId);
    const userMessageId = firstNonEmptyString(lifecycle.userMessageId);
    const assistantMessageId = firstNonEmptyString(lifecycle.assistantMessageId);
    const admissionTiming = optimisticAdmissionTimingPatch(lifecycle);
    const hasAdmissionTiming = Boolean(admissionTiming.timing?.startedAt || admissionTiming.timing?.lastEventAt);
    if (!turnId && !userMessageId && !assistantMessageId && !hasAdmissionTiming && canonicalTraceId === traceId) return canonicalTraceId;
    updateActiveMessages((source) => source.map((message) => {
      if (message.traceId !== traceId) return message;
      const runnerTrace = message.runnerTrace ? { ...message.runnerTrace, traceId: canonicalTraceId } : message.runnerTrace;
      const base = { ...message, traceId: canonicalTraceId, runnerTrace, updatedAt: new Date().toISOString() };
      if (message.role === "user" && userMessageId) return { ...base, id: userMessageId, messageId: userMessageId, turnId };
      if (message.role === "agent") {
        const next = { ...base, ...stableAdmissionTimingPatchForRunningMessage(message, admissionTiming) };
        return {
          ...next,
          ...(assistantMessageId ? { id: assistantMessageId, messageId: assistantMessageId } : {}),
          ...(turnId ? { turnId } : {})
        };
      }
      return turnId ? { ...base, turnId } : base;
    }));
    return canonicalTraceId;
  }

  function stableAdmissionTimingPatchForRunningMessage(message: ChatMessage, patch: Partial<ChatMessage>): Partial<ChatMessage> {
    const incoming = patch.timing;
    const existing = message.timing;
    if (!incoming || !existing?.startedAt || !isRunningAgentMessageForTiming(message)) return patch;
    const timing = {
      ...incoming,
      startedAt: existing.startedAt,
      lastEventAt: existing.lastEventAt ?? incoming.lastEventAt ?? existing.startedAt,
      finishedAt: null,
      durationMs: null,
      valuesRedacted: incoming.valuesRedacted !== false,
    } as WorkbenchTurnTimingProjection;
    return { ...patch, timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: null, durationMs: null };
  }

  function isRunningAgentMessageForTiming(message: ChatMessage): boolean {
    if (message.role !== "agent") return false;
    const status = normalizedStatusText(message.status);
    if (status && isTerminalMessageStatus(status)) return false;
    return status === null || isTraceActiveStatus(status) || message.traceAutoLifecycle === "running";
  }

  function optimisticAdmissionTimingPatch(lifecycle: AgentChatResponse): Partial<ChatMessage> {
    const patch = messageTimingPatch(lifecycle);
    if (patch.timing?.startedAt && patch.timing?.lastEventAt) return patch;
    const record = lifecycle as Record<string, unknown>;
    const admittedAt = firstNonEmptyString(record.startedAt, record.createdAt, record.updatedAt);
    if (!admittedAt) return patch;
    const timing = {
      ...(patch.timing ?? {}),
      startedAt: patch.timing?.startedAt ?? admittedAt,
      lastEventAt: patch.timing?.lastEventAt ?? admittedAt,
      finishedAt: null,
      durationMs: null,
      valuesRedacted: true,
    } as WorkbenchTurnTimingProjection;
    return { ...patch, timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: null, durationMs: null };
  }

  function optimisticRunningTimingPatch(startedAt: string): Partial<ChatMessage> {
    const timing = {
      startedAt,
      lastEventAt: startedAt,
      finishedAt: null,
      durationMs: null,
      valuesRedacted: true,
    } as WorkbenchTurnTimingProjection;
    return { timing, startedAt, lastEventAt: startedAt, finishedAt: null, durationMs: null };
  }

  async function clearActiveTrace(traceId: string, reason: string): Promise<void> {
    if (currentRequest.value?.traceId === traceId) currentRequest.value = null;
  }

  function beginSessionSelection(): number {
    selectionEpoch.value += 1;
    return selectionEpoch.value;
  }

  function isCurrentSessionSelection(epoch: number, sessionId?: string | null): boolean {
    if (epoch !== selectionEpoch.value) return false;
    if (!sessionId) return true;
    return activeSessionId.value === sessionId || switchingSessionId.value === sessionId;
  }

  function isCurrentSessionRequest(epoch: number, requestId: string, selectedSessionId?: string | null): boolean {
    if (epoch !== selectionEpoch.value) return false;
    return activeSessionId.value === requestId || switchingSessionId.value === requestId || Boolean(selectedSessionId && (activeSessionId.value === selectedSessionId || switchingSessionId.value === selectedSessionId));
  }

  function clearSwitchingSession(sessionId: string): void {
    if (switchingSessionId.value === sessionId) switchingSessionId.value = null;
  }

  function clearSessionDetailLoading(sessionId: string | null | undefined): void {
    if (!sessionId || sessionDetailLoadingId.value === sessionId) sessionDetailLoadingId.value = null;
  }

  function applySelectedSessionDetail(session: WorkbenchSessionRecord, source: SessionSelectionSource = "system"): void {
    setActiveSessionSelection(session.sessionId, source);
    const projectedMessages = messagesFromSession(session);
    rememberSessionDetail({ ...session, messages: projectedMessages, messageCount: session.messageCount ?? projectedMessages.length });
    sessionsReady.value = true;
    currentRequest.value = null;
    restartRealtime("apply-selected-session");
  }

  function applyCanonicalSessionArchive(sessionId: string, message: string): void {
    forgetSession(sessionId);
    if (activeSessionId.value === sessionId) replaceActiveSessionSelection(null);
    if (currentRequest.value?.sessionId === sessionId) currentRequest.value = null;
    error.value = message;
    sessionsReady.value = sessions.value.length > 0;
    restartRealtime("session-archived");
  }

  function markSessionReadUnavailable(sessionId: string, message: string): void {
    void sessionId;
    error.value = message;
    sessionsReady.value = sessions.value.length > 0;
    restartRealtime("session-read-unavailable");
  }

  function loadWorkbenchSession(sessionId: string, seed: WorkbenchSessionRecord | null = null): WorkbenchSessionRecord | null {
    const requestId = normalizeWorkbenchSessionRouteId(sessionId);
    if (!requestId) return null;
    const normalizedRequestId = normalizeWorkbenchSessionId(requestId);
    const id = normalizedRequestId ?? seed?.sessionId;
    if (!id) return null;
    const projected = serverState.value.sessionsById[id];
    if (projected) return projected;
    return seed?.sessionId === id ? workbenchSessionNavigationSeed(id, seed) : workbenchSessionNavigationSeed(id);
  }

  function reattachRestoredActiveTrace(): void {
    const traceId = activeTraceIdFromMessages(messages.value, turnStatusAuthority.value);
    if (traceId) reattachTrace(traceId);
  }

  function clearRawHwlabIngress(): void {
    rawHwlabIngress.value = createRawHwlabIngressState(rawHwlabIngress.value.scopeKey);
  }

  installRealtimeVisibilityHandler();
  installWorkbenchProjectionSignalHandler();

  return { sessions, messages, activeMessages, activeSession, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, sessionListLoading, sessionDetailLoading, sessionDetailLoadingProgress, sessionListLoadedCount, sessionListHasMore, sessionListNextCursor, sessionListLoadingMore, sessionListLoadMoreError, chatPending, error, rawHwlabIngress, activeSessionId, activeSessionSelectionSource, selectedSessionId, selectedThreadId, sessionTabs, composer, hydrate, refreshLive, createSession, selectSession, selectSessionById, deleteCurrentSession, refreshSessions, loadMoreSessions, submitMessage, cancelAgentMessage, cancelRunningTrace, retryAgentMessage, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearSessionMessages, clearRawHwlabIngress, recordActivity };
});

function workbenchSessionsFromPayload(payload: unknown): WorkbenchSessionRecord[] {
  const record = recordValue(payload);
  const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
  return sessions
    .map((item) => sessionFromWorkbenchSession(item, { includeMessages: false }))
    .filter((item): item is WorkbenchSessionRecord => Boolean(item))
    .map((session) => workbenchSessionNavigationSeed(session.sessionId, session));
}

function workbenchSessionNavigationSeed(sessionId: string, source: WorkbenchSessionRecord | null = null): WorkbenchSessionRecord {
  return {
    sessionId,
    threadId: firstNonEmptyString(source?.threadId),
    firstUserMessagePreview: firstNonEmptyString(source?.firstUserMessagePreview),
    lastUserMessageAt: firstNonEmptyString(source?.lastUserMessageAt),
    updatedAt: firstNonEmptyString(source?.lastUserMessageAt, source?.updatedAt),
    messages: [],
    messageCount: 0
  };
}

function sessionFromWorkbenchSession(value: unknown, options: { includeMessages?: boolean } = {}): WorkbenchSessionRecord | null {
  const record = recordValue(value);
  const sessionId = normalizeWorkbenchSessionId(record?.sessionId ?? record?.id);
  if (!sessionId) return null;
  return {
    ...record,
    sessionId,
    threadId: firstNonEmptyString(record?.threadId),
    status: firstNonEmptyString(record?.status),
    updatedAt: firstNonEmptyString(record?.updatedAt),
    startedAt: firstNonEmptyString(record?.startedAt),
    lastTraceId: firstNonEmptyString(record?.lastTraceId),
    messageCount: firstFiniteNumber(record?.messageCount) ?? undefined,
    firstUserMessagePreview: firstNonEmptyString(record?.firstUserMessagePreview),
    messages: options.includeMessages === false ? undefined : Array.isArray(record?.messages) ? record.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : undefined
  };
}

function traceIdFromRealtimeRefreshReason(reason: string): string | null {
  const match = /(?:^|[:\s])((?:trc|trace)[_-][A-Za-z0-9_-]+)/u.exec(reason);
  return firstNonEmptyString(match?.[1]);
}

function routeSelectedSessionRecord(sessionId: string | null | undefined, loadingSessionId: string | null | undefined, errorText: string | null | undefined, projected: WorkbenchSessionRecord | null): WorkbenchSessionRecord | null {
  if (projected) return null;
  const id = normalizeWorkbenchSessionId(sessionId);
  if (!id) return null;
  const loadingId = normalizeWorkbenchSessionId(loadingSessionId);
  const loading = loadingId === id;
  const message = loading ? "session 投影加载中" : firstNonEmptyString(errorText, "session 投影暂不可见");
  const projection: ProjectionDiagnostic = {
    projectionStatus: loading ? "pending" : "blocked",
    projectionHealth: loading ? "degraded" : "unavailable",
    blocker: {
      code: loading ? "session_detail_loading" : "session_detail_unavailable",
      message,
      userMessage: message,
      retryable: true
    }
  };
  return {
    sessionId: id,
    status: "unknown",
    messageCount: 0,
    messages: [],
    firstUserMessagePreview: loading ? "正在加载 session 投影" : "session 投影暂不可见",
    projection,
    projectionStatus: projection.projectionStatus ?? undefined,
    projectionHealth: projection.projectionHealth ?? undefined
  } as WorkbenchSessionRecord;
}

function providerThreadIdForRequest(threadId: string | null | undefined): string | null {
  const value = firstNonEmptyString(threadId) ?? null;
  if (!value) return null;
  return /^thr_[A-Za-z0-9]{12,32}$/u.test(value) ? null : value;
}

function uniqueSessionIds(sessions: WorkbenchSessionRecord[]): string[] {
  return [...new Set(sessions.map((session) => firstNonEmptyString(session.sessionId)).filter((sessionId): sessionId is string => Boolean(sessionId)))];
}

function uniqueTraceIds(messages: ChatMessage[]): string[] {
  return [...new Set(messages.map((message) => firstNonEmptyString(message.traceId, message.runnerTrace?.traceId)).filter((traceId): traceId is string => Boolean(traceId)))];
}

function messagesFromSession(session: WorkbenchSessionRecord): ChatMessage[] {
  return (session.messages ?? []).map((message) => normalizeChatMessage(message));
}

function normalizeChatMessage(message: ChatMessage): ChatMessage {
  const role = (message.role as string) === "assistant" ? "agent" : message.role;
  const runnerTrace = normalizeMessageRunnerTrace(message);
  const error = normalizeAgentError(message.error ?? runnerTrace?.error);
  const projection = projectionFromMessage(message, runnerTrace);
  const agentRun = agentRunFromMessage(message) ?? asAgentRun(runnerTrace?.agentRun);
  const rawStatus = normalizeChatMessageStatus(message.status);
  const baseText = firstNonEmptyString(message.text, messageText((message as Record<string, unknown>).content), messageText((message as Record<string, unknown>).message));
  const finalText = finalResponseText((message as Record<string, unknown>).finalResponse);
  const errorText = agentErrorDisplayText(error);
  const status = rawStatus;
  const text = role === "agent"
    ? isTerminalMessageStatus(status)
      ? projectedAgentMessageText({ status, finalText, errorText, baseText })
      : firstNonEmptyString(baseText, finalText) ?? ""
    : firstNonEmptyString(baseText, finalText, errorText) ?? "";
  const messageId = firstNonEmptyString((message as Record<string, unknown>).messageId, message.id) ?? nextProtocolId("msg");
  const timingPatch = isTerminalMessageStatus(status) ? terminalMessageTimingPatchForNormalize(message) : messageTimingPatch(message);
  return { ...message, ...timingPatch, role, text, id: messageId, messageId, title: normalizeWorkbenchMessageTitle(role, message.title), createdAt: message.createdAt ?? new Date().toISOString(), status, runnerTrace, error: error ?? message.error ?? null, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined };
}

function activeTraceIdFromMessages(messages: ChatMessage[], turnStatusAuthority: Record<string, TurnStatusAuthority>): string | null {
  for (const message of [...messages].reverse()) {
    if (message.role !== "agent") continue;
    if (messageHasTerminalResponse(message)) continue;
    const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
    if (!traceId) continue;
    const turn = turnStatusAuthority[traceId];
    if (turn?.running !== true && !isTraceActiveStatus(turn?.status)) continue;
    return traceId;
  }
  return null;
}

export async function liveCall<T>(label: string, call: () => Promise<ApiResult<T>>): Promise<ApiResult<T>> {
  try {
    return await call();
  } catch (error) {
    return { ok: false, status: 0, data: null, error: `${label}: ${error instanceof Error ? error.message : String(error)}` };
  }
}

function makeMessage(role: ChatMessage["role"], text: string, status: ChatMessage["status"], extra: Partial<ChatMessage> = {}): ChatMessage {
  return { id: nextProtocolId("msg"), role, title: extra.title ?? (role === "user" ? "用户" : "Code Agent"), text, status, createdAt: new Date().toISOString(), ...extra };
}

function readString(key: string, fallback: string): string {
  return readWorkbenchString(key, fallback);
}

function readRecentDrafts(): DraftEntry[] {
  return normalizeRecentDrafts(readWorkbenchJson<unknown[]>(RECENT_DRAFTS_STORAGE_KEY, []).value);
}

function writeRecentDrafts(drafts: DraftEntry[]): void {
  writeWorkbenchJson(RECENT_DRAFTS_STORAGE_KEY, drafts);
}

function readNumber(key: string, fallback: number): number {
  return readWorkbenchNumber(key, fallback);
}
