// SPEC: 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.
// Responsibility: Normalized Workbench server-state reducer for session, turn, and trace caches.

import type { ChatMessage, ProjectionDiagnostic, WorkbenchSessionRecord } from "@/types";
import type { SessionStatusAuthority, TurnStatusAuthority } from "./workbench-session";

export interface WorkbenchServerState {
  sessionOrder: string[];
  sessionsById: Record<string, WorkbenchSessionRecord>;
  messagesBySessionId: Record<string, ChatMessage[]>;
  sessionStatusById: Record<string, SessionStatusAuthority>;
  turnStatusByTraceId: Record<string, TurnStatusAuthority>;
  traceById: Record<string, NonNullable<ChatMessage["runnerTrace"]>>;
}

export type WorkbenchServerAction =
  | { type: "session.list"; sessions: WorkbenchSessionRecord[] }
  | { type: "session.detail"; session: WorkbenchSessionRecord | null | undefined }
  | { type: "session.messages"; sessionId: string | null | undefined; messages: ChatMessage[] }
  | { type: "message.snapshot"; sessionId: string | null | undefined; message: ChatMessage | null | undefined }
  | { type: "message.upsert"; sessionId: string | null | undefined; message: ChatMessage | null | undefined }
  | { type: "session.status"; session: SessionStatusAuthority }
  | { type: "session.forget"; sessionId: string }
  | { type: "session.cleanupDropped"; keepSessionIds: string[] }
  | { type: "turn.status"; turn: TurnStatusAuthority }
  | { type: "turn.forget"; traceId: string }
  | { type: "trace.snapshot"; traceId: string; trace: NonNullable<ChatMessage["runnerTrace"]> };

export function createWorkbenchServerState(): WorkbenchServerState {
  return {
    sessionOrder: [],
    sessionsById: {},
    messagesBySessionId: {},
    sessionStatusById: {},
    turnStatusByTraceId: {},
    traceById: {}
  };
}

export function reduceWorkbenchServerState(state: WorkbenchServerState, action: WorkbenchServerAction): WorkbenchServerState {
  switch (action.type) {
    case "session.list":
      return reduceSessionList(state, action.sessions);
    case "session.detail":
      return reduceSessionDetail(state, action.session);
    case "session.messages":
      return reduceSessionMessages(state, action.sessionId, action.messages);
    case "message.snapshot":
      return reduceMessageSnapshot(state, action.sessionId, action.message);
    case "message.upsert":
      return reduceMessageUpsert(state, action.sessionId, action.message);
    case "session.status":
      return { ...state, sessionStatusById: { ...state.sessionStatusById, [action.session.sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[action.session.sessionId], action.session) } };
    case "session.forget":
      return cleanupWorkbenchServerStateSessions(state, [action.sessionId]);
    case "session.cleanupDropped":
      return cleanupWorkbenchServerStateSessions(state, droppedSessionIds(state, new Set(action.keepSessionIds)));
    case "turn.status":
      return reduceTurnStatus(state, action.turn);
    case "turn.forget":
      return { ...state, turnStatusByTraceId: withoutKey(state.turnStatusByTraceId, action.traceId) };
    case "trace.snapshot":
      return { ...state, traceById: { ...state.traceById, [action.traceId]: action.trace } };
  }
}

// Mechanical source: /root/opencode/packages/app/src/context/global-sync/event-reducer.ts:50-91 cleanupDroppedSessionCaches.
export function cleanupWorkbenchServerStateDroppedSessions(state: WorkbenchServerState, keepSessionIds: string[]): WorkbenchServerState {
  return reduceWorkbenchServerState(state, { type: "session.cleanupDropped", keepSessionIds });
}

export function cleanupWorkbenchServerStateSessions(state: WorkbenchServerState, sessionIds: string[]): WorkbenchServerState {
  const ids = unique(sessionIds.filter(Boolean));
  if (ids.length === 0) return state;
  const dropped = new Set(ids);
  const scoped = cleanupSessionScopedAuthority(state, ids);
  return {
    ...scoped,
    sessionOrder: scoped.sessionOrder.filter((sessionId) => !dropped.has(sessionId)),
    sessionsById: omitRecordKeys(scoped.sessionsById, dropped),
    messagesBySessionId: omitRecordKeys(scoped.messagesBySessionId, dropped),
    sessionStatusById: omitRecordKeys(scoped.sessionStatusById, dropped)
  };
}

export function selectSessionStatusAuthority(state: WorkbenchServerState): Readonly<Record<string, SessionStatusAuthority>> {
  return state.sessionStatusById;
}

export function selectTurnStatusAuthority(state: WorkbenchServerState): Readonly<Record<string, TurnStatusAuthority>> {
  return state.turnStatusByTraceId;
}

export function selectTraceAuthorityById(state: WorkbenchServerState): Readonly<Record<string, NonNullable<ChatMessage["runnerTrace"]>>> {
  return state.traceById;
}

export function selectSessionList(state: WorkbenchServerState): WorkbenchSessionRecord[] {
  return state.sessionOrder.map((sessionId) => projectSessionRecord(state.sessionsById[sessionId], state.sessionStatusById[sessionId])).filter((session): session is WorkbenchSessionRecord => Boolean(session));
}

export function selectActiveSession(state: WorkbenchServerState, sessionId: string | null): WorkbenchSessionRecord | null {
  if (!sessionId) return null;
  return projectSessionRecord(state.sessionsById[sessionId], state.sessionStatusById[sessionId]);
}

export function selectActiveMessages(state: WorkbenchServerState, sessionId: string | null): ChatMessage[] {
  return sessionId ? state.messagesBySessionId[sessionId] ?? [] : [];
}

function reduceSessionDetail(state: WorkbenchServerState, session: WorkbenchSessionRecord | null | undefined): WorkbenchServerState {
  const sessionId = session?.sessionId;
  if (!sessionId || !session) return state;
  const existing = state.sessionsById[sessionId];
  const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages ?? [];
  const messages = Array.isArray(session.messages) ? mergeMessageList(existingMessages, session.messages) : existingMessages;
  const merged = mergeSessionRecord(existing, { ...session, messages });
  const sessionStatus = sessionStatusAuthorityFromDetail(session);
  const nextSessionStatus = sessionStatus ? mergeSessionStatusAuthority(state.sessionStatusById[sessionId], sessionStatus) : null;
  if (existing && state.sessionOrder.includes(sessionId) && sessionRecordsEquivalent(existing, merged) && messageListsEquivalent(existingMessages, messages) && sessionStatusAuthorityEquivalent(state.sessionStatusById[sessionId], nextSessionStatus)) return state;
  return {
    ...state,
    sessionOrder: state.sessionOrder.includes(sessionId) ? state.sessionOrder : [sessionId, ...state.sessionOrder],
    sessionsById: { ...state.sessionsById, [sessionId]: merged },
    sessionStatusById: sessionStatus
      ? { ...state.sessionStatusById, [sessionId]: nextSessionStatus ?? sessionStatus }
      : state.sessionStatusById,
    messagesBySessionId: {
      ...state.messagesBySessionId,
      [sessionId]: messages
    }
  };
}

function reduceSessionList(state: WorkbenchServerState, sessions: WorkbenchSessionRecord[]): WorkbenchServerState {
  const sessionIds = sessions.map((session) => session.sessionId).filter((sessionId): sessionId is string => Boolean(sessionId));
  const sessionsById = { ...state.sessionsById };
  const sessionStatusById = { ...state.sessionStatusById };
  for (const session of sessions) {
    const sessionId = session.sessionId;
    if (!sessionId) continue;
    const existing = sessionsById[sessionId];
    const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages;
    const messages = Array.isArray(existingMessages) ? existingMessages : Array.isArray(session.messages) ? session.messages : [];
    sessionsById[sessionId] = mergeSessionRecord(existing, { ...session, messages });
    const sessionStatus = sessionStatusAuthorityFromDetail(session);
    if (sessionStatus) sessionStatusById[sessionId] = mergeSessionStatusAuthority(sessionStatusById[sessionId], sessionStatus);
  }
  return {
    ...state,
    sessionOrder: unique(sessionIds),
    sessionsById,
    sessionStatusById
  };
}

function reduceSessionMessages(state: WorkbenchServerState, sessionId: string | null | undefined, messages: ChatMessage[]): WorkbenchServerState {
  if (!sessionId) return state;
  const existing = state.sessionsById[sessionId];
  const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages ?? [];
  const mergedMessages = mergeMessageList(existingMessages, messages);
  const session = existing ? { ...existing, messages: mergedMessages, messageCount: mergedMessages.length } : null;
  return {
    ...state,
    sessionsById: session ? { ...state.sessionsById, [sessionId]: session } : state.sessionsById,
    messagesBySessionId: { ...state.messagesBySessionId, [sessionId]: mergedMessages }
  };
}

function reduceTurnStatus(state: WorkbenchServerState, turn: TurnStatusAuthority): WorkbenchServerState {
  const nextTurn = mergeTurnStatusAuthority(state.turnStatusByTraceId[turn.traceId], turn);
  if (turnStatusEquivalent(state.turnStatusByTraceId[turn.traceId], nextTurn)) return state;
  const nextTurns = { ...state.turnStatusByTraceId, [turn.traceId]: nextTurn };
  const sessionStatus = sessionStatusAuthorityFromTurn(nextTurn);
  return {
    ...state,
    turnStatusByTraceId: nextTurns,
    sessionStatusById: sessionStatus
      ? { ...state.sessionStatusById, [sessionStatus.sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[sessionStatus.sessionId], sessionStatus) }
      : state.sessionStatusById
  };
}

function reduceMessageSnapshot(state: WorkbenchServerState, sessionId: string | null | undefined, message: ChatMessage | null | undefined): WorkbenchServerState {
  const id = sessionId || message?.sessionId;
  if (!id || !message) return state;
  const existing = state.messagesBySessionId[id] ?? state.sessionsById[id]?.messages ?? [];
  const index = existing.findIndex((item) => messageMatchesSnapshot(item, message));
  if (index < 0 && message.role === "agent" && existing.length > 0 && !canAppendAuthoritativeAgentSnapshot(existing, message)) return state;
  const merged = index >= 0
    ? existing.map((item, itemIndex) => itemIndex === index ? mergeMessageSnapshot(item, message) : item)
    : [...existing, message];
  return reduceSessionMessages(state, id, merged);
}

function reduceMessageUpsert(state: WorkbenchServerState, sessionId: string | null | undefined, message: ChatMessage | null | undefined): WorkbenchServerState {
  const id = sessionId || message?.sessionId;
  if (!id || !message) return state;
  const existingSession = state.sessionsById[id];
  const existing = state.messagesBySessionId[id] ?? existingSession?.messages ?? [];
  const index = existing.findIndex((item) => messageMatchesSnapshot(item, message));
  const current = index >= 0 ? existing[index] : undefined;
  const mergedMessage = current ? mergeMessageSnapshot(current, message) : message;
  if (current && messageEquivalent(current, mergedMessage)) return state;
  const mergedMessages = index >= 0
    ? existing.map((item, itemIndex) => itemIndex === index ? mergedMessage : item)
    : [...existing, mergedMessage];
  const session = existingSession ? { ...existingSession, messages: mergedMessages, messageCount: mergedMessages.length } : null;
  return {
    ...state,
    sessionsById: session ? { ...state.sessionsById, [id]: session } : state.sessionsById,
    messagesBySessionId: { ...state.messagesBySessionId, [id]: mergedMessages }
  };
}

function messageMatchesSnapshot(existing: ChatMessage, incoming: ChatMessage): boolean {
  const existingId = existing.messageId ?? existing.id;
  const incomingId = incoming.messageId ?? incoming.id;
  if (existingId && incomingId && existingId === incomingId) return true;
  return Boolean(existing.traceId && incoming.traceId && existing.traceId === incoming.traceId && existing.role === incoming.role);
}

function canAppendAuthoritativeAgentSnapshot(existing: ChatMessage[], incoming: ChatMessage): boolean {
  const incomingId = incoming.messageId ?? incoming.id;
  const incomingTraceId = incoming.traceId ?? incoming.runnerTrace?.traceId;
  if (!incomingId || !incomingTraceId) return false;
  return existing.some((message) => {
    const existingTraceId = message.traceId ?? message.runnerTrace?.traceId;
    return existingTraceId === incomingTraceId;
  });
}

function mergeMessageSnapshot(existing: ChatMessage, incoming: ChatMessage): ChatMessage {
  const normalizedIncoming = normalizeUnsealedTerminalMessage(incoming);
  const merged = {
    ...existing,
    ...normalizedIncoming,
    runnerTrace: normalizedIncoming.runnerTrace ?? existing.runnerTrace ?? null,
    traceAutoLifecycle: existing.traceAutoLifecycle,
    updatedAt: normalizedIncoming.updatedAt ?? existing.updatedAt
  };
  return sealExistingMessageTiming(existing, merged);
}

function mergeMessageList(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[] {
  const merged = existing.map((message) => normalizeUnsealedTerminalMessage(message));
  for (const rawMessage of incoming) {
    const message = normalizeUnsealedTerminalMessage(rawMessage);
    const index = merged.findIndex((item) => messageMatchesSnapshot(item, message));
    if (index >= 0) {
      const existing = merged[index];
      merged[index] = existing ? sealExistingMessageTiming(existing, message) : message;
    } else {
      merged.push(message);
    }
  }
  return merged;
}

function normalizeUnsealedTerminalMessage(message: ChatMessage): ChatMessage {
  if (message.role !== "agent") return message;
  if (!isTerminalMessageStatus(message.status)) return message;
  if (messageHasTerminalResponse(message)) return message;
  const timing = message.timing ? { ...message.timing, finishedAt: null, durationMs: null } : message.timing;
  return {
    ...message,
    status: "running",
    traceAutoLifecycle: message.traceAutoLifecycle ?? "running",
    timing,
    finishedAt: null,
    durationMs: null
  };
}

function sealExistingMessageTiming(existing: ChatMessage, incoming: ChatMessage): ChatMessage {
  const existingTerminal = messageIsTerminalForState(existing);
  const incomingTerminal = messageIsTerminalForState(incoming);
  if (!existingTerminal && incomingTerminal) return sealTerminalTransitionMessageTiming(existing, incoming);
  if (existingTerminal) return sealExistingTerminalMessageTiming(existing, incoming);
  return sealExistingRunningMessageTiming(existing, incoming);
}

function sealExistingRunningMessageTiming(existing: ChatMessage, incoming: ChatMessage): ChatMessage {
  if (!isRunningAgentMessageForTiming(existing) || !isRunningAgentMessageForTiming(incoming)) return incoming;
  const existingTiming = messageTimingProjection(existing);
  if (!existingTiming?.startedAt) return incoming;
  const incomingTiming = messageTimingProjection(incoming) ?? null;
  const timing: NonNullable<ChatMessage["timing"]> = {
    ...(incomingTiming ?? {}),
    startedAt: existingTiming.startedAt,
    lastEventAt: incomingTiming?.lastEventAt ?? existingTiming.lastEventAt ?? existingTiming.startedAt,
    finishedAt: null,
    durationMs: null,
    valuesRedacted: incomingTiming?.valuesRedacted !== false && existingTiming.valuesRedacted !== false,
  };
  return { ...incoming, timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: null, durationMs: null };
}

function sealExistingTerminalMessageTiming(existing: ChatMessage, incoming: ChatMessage): ChatMessage {
  if (!messageIsTerminalForState(existing)) return incoming;
  const timing = sealedTerminalTimingProjection(existing, incoming);
  const patch: Partial<ChatMessage> = sealedTerminalMessagePatch(existing);
  if (timing && timing.durationMs != null) Object.assign(patch, {
    timing,
    startedAt: timing.startedAt ?? null,
    lastEventAt: timing.lastEventAt ?? null,
    finishedAt: timing.finishedAt ?? null,
    durationMs: timing.durationMs ?? null
  });
  return { ...incoming, ...patch };
}

function sealedTerminalMessagePatch(existing: ChatMessage): Partial<ChatMessage> {
  const patch: Partial<ChatMessage> = {
    status: existing.status,
    traceAutoLifecycle: existing.traceAutoLifecycle ?? "terminal"
  };
  if (typeof existing.text === "string" && existing.text.trim()) patch.text = existing.text;
  const finalResponse = (existing as Record<string, unknown>).finalResponse;
  if (finalResponse) patch.finalResponse = finalResponse;
  return patch;
}

function sealTerminalTransitionMessageTiming(existing: ChatMessage, incoming: ChatMessage): ChatMessage {
  const incomingTiming = terminalTimingProjection(incoming);
  if (positiveDuration(incomingTiming?.durationMs) !== null) return incoming;
  const existingTiming = messageTimingProjection(existing);
  const startedAt = existingTiming?.startedAt ?? incomingTiming?.startedAt ?? timestampOrNull(existing.createdAt) ?? null;
  const finishedAt = terminalFinishedAt(startedAt, incomingTiming?.finishedAt) ?? incomingTiming?.finishedAt ?? null;
  const durationMs = positiveDuration(durationBetween(startedAt, finishedAt)) ?? positiveDuration(incomingTiming?.durationMs) ?? (incomingTiming?.durationMs === 0 ? 0 : null);
  if (durationMs === null) return incoming;
  const timing: NonNullable<ChatMessage["timing"]> = {
    ...(incomingTiming ?? {}),
    startedAt,
    lastEventAt: incomingTiming?.lastEventAt ?? finishedAt ?? startedAt,
    finishedAt: finishedAt ?? incomingTiming?.finishedAt ?? null,
    durationMs,
    valuesRedacted: incomingTiming?.valuesRedacted !== false && existingTiming?.valuesRedacted !== false,
  };
  return { ...incoming, timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: timing.finishedAt ?? null, durationMs: timing.durationMs ?? null };
}

function sealedTerminalTimingProjection(existing: ChatMessage, incoming: ChatMessage): ChatMessage["timing"] | null {
  const existingTiming = terminalTimingProjection(existing);
  const incomingTiming = terminalTimingProjection(incoming);
  const existingDuration = positiveDuration(existingTiming?.durationMs);
  if (existingTiming && existingDuration !== null) return existingTiming;
  const incomingDuration = positiveDuration(incomingTiming?.durationMs) ?? positiveDuration(durationBetween(incomingTiming?.startedAt, terminalFinishedAt(incomingTiming?.startedAt, incomingTiming?.finishedAt)));
  if (incomingTiming && incomingDuration !== null) return { ...(existingTiming ?? {}), ...incomingTiming, durationMs: incomingDuration, valuesRedacted: incomingTiming.valuesRedacted !== false && existingTiming?.valuesRedacted !== false };
  return existingTiming ?? incomingTiming;
}

function terminalTimingProjection(message: ChatMessage): ChatMessage["timing"] | null {
  return messageTimingProjection(message);
}

function messageTimingProjection(message: ChatMessage): ChatMessage["timing"] | null {
  const timing = message.timing && typeof message.timing === "object" ? message.timing : null;
  const startedAt = timestampOrNull(timing?.startedAt ?? message.startedAt);
  const lastEventAt = timestampOrNull(timing?.lastEventAt ?? message.lastEventAt);
  const finishedAt = timestampOrNull(timing?.finishedAt ?? message.finishedAt);
  const durationMs = nonNegativeDuration(timing?.durationMs ?? message.durationMs);
  if (!startedAt && !lastEventAt && !finishedAt && durationMs == null) return null;
  return { ...(timing ?? {}), startedAt, lastEventAt, finishedAt, durationMs, valuesRedacted: timing?.valuesRedacted !== false };
}

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

function isTerminalMessageStatus(value: unknown): boolean {
  return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(normalizedMessageStatus(value));
}

function normalizedMessageStatus(value: unknown): string {
  return String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
}

function timestampOrNull(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const text = value.trim();
  return text && Number.isFinite(Date.parse(text)) ? text : null;
}

function nonNegativeDuration(value: unknown): number | null {
  if (value === null || value === undefined || value === "") return null;
  const number = Number(value);
  return Number.isFinite(number) && number >= 0 ? Math.trunc(number) : null;
}

function positiveDuration(value: unknown): number | null {
  const duration = nonNegativeDuration(value);
  return duration !== null && duration > 0 ? duration : null;
}

function durationBetween(startedAt: unknown, finishedAt: unknown): number | null {
  const startedMs = timestampMs(startedAt);
  const finishedMs = timestampMs(finishedAt);
  if (startedMs === null || finishedMs === null || finishedMs <= startedMs) return null;
  return Math.trunc(finishedMs - startedMs);
}

function terminalFinishedAt(startedAt: unknown, ...values: unknown[]): string | null {
  for (const value of values) {
    const timestamp = timestampOrNull(value);
    if (durationBetween(startedAt, timestamp) !== null) return timestamp;
  }
  return null;
}

function timestampMs(value: unknown): number | null {
  const timestamp = timestampOrNull(value);
  if (!timestamp) return null;
  const parsed = Date.parse(timestamp);
  return Number.isFinite(parsed) ? parsed : null;
}

function mergeSessionRecord(existing: WorkbenchSessionRecord | undefined, incoming: WorkbenchSessionRecord): WorkbenchSessionRecord {
  if (!existing) return incoming;
  const incomingMessages = Array.isArray(incoming.messages) ? incoming.messages : undefined;
  const existingMessages = Array.isArray(existing.messages) ? existing.messages : [];
  return {
    ...existing,
    ...incoming,
    messages: incomingMessages ?? existing.messages,
    messageCount: incoming.messageCount ?? existing.messageCount ?? incomingMessages?.length ?? existingMessages.length
  };
}

function sessionRecordsEquivalent(left: WorkbenchSessionRecord | null | undefined, right: WorkbenchSessionRecord | null | undefined): boolean {
  if (left === right) return true;
  if (!left || !right) return false;
  const leftMessages = Array.isArray(left.messages) ? left.messages : [];
  const rightMessages = Array.isArray(right.messages) ? right.messages : [];
  const leftRecord = { ...left, messages: undefined };
  const rightRecord = { ...right, messages: undefined };
  return valuesEquivalent(leftRecord, rightRecord) && messageListsEquivalent(leftMessages, rightMessages);
}

function sessionStatusAuthorityEquivalent(left: SessionStatusAuthority | null | undefined, right: SessionStatusAuthority | null | undefined): boolean {
  if (left === right) return true;
  if (!left || !right) return !left && !right;
  return left.sessionId === right.sessionId
    && left.status === right.status
    && (left.updatedAt ?? null) === (right.updatedAt ?? null)
    && (left.lastTraceId ?? null) === (right.lastTraceId ?? null)
    && valuesEquivalent(left.projection ?? null, right.projection ?? null);
}

function messageListsEquivalent(left: ChatMessage[], right: ChatMessage[]): boolean {
  if (left === right) return true;
  if (left.length !== right.length) return false;
  for (let index = 0; index < left.length; index += 1) {
    if (!valuesEquivalent(left[index], right[index])) return false;
  }
  return true;
}

function messageEquivalent(left: ChatMessage | undefined, right: ChatMessage | undefined): boolean {
  return valuesEquivalent(left ?? null, right ?? null);
}

function turnStatusEquivalent(left: TurnStatusAuthority | undefined, right: TurnStatusAuthority | undefined): boolean {
  if (left === right) return true;
  if (!left || !right) return false;
  const leftComparable = { ...left, loadedAt: undefined };
  const rightComparable = { ...right, loadedAt: undefined };
  return valuesEquivalent(leftComparable, rightComparable);
}

function valuesEquivalent(left: unknown, right: unknown): boolean {
  if (Object.is(left, right)) return true;
  if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
  if (Array.isArray(left) || Array.isArray(right)) {
    if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
    for (let index = 0; index < left.length; index += 1) {
      if (!valuesEquivalent(left[index], right[index])) return false;
    }
    return true;
  }
  const leftRecord = left as Record<string, unknown>;
  const rightRecord = right as Record<string, unknown>;
  const leftKeys = Object.keys(leftRecord).filter((key) => leftRecord[key] !== undefined).sort();
  const rightKeys = Object.keys(rightRecord).filter((key) => rightRecord[key] !== undefined).sort();
  if (leftKeys.length !== rightKeys.length) return false;
  for (let index = 0; index < leftKeys.length; index += 1) {
    const key = leftKeys[index];
    if (key === undefined || key !== rightKeys[index]) return false;
    if (!valuesEquivalent(leftRecord[key], rightRecord[key])) return false;
  }
  return true;
}

function projectSessionRecord(session: WorkbenchSessionRecord | null | undefined, authority: SessionStatusAuthority | null | undefined): WorkbenchSessionRecord | null {
  if (!session) return null;
  if (!authority?.status || normalizedMessageStatus(session.status) === "archived") return session;
  const projection = authority.projection ?? session.projection;
  return {
    ...session,
    status: authority.status,
    updatedAt: textValue(authority.updatedAt) ?? session.updatedAt,
    lastTraceId: textValue(authority.lastTraceId) ?? session.lastTraceId,
    projection,
    projectionHealth: authority.projection?.projectionHealth ?? session.projectionHealth,
    projectionStatus: authority.projection?.projectionStatus ?? session.projectionStatus
  };
}

function sessionStatusAuthorityFromDetail(session: WorkbenchSessionRecord): SessionStatusAuthority | null {
  const sessionId = session.sessionId;
  if (!sessionId || typeof session.status !== "string") return null;
  return {
    sessionId,
    status: session.status,
    updatedAt: typeof session.updatedAt === "string" ? session.updatedAt : null,
    lastTraceId: typeof session.lastTraceId === "string" ? session.lastTraceId : null,
    projection: projectionFromSessionRecord(session),
    loadedAt: new Date().toISOString()
  };
}

function sessionStatusAuthorityFromTurn(turn: TurnStatusAuthority): SessionStatusAuthority | null {
  const sessionId = textValue(turn.sessionId);
  if (!sessionId) return null;
  const status = textValue(turn.status) ?? (turn.running === true ? "running" : null);
  if (!status) return null;
  return {
    sessionId,
    status,
    updatedAt: textValue(turn.updatedAt),
    lastTraceId: textValue(turn.traceId),
    projection: turn.projection ?? null,
    loadedAt: turn.loadedAt ?? new Date().toISOString()
  };
}

function mergeSessionStatusAuthority(existing: SessionStatusAuthority | undefined, incoming: SessionStatusAuthority): SessionStatusAuthority {
  if (existing && isTerminalSessionStatusAuthority(existing) && !isTerminalSessionStatusAuthority(incoming) && isSameTraceAuthority(existing, incoming) && isRunningSessionStatus(incoming.status)) return existing;
  if (existing && isRunningSessionStatus(existing.status) && isTerminalSessionStatusAuthority(incoming) && isDifferentTraceAuthority(existing, incoming)) return existing;
  return {
    ...(existing ?? {}),
    ...incoming,
    projection: incoming.projection ?? existing?.projection ?? null,
    loadedAt: incoming.loadedAt ?? existing?.loadedAt ?? null
  };
}

function isTerminalSessionStatusAuthority(session: SessionStatusAuthority | undefined): boolean {
  if (!session) return false;
  return isTerminalMessageStatus(session.status) && !isRunningSessionStatus(session.status);
}

function mergeTurnStatusAuthority(existing: TurnStatusAuthority | undefined, incoming: TurnStatusAuthority): TurnStatusAuthority {
  if (existing && isTerminalTurnStatusAuthority(existing) && !isTerminalTurnStatusAuthority(incoming) && isRunningSessionStatus(incoming.status)) return existing;
  return {
    ...(existing ?? {}),
    ...incoming,
    projection: incoming.projection ?? existing?.projection ?? null,
    loadedAt: incoming.loadedAt ?? existing?.loadedAt ?? null
  };
}

function isTerminalTurnStatusAuthority(turn: TurnStatusAuthority): boolean {
  return turn.terminal === true || (turn.running !== true && isTerminalMessageStatus(turn.status));
}

function messageIsTerminalForState(message: ChatMessage): boolean {
  return messageHasTerminalResponse(message);
}

function isSameTraceAuthority(left: SessionStatusAuthority, right: SessionStatusAuthority): boolean {
  const leftTrace = textValue(left.lastTraceId);
  const rightTrace = textValue(right.lastTraceId);
  return Boolean(leftTrace && rightTrace && leftTrace === rightTrace);
}

function isDifferentTraceAuthority(left: SessionStatusAuthority | undefined, right: SessionStatusAuthority | undefined): boolean {
  const leftTrace = textValue(left?.lastTraceId);
  const rightTrace = textValue(right?.lastTraceId);
  return Boolean(leftTrace && rightTrace && leftTrace !== rightTrace);
}

function isRunningSessionStatus(value: unknown): boolean {
  return ["", "pending", "running", "accepted", "queued", "dispatching", "streaming", "processing", "retrying", "busy", "creating"].includes(normalizedMessageStatus(value));
}

function messageHasTerminalResponse(message: ChatMessage): boolean {
  if (message.role !== "agent") return false;
  const status = normalizedMessageStatus(message.status);
  if (!isTerminalMessageStatus(status)) return false;
  return Boolean(messageFinalResponseText(message));
}

function messageFinalResponseText(message: ChatMessage): string | null {
  return textValue(message.text) ?? textValue(message.content) ?? nestedTextValue((message as Record<string, unknown>).finalResponse);
}

function nestedTextValue(value: unknown): string | null {
  const direct = textValue(value);
  if (direct) return direct;
  if (Array.isArray(value)) return value.map(nestedTextValue).filter((item): item is string => Boolean(item)).join("\n") || null;
  if (!value || typeof value !== "object") return null;
  const record = value as Record<string, unknown>;
  return textValue(record.text) ?? textValue(record.content) ?? textValue(record.message) ?? textValue(record.summary) ?? textValue(record.preview);
}

function textValue(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const text = value.trim();
  return text || null;
}

function projectionFromSessionRecord(session: WorkbenchSessionRecord): ProjectionDiagnostic | null {
  const turnSummary = session.turnSummary && typeof session.turnSummary === "object" ? session.turnSummary as Record<string, unknown> : null;
  const direct = session.projection && typeof session.projection === "object" ? session.projection : null;
  const turnProjection = turnSummary?.projection && typeof turnSummary.projection === "object" ? turnSummary.projection as ProjectionDiagnostic : null;
  if (direct) return direct;
  if (turnProjection) return turnProjection;
  const blocker = session.blocker && typeof session.blocker === "object" ? session.blocker : turnSummary?.blocker && typeof turnSummary.blocker === "object" ? turnSummary.blocker : null;
  if (!session.projectionHealth && !session.projectionStatus && !blocker) return null;
  return {
    projectionStatus: typeof session.projectionStatus === "string" ? session.projectionStatus : typeof turnSummary?.projectionStatus === "string" ? turnSummary.projectionStatus : null,
    projectionHealth: typeof session.projectionHealth === "string" ? session.projectionHealth : typeof turnSummary?.projectionHealth === "string" ? turnSummary.projectionHealth : null,
    blocker: blocker as ProjectionDiagnostic["blocker"] ?? null
  };
}

function withoutKey<T>(source: Record<string, T>, key: string): Record<string, T> {
  if (!(key in source)) return source;
  const next = { ...source };
  delete next[key];
  return next;
}

function cleanupSessionScopedAuthority(state: WorkbenchServerState, sessionIds: string[]): WorkbenchServerState {
  if (sessionIds.length === 0) return state;
  const dropped = new Set(sessionIds);
  const traceIds = traceIdsForSessions(state, dropped);
  return {
    ...state,
    sessionStatusById: omitRecordKeys(state.sessionStatusById, dropped),
    turnStatusByTraceId: omitRecordKeysByValue(state.turnStatusByTraceId, (turn, traceId) => dropped.has(String(turn.sessionId ?? "")) || traceIds.has(traceId)),
    traceById: omitRecordKeysByValue(state.traceById, (trace, traceId) => dropped.has(String(trace.sessionId ?? "")) || traceIds.has(traceId))
  };
}

function droppedSessionIds(state: WorkbenchServerState, keep: Set<string>): string[] {
  return [
    ...Object.keys(state.sessionsById),
    ...Object.keys(state.messagesBySessionId),
    ...Object.keys(state.sessionStatusById),
    ...state.sessionOrder
  ].filter((sessionId, index, list) => Boolean(sessionId) && !keep.has(sessionId) && list.indexOf(sessionId) === index);
}

function traceIdsForSessions(state: WorkbenchServerState, sessionIds: Set<string>): Set<string> {
  const traces = new Set<string>();
  for (const sessionId of sessionIds) {
    for (const message of state.messagesBySessionId[sessionId] ?? []) {
      const traceId = message.traceId ?? message.runnerTrace?.traceId;
      if (traceId) traces.add(traceId);
    }
  }
  for (const [traceId, turn] of Object.entries(state.turnStatusByTraceId)) if (sessionIds.has(String(turn.sessionId ?? ""))) traces.add(traceId);
  for (const [traceId, trace] of Object.entries(state.traceById)) if (sessionIds.has(String(trace.sessionId ?? ""))) traces.add(traceId);
  return traces;
}

function omitRecordKeys<T>(record: Record<string, T>, keys: Set<string>): Record<string, T> {
  const next: Record<string, T> = {};
  for (const key of Object.keys(record)) if (!keys.has(key)) next[key] = record[key] as T;
  return next;
}

function omitRecordKeysByValue<T>(record: Record<string, T>, shouldDrop: (value: T, key: string) => boolean): Record<string, T> {
  const next: Record<string, T> = {};
  for (const [key, value] of Object.entries(record)) if (!shouldDrop(value as T, key)) next[key] = value as T;
  return next;
}

function unique(values: string[]): string[] {
  return [...new Set(values)];
}
