/*
 * SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; draft-2026-06-27-p0-read-model-timeline-contract; draft-2026-06-28-p0-d518-session-timeline-consistency; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery; draft-2026-06-27-p0-workbench-read-api-contract; PJ2026-010401 Web工作台 draft-2026-06-20-p2-terminal-outbox-recovery; draft-2026-06-27-p0-workbench-read-model-contract; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
 * 职责: Workbench REST read model。只读投影 session/message/turn/trace facts，不执行 AgentRun sync、billing finalize 或 workspace repair。
 */
import { createHash } from "node:crypto";

import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import {
  parsePositiveInteger,
  safeConversationId,
  safeOpaqueId,
  safeSessionId,
  safeTraceId,
  sendJson
} from "./server-http-utils.ts";
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
import { createWorkbenchRuntimeClient } from "./workbench-runtime-client.ts";
import { buildWorkbenchSessionDetail, compactLaunchContext, includeMessagesForSessionDetail } from "./workbench-session-detail-response.ts";
import { durableTraceStatus, RUNNING_STATUSES, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
import { emitCodeAgentOtelSpan, emitHttpServerRequestSpan } from "./otel-trace.ts";
import * as workbenchFacts from "./server-workbench-facts.ts";

const DEFAULT_PAGE_LIMIT = 50;
const DEFAULT_SESSION_LIST_LIMIT = 20;
const MAX_PAGE_LIMIT = 100;
const WORKBENCH_SESSION_LIST_PAGE_FAMILIES = Object.freeze(["sessions"]);
const WORKBENCH_SESSION_DETAIL_FAMILIES = Object.freeze(["sessions", "messages", "parts", "turns", "checkpoints"]);
const WORKBENCH_SESSION_MESSAGE_PAGE_FAMILIES = Object.freeze(["sessions", "messages", "parts", "turns", "checkpoints"]);
const WORKBENCH_TRACE_EVENT_METADATA_FAMILIES = Object.freeze(["sessions", "messages", "parts", "turns", "checkpoints"]);
const WORKBENCH_TRACE_EVENT_PAGE_FAMILIES = Object.freeze(["traceEvents"]);

function attachWorkbenchReadModelDiagnosticOtel(request, fields = {}) {
  const context = request?.hwlabHttpRequestContext;
  if (!context) return;
  const code = textValue(fields.code) || "workbench_read_model_not_found";
  const route = textValue(fields.route) || context.route || "/v1/workbench";
  context.route = route;
  context.lastHttpError = { code, category: "workbench_read_model", layer: "workbench.read_model" };
  context.otelAttributes = {
    ...(context.otelAttributes ?? {}),
    "workbench.route": route,
    "workbench.read_model.route": route,
    "workbench.read_model.result": code,
    "workbench.read_model.count": Number.isFinite(Number(fields.count)) ? Math.trunc(Number(fields.count)) : null,
    "workbench.session_id": fields.sessionId ?? null,
    "workbench.trace_id": fields.traceId ?? null,
    "workbench.turn_id": fields.turnId ?? null,
    traceId: fields.traceId ?? null,
    "error.code": code,
    "error.category": "workbench_read_model",
    "error.layer": "workbench.read_model"
  };
}

const {
  factSessionSummary,
  factSessionDetail,
  factMessagesForSession,
  canonicalFactMessageGroupsForSession,
  factMessageCanonicalGroupKey,
  selectCanonicalFactMessage,
  factMessageCanonicalScore,
  factMessageHasFinalResponsePart,
  workbenchLifecycleMessageId,
  factMessageDto,
  factMessageAuthorityText,
  factPartsHaveFinalResponse,
  factSyntheticTerminalFinalResponse,
  isTerminalProjectionStatus,
  firstFactPartText,
  factMessageFinalResponseText,
  factTerminalMessageForTrace,
  factTerminalStatusFromMessageProjection,
  factPartDto,
  factTurnForTrace,
  factTurnSnapshot,
  factTraceSnapshot,
  factTraceTimingProjection,
  factTraceEventIsTerminalAuthority,
  factTraceEventTerminalTimes,
  factTraceEventDto,
  factProjectionForTrace,
  factTimingProjection,
  factCombinedTimingProjection,
  factTimingSource,
  firstTimestampIso,
  latestTimestampIso,
  elapsedFactMs,
  timestampIso,
  factCheckpointForTrace,
  workbenchProjectionStoreError,
  nonNegativeIntegerOrNull,
  factSessionId,
  factLastTraceId,
  factLatestTraceIdForSession,
  factCurrentTraceForSession,
  factMessageTraceCandidate,
  factTraceCandidate,
  compareTraceCandidatesDesc,
  nonUnknownStatus,
  factTurnId,
  factUpdatedAt,
  factSeq,
  factProjectedSeq,
  normalizeProjectionStatus,
  normalizeProjectionHealth,
  compareFactSessionsDesc,
  compareFactMessagesAsc,
  compareFactMessagesForSessionAsc,
  factMessageTimelineKey,
  factTimelineAnchorMessageForTrace,
  factMessageTimelineRoleRank,
  firstFiniteNumber,
  compareOptionalNumberAsc,
  compareFactPartsAsc,
  compareFactTraceEventsAsc,
  compareFactRecordsDesc,
  compareTimestampAsc,
  compareTimestampDesc,
  compareOptionalTimestampAsc,
  compareNumberAsc,
  compareNumberDesc,
  compareText,
  timestampMs,
  optionalTimestampMs,
  numericValue,
  mergeFactSets,
  emptyFactSet,
  factArray,
  uniqueText,
  eventSeq,
  normalizeStatus,
  normalizeTerminalStatus,
  firstUserPreview,
  latestUserPreview,
  latestValidMessagePreview,
  latestAssistantPreview,
  sessionTitleFromMessages,
  sessionPreviewFromMessages,
  boundedPreviewText,
  isAssistantLikeRole,
  objectValue,
  textValue,
  messageAuthorityTextValue,
  safeTurnId,
  safeMessageId,
  safePartId,
  hash
} = workbenchFacts;

export async function handleWorkbenchReadModelHttp(request, response, url, options = {}) {
  try {
    const perf = options.backendPerformance;
    const auth = perf ? await perf.measure("workbench_auth", () => authenticateWorkbenchRead(request, response, options)) : await authenticateWorkbenchRead(request, response, options);
    if (!auth) return;

    if (url.pathname === "/v1/workbench/sessions") {
      if (request.method !== "GET") return methodNotAllowed(response, "GET");
      await (perf ? perf.measure("workbench_session_list", () => handleWorkbenchSessionList(request, response, url, options, auth.actor)) : handleWorkbenchSessionList(request, response, url, options, auth.actor));
      return;
    }

    const sessionMessagesMatch = url.pathname.match(/^\/v1\/workbench\/sessions\/([^/]+)\/messages$/u);
    if (sessionMessagesMatch) {
      if (request.method !== "GET") return methodNotAllowed(response, "GET");
      await (perf ? perf.measure("workbench_message_page", () => handleWorkbenchMessagePage(request, response, url, options, auth.actor, decodeURIComponent(sessionMessagesMatch[1]))) : handleWorkbenchMessagePage(request, response, url, options, auth.actor, decodeURIComponent(sessionMessagesMatch[1])));
      return;
    }

    const sessionMatch = url.pathname.match(/^\/v1\/workbench\/sessions\/([^/]+)$/u);
    if (sessionMatch) {
      if (request.method !== "GET") return methodNotAllowed(response, "GET");
      await (perf ? perf.measure("workbench_session_detail", () => handleWorkbenchSessionDetail(request, response, url, options, auth.actor, decodeURIComponent(sessionMatch[1]))) : handleWorkbenchSessionDetail(request, response, url, options, auth.actor, decodeURIComponent(sessionMatch[1])));
      return;
    }

    const turnMatch = url.pathname.match(/^\/v1\/workbench\/turns\/([^/]+)$/u);
    if (turnMatch) {
      if (request.method !== "GET") return methodNotAllowed(response, "GET");
      await (perf ? perf.measure("workbench_turn_snapshot", () => handleWorkbenchTurnSnapshot(request, response, url, options, auth.actor, decodeURIComponent(turnMatch[1]))) : handleWorkbenchTurnSnapshot(request, response, url, options, auth.actor, decodeURIComponent(turnMatch[1])));
      return;
    }

    const traceEventsMatch = url.pathname.match(/^\/v1\/workbench\/traces\/([^/]+)\/events$/u);
    if (traceEventsMatch) {
      if (request.method !== "GET") return methodNotAllowed(response, "GET");
      await (perf ? perf.measure("workbench_trace_events", () => handleWorkbenchTraceEventPage(request, response, url, options, auth.actor, decodeURIComponent(traceEventsMatch[1]))) : handleWorkbenchTraceEventPage(request, response, url, options, auth.actor, decodeURIComponent(traceEventsMatch[1])));
      return;
    }

    sendJson(response, 404, workbenchError("workbench_route_not_found", "Workbench read model route is not implemented.", { route: url.pathname }));
  } catch (error) {
    handleWorkbenchReadModelFailure(response, url, options, error);
  }
}


export function recordWorkbenchTurnReadMetric(options = {}, url, statusCode, body = {}, startedAt = nowMs()) {
  const performanceStore = options.backendPerformanceStore ?? options.backendPerformance;
  if (!performanceStore?.recordWorkbenchTurnRead) return;
  performanceStore.recordWorkbenchTurnRead({
    route: url?.pathname ?? "/v1/workbench/turns/:id",
    status: body?.status ?? statusClassLabel(statusCode),
    degradedReason: workbenchTurnDegradedReason(body),
    durationMs: nowMs() - startedAt,
    responseBytes: responseBodyBytes(body),
    timedOut: statusCode === 504 || body?.error?.code === "timeout"
  });
}

export function recordWorkbenchCacheMetric(options = {}, url, body = {}, startedAt = nowMs()) {
  const performanceStore = options.backendPerformanceStore ?? options.backendPerformance;
  if (!performanceStore?.recordWorkbenchCache) return;
  const cache = body?.cache && typeof body.cache === "object" ? body.cache : null;
  if (!cache) return;
  performanceStore.recordWorkbenchCache({
    route: url?.pathname ?? "/v1/workbench",
    cacheKeyClass: cache.class,
    cacheStatus: cache.status,
    operation: "read",
    durationMs: nowMs() - startedAt,
    payloadBytes: cache.payloadBytes,
    cacheAgeMs: cache.cacheAgeMs,
    dbQueryAvoided: cache.dbQueryAvoided,
    freshnessSloMs: cache.freshnessSloMs,
    projectionSeq: cache.projectionSeq
  });
}

export function recordWorkbenchProjectionMetric(options = {}, { result = null, trace = null, projection = null, diagnostic = null } = {}) {
  const performanceStore = options.backendPerformanceStore ?? options.backendPerformance;
  if (!performanceStore?.recordWorkbenchProjection || !projection) return;
  const sourceLatestSeq = workbenchProjectionSourceLatestSeq(result, trace, projection);
  const lastProjectedSeq = Number(diagnostic?.lastProjectedSeq ?? projection?.lastProjectedSeq ?? 0);
  if (!Number.isFinite(sourceLatestSeq) && !Number.isFinite(lastProjectedSeq)) return;
  performanceStore.recordWorkbenchProjection({
    sourceLatestSeq: Number.isFinite(sourceLatestSeq) ? sourceLatestSeq : lastProjectedSeq,
    lastProjectedSeq: Number.isFinite(lastProjectedSeq) ? lastProjectedSeq : 0,
    sourceLatestEventAt: workbenchProjectionSourceLatestAt(result, trace, projection),
    projectedLatestEventAt: diagnostic?.updatedAt ?? projection?.updatedAt ?? trace?.updatedAt ?? result?.updatedAt ?? null,
    terminalSourceAt: workbenchTerminalSourceAt(result, projection),
    terminalProjectedAt: diagnostic?.projectionStatus === "caught-up" ? diagnostic?.updatedAt ?? projection?.updatedAt ?? trace?.updatedAt ?? result?.updatedAt ?? null : null,
    status: projection?.status ?? "unknown",
    reason: workbenchProjectionReason(diagnostic),
    projectionStatus: diagnostic?.projectionStatus ?? "unknown",
    source: "workbench_read_model"
  });
}

export function workbenchProjectionSourceLatestSeq(result = null, trace = null, projection = null) {
  const candidates = [
    result?.agentRun?.lastSeq,
    result?.traceSummary?.agentRun?.lastSeq,
    result?.traceSummary?.lastSeq,
    result?.providerTrace?.lastSeq,
    result?.runnerTrace?.lastEvent?.sourceSeq,
    result?.runnerTrace?.lastEvent?.seq,
    result?.runnerTrace?.eventCount,
    trace?.lastSeq,
    trace?.lastEvent?.sourceSeq,
    trace?.lastEvent?.seq,
    trace?.eventCount,
    projection?.lastProjectedSeq,
    projection?.eventCount
  ];
  for (const value of candidates) {
    const seq = Number(value);
    if (Number.isFinite(seq) && seq >= 0) return Math.floor(seq);
  }
  return NaN;
}

export function workbenchProjectionSourceLatestAt(result = null, trace = null, projection = null) {
  return result?.traceSummary?.updatedAt
    ?? result?.agentRun?.updatedAt
    ?? result?.providerTrace?.updatedAt
    ?? result?.runnerTrace?.updatedAt
    ?? trace?.updatedAt
    ?? result?.updatedAt
    ?? projection?.updatedAt
    ?? null;
}

export function workbenchTerminalSourceAt(result = null, projection = null) {
  if (projection?.terminal !== true) return null;
  return result?.traceSummary?.updatedAt
    ?? result?.agentRun?.updatedAt
    ?? result?.updatedAt
    ?? projection?.updatedAt
    ?? null;
}

export function workbenchProjectionReason(diagnostic = null) {
  if (diagnostic?.blocker) return diagnostic.blocker.code ?? "projection_blocked";
  if (diagnostic?.projectionStatus === "projecting") return "projecting";
  if (diagnostic?.projectionStatus === "caught-up") return "none";
  return diagnostic?.projectionStatus ?? "unknown";
}

export function workbenchTurnDegradedReason(body = {}) {
  if (body?.projection?.blocker?.code) return body.projection.blocker.code;
  if (body?.blocker?.code) return body.blocker.code;
  if (body?.error?.code) return body.error.code;
  if (body?.projectionStatus === "projecting") return "projecting";
  if (body?.projectionStatus === "blocked") return "projection_blocked";
  return "none";
}

export function responseBodyBytes(body = {}) {
  try { return Buffer.byteLength(JSON.stringify(body)); } catch { return 0; }
}

export function statusClassLabel(value) {
  const status = Number(value);
  if (!Number.isFinite(status) || status <= 0) return "unknown";
  return `${Math.floor(status / 100)}xx`;
}

export async function authenticateWorkbenchRead(request, response, options) {
  const access = options.accessController;
  if (!access?.authenticate) {
    sendJson(response, 503, workbenchError("workbench_access_controller_missing", "Workbench read model requires access controller."));
    return null;
  }
  await access.ensureBootstrap?.();
  const auth = await access.authenticate(request, { required: true });
  if (!auth?.ok) {
    sendJson(response, auth?.status ?? 401, auth ?? workbenchError("auth_required", "Authentication is required."));
    return null;
  }
  return auth;
}

export async function handleWorkbenchSessionList(request, response, url, options, actor) {
  const startedAt = nowMs();
  if (url.searchParams.has("projectId") || url.searchParams.has("workspaceId")) return sendJson(response, 400, workbenchError("workbench_authority_removed", "Workbench session list is keyed by sessionId only."));
  const limit = boundedSessionListLimit(url.searchParams.get("limit"));
  const offset = cursorOffset(url.searchParams.get("cursor") ?? url.searchParams.get("after"));
  const includeSessionId = safeSessionId(url.searchParams.get("includeSessionId"));
  const includeRouteId = includeSessionId ?? safeConversationId(url.searchParams.get("includeSessionId"));
  const runtime = workbenchRuntimeReader(request, options);
  if (typeof runtime?.listWorkbenchSessions !== "function") {
    if (typeof runtime?.queryWorkbenchFacts !== "function") {
      throw runtimeDependencyError("workbench_runtime_unconfigured", "Workbench runtime service is required for session list.", false);
    }
    const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
    const payload = await handleWorkbenchSessionListFromFacts(readModel, url, options, actor, { limit, offset, includeRouteId });
    recordWorkbenchCacheMetric(options, url, payload, startedAt);
    emitWorkbenchSessionListReadOtel(request, options, {
      startTimeMs: startedAt,
      statusCode: 200,
      source: "facts",
      limit,
      offset,
      includeSessionId: includeRouteId,
      payload
    });
    return sendJson(response, 200, payload);
  }
  const payload = await runtime.listWorkbenchSessions({
    limit,
    offset,
    cursor: url.searchParams.get("cursor") ?? url.searchParams.get("after") ?? null,
    includeSessionId: includeRouteId ?? null,
    actor: { id: actor.id, role: actor.role ?? "user", valuesRedacted: true }
  });
  recordWorkbenchCacheMetric(options, url, payload, startedAt);
  emitWorkbenchSessionListReadOtel(request, options, {
    startTimeMs: startedAt,
    statusCode: 200,
    source: "runtime",
    limit,
    offset,
    includeSessionId: includeRouteId,
    payload
  });
  return sendJson(response, 200, payload);
}

export async function handleWorkbenchSessionListFromFacts(readModel, url, options, actor, { limit, offset, includeRouteId }) {
  const perf = options.backendPerformance;
  const pageQuery = { ownerUserId: actor.role === "admin" ? undefined : actor.id, limit: limit + offset + 1, families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES, sessionsOrder: "updated_desc", sessionProjection: "summary" };
  const includeQueryPromise = includeRouteId ? queryWorkbenchSessionInclude(readModel, includeRouteId, perf) : null;
  const pageResult = await (perf ? perf.measure("workbench_session_page_query", () => readModel.queryFacts(pageQuery)) : readModel.queryFacts(pageQuery));
  if (pageResult.error) throw pageResult.error;
  let pageFacts = pageResult.facts;
  let naturalPage = visibleFactSessions(pageFacts, actor).slice(offset, offset + limit + 1);
  if (includeRouteId && !naturalPage.some((session) => factSessionMatchesRouteId(session, includeRouteId))) {
    const includeResult = await includeQueryPromise;
    if (includeResult.error) throw includeResult.error;
    const included = visibleFactSessions(includeResult.facts, actor)[0] ?? null;
    if (included) {
      pageFacts = mergeFactSets(pageFacts, includeResult.facts);
      naturalPage = [included, ...naturalPage];
    }
  }
  const pageSessions = naturalPage.slice(0, limit);
  const summaryResult = await (perf ? perf.measure("workbench_session_summary_query", () => queryFactsForSessionSummaries(readModel, pageSessions)) : queryFactsForSessionSummaries(readModel, pageSessions));
  if (summaryResult.error) throw summaryResult.error;
  pageFacts = mergeFactSets(pageFacts, summaryResult.facts);
  const summaries = pageSessions.map((session) => factSessionSummary(session, pageFacts)).filter(Boolean);
  const hasMore = naturalPage.length > limit;
  return {
    ok: true,
    status: "succeeded",
    contractVersion: "workbench-sessions-v1",
    sessions: summaries,
    count: summaries.length,
    cursor: offset > 0 ? cursorFromOffset(offset) : null,
    hasMore,
    nextCursor: hasMore ? cursorFromOffset(offset + limit) : null,
    cache: pageResult.cache ?? summaryResult.cache ?? null,
    valuesRedacted: true,
    secretMaterialStored: false
  };
}

export function runtimeDependencyError(code, message, retryable = true) {
  const error = new Error(message || code);
  error.name = "WorkbenchRuntimeDependencyError";
  error.code = code;
  error.data = { serviceId: "hwlab-workbench-runtime", retryable, transient: retryable, valuesRedacted: true };
  return error;
}

export function workbenchRuntimeReader(request, options) {
  return options.workbenchRuntime
    ?? options.runtimeStore
    ?? createWorkbenchRuntimeClient({ env: options.env ?? process.env, fetch: options.fetch, logger: options.logger, traceparent: request?.hwlabHttpRequestContext?.traceparent });
}

export function workbenchRuntimeFactsReadModel(request, options, actor) {
  const runtime = workbenchRuntimeReader(request, options);
  if (typeof runtime?.queryWorkbenchFacts !== "function") {
    throw runtimeDependencyError("workbench_runtime_unconfigured", "Workbench runtime service is required for Workbench facts reads.", false);
  }
  return {
    async queryFacts(query = {}) {
      try {
        return await runtime.queryWorkbenchFacts({
          ...query,
          actor: { id: actor.id, role: actor.role ?? "user", valuesRedacted: true }
        });
      } catch (error) {
        return { facts: emptyFactSet(), count: 0, durable: true, error: workbenchRuntimeFactsQueryError(error), persistence: null };
      }
    }
  };
}

export function workbenchRuntimeFactsQueryError(error) {
  const normalized = error instanceof Error ? error : new Error(String(error ?? "Workbench facts query failed."));
  const data = objectValue(normalized.data);
  normalized.data = {
    ...data,
    retryable: data.retryable !== false,
    transient: data.transient !== false,
    retryAfterMs: data.retryAfterMs ?? 5000,
    valuesRedacted: true
  };
  return normalized;
}

export function queryWorkbenchSessionInclude(readModel, includeRouteId, perf = null) {
  const run = () => queryFactsByRouteId(readModel, includeRouteId, { families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES });
  const promise = perf ? perf.measure("workbench_session_include_query", run) : run();
  return promise.catch((error) => ({ error }));
}

export async function queryFactsByRouteId(readModel, routeId, query = {}) {
  const baseQuery = { ...query, limit: query.limit ?? MAX_PAGE_LIMIT };
  const sessionId = safeSessionId(routeId);
  if (sessionId) return readModel.queryFacts({ ...baseQuery, sessionId });
  const conversationId = safeConversationId(routeId);
  if (conversationId) {
    const sessionResult = await readModel.queryFacts({ ...baseQuery, families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES, conversationId });
    if (sessionResult.error || queryRequestsOnlySessionFacts(baseQuery)) return sessionResult;
    const resolvedSessionId = factSessionId(factArray(sessionResult.facts?.sessions)[0] ?? null);
    if (!resolvedSessionId) return sessionResult;
    const relatedResult = await readModel.queryFacts({ ...baseQuery, sessionId: resolvedSessionId });
    if (relatedResult.error) return relatedResult;
    const facts = mergeFactSets(sessionResult.facts, relatedResult.facts);
    return { facts, count: Object.values(facts).reduce((sum, rows) => sum + rows.length, 0), durable: relatedResult.durable ?? sessionResult.durable, persistence: relatedResult.persistence ?? sessionResult.persistence ?? null };
  }
  return { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
}

export function queryRequestsOnlySessionFacts(query = {}) {
  const families = Array.isArray(query.families) ? query.families : [];
  return families.length === 1 && families[0] === "sessions";
}

export async function queryFactsForSessionSummaries(readModel, sessions = []) {
  const sessionIds = uniqueText(sessions.map(factSessionId));
  if (sessionIds.length === 0 && uniqueText(sessions.map(factLastTraceId)).length === 0) return { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
  // Session list summary is hot and is also sampled by the two-page web-probe
  // observer. Avoid parallel fan-out here so one list request cannot consume
  // multiple durable-read connections while another page is refreshing.
  const bySession = sessionIds.length > 0
    ? await readModel.queryFacts({ sessionIds, families: ["messages", "parts", "turns"] })
    : { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
  if (bySession.error) return bySession;
  const traceIds = summaryTraceIds(sessions, bySession.facts);
  const byTrace = traceIds.length > 0
    ? await readModel.queryFacts({ traceIds, families: ["checkpoints"] })
    : { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
  if (byTrace.error) return byTrace;
  const facts = mergeFactSets(bySession.facts, byTrace.facts);
  return { facts, count: Object.values(facts).reduce((sum, rows) => sum + rows.length, 0), durable: bySession.durable ?? byTrace.durable, persistence: bySession.persistence ?? byTrace.persistence ?? null };
}

export function summaryTraceIds(sessions = [], facts = {}) {
  return uniqueText([
    ...sessions.map(factLastTraceId),
    ...sessions.map((session) => factCurrentTraceForSession(facts, factSessionId(session), session)?.traceId)
  ]);
}

export function visibleFactSessions(facts, actor) {
  return hideShadowedAgentRunAliasSessions(factArray(facts?.sessions)
    .filter((session) => canActorReadFactSession(session, actor))
    .sort(compareFactSessionsDesc));
}

export function hideShadowedAgentRunAliasSessions(sessions = []) {
  const canonicalTraceIds = new Set(sessions
    .filter((session) => !isAgentRunAliasSessionId(factSessionId(session)))
    .map(factLastTraceId)
    .filter(Boolean));
  if (canonicalTraceIds.size === 0) return sessions;
  return sessions.filter((session) => !isAgentRunAliasSessionId(factSessionId(session)) || !canonicalTraceIds.has(factLastTraceId(session)));
}

export function isAgentRunAliasSessionId(sessionId) {
  return /^ses_agentrun_/u.test(textValue(sessionId));
}

export function canActorReadFactSession(session, actor) {
  if (!session || !actor) return false;
  if (normalizeStatus(session.status) === "archived") return false;
  if (actor.role === "admin") return true;
  return session.ownerUserId === actor.id;
}

export async function visibleFactSessionForTrace(readModel, facts, actor, traceId) {
  const visible = visibleFactSessions(facts, actor);
  const byLastTrace = visible.find((item) => item.lastTraceId === traceId) ?? null;
  if (byLastTrace) return { session: byLastTrace, facts };
  const relatedSessionIds = uniqueText([
    factTurnForTrace(facts, traceId)?.sessionId,
    factArray(facts.messages).find((message) => message.traceId === traceId)?.sessionId,
    factCheckpointForTrace(facts, traceId)?.sessionId
  ]);
  const alreadyLoaded = visible.find((item) => relatedSessionIds.includes(factSessionId(item))) ?? null;
  if (alreadyLoaded) return { session: alreadyLoaded, facts };
  for (const sessionId of relatedSessionIds) {
    const result = await queryFactsByRouteId(readModel, sessionId, { families: ["sessions"], limit: 1 });
    if (result.error) return { error: result.error };
    const session = visibleFactSessions(result.facts, actor)[0] ?? null;
    if (session) return { session, facts: mergeFactSets(facts, result.facts) };
  }
  return { session: null, facts };
}

export function factSessionMatchesRouteId(session, routeId) {
  const sessionId = safeSessionId(routeId);
  if (sessionId && factSessionId(session) === sessionId) return true;
  const conversationId = safeConversationId(routeId);
  return Boolean(conversationId && session?.conversationId === conversationId);
}


export async function handleWorkbenchSessionDetail(request, response, url, options, actor, sessionId) {
  const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
  const includeMessages = includeMessagesForSessionDetail(url.searchParams);
  const result = await queryFactsByRouteId(readModel, sessionId, { families: WORKBENCH_SESSION_DETAIL_FAMILIES });
  if (result.error) return sendJson(response, 503, workbenchProjectionStoreError(result.error));
  const session = visibleFactSessions(result.facts, actor)[0] ?? null;
  if (!session) {
    const body = workbenchError("workbench_session_not_found", "Workbench session is not visible to the current actor.", { sessionId });
    attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/sessions/:id", code: "workbench_session_not_found", sessionId, count: result.count });
    return sendJson(response, 404, body);
  }
  sendJson(response, 200, {
    ok: true,
    status: "found",
    contractVersion: "workbench-session-detail-v1",
    session: factSessionDetail(session, result.facts, { includeMessages }),
    detailMode: includeMessages ? "full" : "metadata-only",
    messagesIncluded: includeMessages,
    valuesRedacted: true,
    secretMaterialStored: false
  });
}

export async function handleWorkbenchMessagePage(request, response, url, options, actor, sessionId) {
  const startedAt = nowMs();
  const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
  const limit = boundedLimit(url.searchParams.get("limit"));
  const cursorParam = url.searchParams.get("cursor") ?? url.searchParams.get("after");
  const hasCursor = Boolean(textValue(cursorParam));
  const result = await queryFactsByRouteId(readModel, sessionId, {
    families: WORKBENCH_SESSION_MESSAGE_PAGE_FAMILIES,
    ...(hasCursor ? {} : { limit: limit + 1, messagesOrder: "updated_desc", partsOrder: "updated_desc", turnsOrder: "updated_desc", checkpointsOrder: "updated_desc" })
  });
  if (result.error) return sendJson(response, 503, workbenchProjectionStoreError(result.error));
  const session = visibleFactSessions(result.facts, actor)[0] ?? null;
  if (!session) {
    const body = workbenchError("workbench_session_not_found", "Workbench session is not visible to the current actor.", { sessionId });
    attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/sessions/:id/messages", code: "workbench_session_not_found", sessionId, count: result.count });
    return sendJson(response, 404, body);
  }
  const messages = factMessagesForSession(session, result.facts);
  const offset = hasCursor ? cursorOffset(cursorParam) : 0;
  const page = messages.slice(offset, offset + limit);
  const nextOffset = offset + page.length;
  const resolvedSessionId = factSessionId(session);
  const body = {
    ok: true,
    status: "succeeded",
    contractVersion: "workbench-message-page-v1",
    sessionId: resolvedSessionId,
    messages: page,
    count: page.length,
    total: messages.length,
    cursor: offset > 0 ? cursorFromOffset(offset) : null,
    nextCursor: nextOffset < messages.length ? cursorFromOffset(nextOffset) : null,
    hasMore: nextOffset < messages.length,
    timelineDigest: workbenchTimelineDigest(page),
    roleSequencePrefix: messagesRoleSequence(page).slice(0, 32),
    adjacentSameRoleCount: adjacentSameRoleCount(page),
    traceIds: uniqueText(page.map((message) => message?.traceId)).slice(0, 16),
    valuesRedacted: true,
    secretMaterialStored: false
  };
  emitWorkbenchSessionMessagesReadOtel(request, options, {
    startTimeMs: startedAt,
    statusCode: 200,
    sessionId: resolvedSessionId,
    limit,
    offset,
    messages: page,
    total: messages.length,
    hasMore: body.hasMore
  });
  sendJson(response, 200, body);
}

export async function handleWorkbenchTurnSnapshot(request, response, url, options, actor, rawTurnId) {
  const startedAt = nowMs();
  const queryTraceId = safeTraceId(url.searchParams.get("traceId"));
  const traceId = safeTraceId(rawTurnId) ?? queryTraceId;
  const turnId = safeTurnId(rawTurnId) || traceId || rawTurnId;
  if (!traceId) {
    const body = workbenchError("invalid_turn_id", "turnId must currently be a trace id or include traceId query.", { turnId });
    recordWorkbenchTurnReadMetric(options, url, 400, body, startedAt);
    return sendJson(response, 400, body);
  }
  const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
  const result = await readModel.queryFacts({ traceId, limit: MAX_PAGE_LIMIT, families: [...WORKBENCH_TRACE_EVENT_METADATA_FAMILIES, ...WORKBENCH_TRACE_EVENT_PAGE_FAMILIES] });
  if (result.error) {
    const body = workbenchProjectionStoreError(result.error);
    emitWorkbenchTurnStatusReadOtel(traceId, options, {
      startTimeMs: startedAt,
      statusCode: 503,
      phase: "query_failed",
      turnId,
      errorCode: result.error?.code ?? "workbench_turn_query_failed"
    }, result.error);
    recordWorkbenchTurnReadMetric(options, url, 503, body, startedAt);
    return sendJson(response, 503, body);
  }
  const session = visibleFactSessions(result.facts, actor).find((item) => item.lastTraceId === traceId) ?? visibleFactSessions(result.facts, actor)[0] ?? null;
  const turn = factTurnForTrace(result.facts, traceId, turnId);
  const found = Boolean(session || turn);
  if (!found) {
    const body = workbenchError("workbench_turn_not_found", "Workbench turn is not visible to the current actor.", { turnId, traceId });
    attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/turns/:id", code: "workbench_turn_not_found", traceId, turnId, count: result.count });
    emitWorkbenchTurnStatusReadOtel(traceId, options, {
      startTimeMs: startedAt,
      statusCode: 404,
      phase: "not_found",
      turnId,
      readCount: result.count,
      errorCode: "workbench_turn_not_found"
    });
    recordWorkbenchTurnReadMetric(options, url, 404, body, startedAt);
    return sendJson(response, 404, body);
  }
  const projection = factProjectionForTrace(result.facts, traceId);
  const snapshot = factTurnSnapshot({ turn, session, facts: result.facts, traceId, turnId });
  const status = snapshot.status;
  const body = {
    ok: true,
    status,
    contractVersion: "workbench-turn-snapshot-v1",
    turn: snapshot,
    projection,
    cache: result.cache ?? null,
    projectionStatus: projection.projectionStatus,
    projectionHealth: projection.projectionHealth,
    lastProjectedSeq: projection.lastProjectedSeq,
    sourceRunId: projection.sourceRunId,
    sourceCommandId: projection.sourceCommandId,
    staleMs: projection.staleMs,
    blocker: projection.blocker,
    valuesRedacted: true,
    secretMaterialStored: false
  };
  recordWorkbenchProjectionMetric(options, {
    projection: { ...projection, status, eventCount: snapshot.trace?.eventCount ?? projection.lastProjectedSeq ?? 0 },
    diagnostic: projection
  });
  emitWorkbenchTurnStatusReadOtel(traceId, options, {
    startTimeMs: startedAt,
    statusCode: 200,
    phase: "succeeded",
    status,
    terminal: snapshot.terminal,
    sessionId: snapshot.sessionId,
    turnId: snapshot.turnId,
    traceEventCount: snapshot.trace?.eventCount ?? null,
    latestProjectedSeq: projection.lastProjectedSeq,
    projectionStatus: projection.projectionStatus,
    projectionHealth: projection.projectionHealth,
    sourceRunId: projection.sourceRunId,
    sourceCommandId: projection.sourceCommandId,
    readCount: result.count
  });
  recordWorkbenchTurnReadMetric(options, url, 200, body, startedAt);
  recordWorkbenchCacheMetric(options, url, body, startedAt);
  sendJson(response, 200, body);
}

export async function handleWorkbenchTraceEventPage(request, response, url, options, actor, rawTraceId) {
  const startedAt = nowMs();
  const traceId = safeTraceId(rawTraceId);
  if (!traceId) return sendJson(response, 400, workbenchError("invalid_trace_id", "traceId must start with trc_.", { traceId: rawTraceId }));
  const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
  const pageOptions = tracePageOptions(url);
  const metadata = await readModel.queryFacts({ traceId, families: WORKBENCH_TRACE_EVENT_METADATA_FAMILIES, limit: 1 });
  if (metadata.error) {
    emitWorkbenchTraceEventsReadOtel(traceId, options, {
      startTimeMs: startedAt,
      statusCode: 503,
      phase: "metadata_query_failed",
      pageOptions,
      errorCode: metadata.error?.code ?? "workbench_trace_metadata_query_failed"
    }, metadata.error);
    return sendJson(response, 503, workbenchProjectionStoreError(metadata.error));
  }
  const resolved = await visibleFactSessionForTrace(readModel, metadata.facts, actor, traceId);
  if (resolved.error) {
    emitWorkbenchTraceEventsReadOtel(traceId, options, {
      startTimeMs: startedAt,
      statusCode: 503,
      phase: "session_lookup_failed",
      pageOptions,
      metadataCount: metadata.count,
      errorCode: resolved.error?.code ?? "workbench_trace_session_lookup_failed"
    }, resolved.error);
    return sendJson(response, 503, workbenchProjectionStoreError(resolved.error));
  }
  const session = resolved.session;
  const metadataFacts = resolved.facts ?? metadata.facts;
  if (!session) {
    const context = await readModel.queryFacts({ traceId, families: ["sessions", "turns", "checkpoints"], limit: MAX_PAGE_LIMIT });
    if (context.error) {
      emitWorkbenchTraceEventsReadOtel(traceId, options, {
        startTimeMs: startedAt,
        statusCode: 503,
        phase: "context_query_failed",
        pageOptions,
        metadataCount: metadata.count,
        errorCode: context.error?.code ?? "workbench_trace_context_query_failed"
      }, context.error);
      return sendJson(response, 503, workbenchProjectionStoreError(context.error));
    }
    const contextSessions = visibleFactSessions(context.facts, actor);
    const contextSession = contextSessions.find((item) => item.lastTraceId === traceId) ?? contextSessions[0] ?? null;
    const contextTurn = factTurnForTrace(context.facts, traceId, traceId);
    if (contextSession || contextTurn) {
      const projection = factProjectionForTrace(context.facts, traceId);
      const blocker = traceEventsReadModelBlocker("workbench_trace_metadata_missing", "Workbench trace metadata is missing from the trace events read model.", { traceId, projection, session: contextSession, turn: contextTurn, route: url.pathname });
      attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/traces/:id/events", code: "workbench_trace_metadata_missing", sessionId: factSessionId(contextSession), traceId, count: context.count });
      emitWorkbenchTraceEventsReadOtel(traceId, options, {
        startTimeMs: startedAt,
        statusCode: 404,
        phase: "metadata_missing",
        pageOptions,
        projection,
        sessionId: factSessionId(contextSession),
        turnId: factTurnId(contextTurn, traceId),
        readCount: context.count,
        metadataCount: metadata.count,
        errorCode: "workbench_trace_metadata_missing"
      });
      return sendJson(response, 404, workbenchTraceEventsReadModelError(blocker, { traceId, projection, session: contextSession, turn: contextTurn }));
    }
    attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/traces/:id/events", code: "workbench_trace_not_found", traceId, count: context.count });
    emitWorkbenchTraceEventsReadOtel(traceId, options, {
      startTimeMs: startedAt,
      statusCode: 404,
      phase: "not_found",
      pageOptions,
      readCount: context.count,
      metadataCount: metadata.count,
      errorCode: "workbench_trace_not_found"
    });
    return sendJson(response, 404, workbenchError("workbench_trace_not_found", "Workbench trace is not visible to the current actor.", { traceId }));
  }
  const projection = factProjectionForTrace(metadataFacts, traceId);
  const pageResult = await readModel.queryFacts({ traceId, families: WORKBENCH_TRACE_EVENT_PAGE_FAMILIES, afterProjectedSeq: pageOptions.afterProjectedSeq, limit: pageOptions.limit + 1 });
  if (pageResult.error) {
    emitWorkbenchTraceEventsReadOtel(traceId, options, {
      startTimeMs: startedAt,
      statusCode: 503,
      phase: "events_query_failed",
      pageOptions,
      projection,
      sessionId: factSessionId(session),
      metadataCount: metadata.count,
      errorCode: pageResult.error?.code ?? "workbench_trace_events_query_failed"
    }, pageResult.error);
    return sendJson(response, 503, workbenchProjectionStoreError(pageResult.error));
  }
  const traceTurn = factTurnForTrace(metadataFacts, traceId, traceId);
  const turnTraceStatus = normalizeStatus(traceTurn?.status);
  const traceStatus = turnTraceStatus !== "unknown" ? turnTraceStatus : durableTraceStatus(pageResult.facts.traceEvents);
  const page = traceEventPageFromFacts(pageResult.facts.traceEvents, pageOptions, { total: projection.lastProjectedSeq, traceLastSeq: projection.lastProjectedSeq, traceStatus });
  const missingTraceEvents = traceEventPageMissing(page, projection, pageOptions);
  const missingTraceEventsDiagnostic = missingTraceEvents
    ? traceEventsReadModelBlocker("workbench_trace_events_missing", "Workbench trace events are still catching up in the durable read model.", { traceId, projection, session, turn: traceTurn, route: url.pathname })
    : null;
  if (missingTraceEvents) {
    attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/traces/:id/events", code: "workbench_trace_events_missing", sessionId: factSessionId(session), traceId, count: pageResult.count });
  }
  const responseProjection = page.blocker ? blockedTraceEventProjection(projection, page.blocker) : missingTraceEvents ? catchingUpTraceEventProjection(projection, missingTraceEventsDiagnostic) : projection;
  const body = {
    ok: true,
    status: page.blocker ? "blocked" : missingTraceEvents ? "projecting" : "succeeded",
    contractVersion: "workbench-trace-events-v1",
    traceId,
    sessionId: factSessionId(session),
    threadId: safeOpaqueId(session?.threadId) ?? (textValue(session?.threadId) || null),
    ...page,
    events: factArray(page.events).map((event) => ({ ...event, detailProjection: true, authority: "trace-detail-only" })),
    projection: responseProjection,
    detailProjection: true,
    authority: "trace-detail-only",
    realtimeAuthority: "workbench-realtime-authority-v2",
    cache: pageResult.cache ?? metadata.cache ?? null,
    projectionStatus: responseProjection.projectionStatus,
    projectionHealth: responseProjection.projectionHealth,
    lastProjectedSeq: responseProjection.lastProjectedSeq,
    sourceRunId: responseProjection.sourceRunId,
    sourceCommandId: responseProjection.sourceCommandId,
    staleMs: responseProjection.staleMs,
    blocker: responseProjection.blocker,
    timing: responseProjection.timing,
    startedAt: responseProjection.startedAt,
    lastEventAt: responseProjection.lastEventAt,
    finishedAt: responseProjection.finishedAt,
    durationMs: responseProjection.durationMs,
    diagnostic: responseProjection.diagnostic ?? null,
    valuesRedacted: true,
    secretMaterialStored: false
  };
  recordWorkbenchCacheMetric(options, url, body, startedAt);
  emitWorkbenchTraceEventsReadOtel(traceId, options, {
    startTimeMs: startedAt,
    statusCode: 200,
    phase: page.blocker ? "blocked" : missingTraceEvents ? "events_catching_up" : "succeeded",
    pageOptions,
    page,
    projection: responseProjection,
    sessionId: body.sessionId,
    turnId: factTurnId(traceTurn, traceId),
    readCount: pageResult.count,
    rawEventCount: factArray(pageResult.facts?.traceEvents).length,
    metadataCount: metadata.count,
    traceStatus
  });
  sendJson(response, 200, body);
}

export function emitWorkbenchTurnStatusReadOtel(traceId, options = {}, fields = {}, error = null) {
  const safeId = safeTraceId(traceId);
  if (!safeId) return;
  const statusCode = Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : 0;
  void emitCodeAgentOtelSpan("turn_status_read", safeId, options.env ?? process.env, {
    startTimeMs: fields.startTimeMs,
    status: statusCode >= 500 || error ? "error" : "ok",
    error,
    attributes: {
      "http.method": "GET",
      "http.route": "/v1/workbench/turns/:id",
      "http.status_code": statusCode || null,
      phase: fields.phase ?? null,
      status: fields.status ?? null,
      terminal: typeof fields.terminal === "boolean" ? fields.terminal : null,
      sessionId: fields.sessionId ?? null,
      turnId: fields.turnId ?? safeId,
      readCount: nonNegativeInteger(fields.readCount),
      traceEventCount: nonNegativeInteger(fields.traceEventCount),
      latestProjectedSeq: nonNegativeInteger(fields.latestProjectedSeq),
      projectionStatus: fields.projectionStatus ?? null,
      projectionHealth: fields.projectionHealth ?? null,
      sourceRunId: fields.sourceRunId ?? null,
      sourceCommandId: fields.sourceCommandId ?? null,
      errorCode: fields.errorCode ?? null,
      valuesRedacted: true
    }
  });
}

export function emitWorkbenchTraceEventsReadOtel(traceId, options = {}, fields = {}, error = null) {
  const safeId = safeTraceId(traceId);
  if (!safeId) return;
  const page = fields.page ?? {};
  const range = page.range ?? {};
  const projection = fields.projection ?? {};
  const pageOptions = fields.pageOptions ?? {};
  const statusCode = Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : 0;
  const returnedEvents = Array.isArray(page.events) ? page.events.length : nonNegativeInteger(fields.returnedEvents);
  const afterProjectedSeq = nonNegativeInteger(pageOptions.afterProjectedSeq ?? range.afterProjectedSeq);
  const limit = nonNegativeInteger(pageOptions.limit ?? range.limit);
  const hasMore = typeof page.hasMore === "boolean" ? page.hasMore : null;
  void emitCodeAgentOtelSpan("trace_events_read", safeId, options.env ?? process.env, {
    startTimeMs: fields.startTimeMs,
    status: statusCode >= 500 || error ? "error" : "ok",
    error,
    attributes: {
      "http.method": "GET",
      "http.route": "/v1/workbench/traces/:id/events",
      "http.status_code": statusCode || null,
      phase: fields.phase ?? null,
      sessionId: fields.sessionId ?? null,
      turnId: fields.turnId ?? safeId,
      returnedEvents,
      rawEventCount: nonNegativeInteger(fields.rawEventCount),
      readCount: nonNegativeInteger(fields.readCount),
      metadataCount: nonNegativeInteger(fields.metadataCount),
      sinceSeq: afterProjectedSeq,
      afterProjectedSeq,
      limit,
      fromSeq: nonNegativeInteger(range.fromProjectedSeq),
      toSeq: nonNegativeInteger(range.toProjectedSeq),
      totalEvents: nonNegativeInteger(range.total ?? page.eventCount),
      hasMore,
      fullTraceLoaded: typeof page.fullTraceLoaded === "boolean" ? page.fullTraceLoaded : hasMore === null ? null : !hasMore && !page.blocker,
      traceLastSeq: nonNegativeInteger(page.traceLastSeq ?? projection.lastProjectedSeq),
      projectionStatus: projection.projectionStatus ?? null,
      projectionHealth: projection.projectionHealth ?? null,
      sourceRunId: projection.sourceRunId ?? null,
      sourceCommandId: projection.sourceCommandId ?? null,
      traceStatus: fields.traceStatus ?? page.traceStatus ?? null,
      errorCode: fields.errorCode ?? null,
      valuesRedacted: true
    }
  });
}

export function emitWorkbenchSessionListReadOtel(request, options = {}, fields = {}, error = null) {
  const payload = fields.payload && typeof fields.payload === "object" ? fields.payload : {};
  const sessions = Array.isArray(payload.sessions) ? payload.sessions : [];
  const fallbackTitleCount = sessions.filter(sessionSummaryNeedsTitleFallback).length;
  const traceIds = uniqueText(sessions.map((session) => session?.lastTraceId)).slice(0, 8);
  const businessTraceId = (safeTraceId(traceIds[0]) ?? textValue(fields.includeSessionId)) || "workbench_session_list";
  void emitCodeAgentOtelSpan("session_list_read", businessTraceId, options.env ?? process.env, {
    startTimeMs: fields.startTimeMs,
    status: error ? "error" : "ok",
    error,
    attributes: {
      "http.method": "GET",
      "http.route": "/v1/workbench/sessions",
      "http.status_code": Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : null,
      source: fields.source ?? null,
      limit: nonNegativeInteger(fields.limit),
      offset: nonNegativeInteger(fields.offset),
      includeSessionId: fields.includeSessionId ?? null,
      sessionCount: sessions.length,
      fallbackTitleCount,
      fallbackTitleRatio: sessions.length > 0 ? Number((fallbackTitleCount / sessions.length).toFixed(4)) : 0,
      emptyPreviewCount: sessions.filter((session) => !sessionPreviewText(session)).length,
      sessionIds: uniqueText(sessions.map((session) => session?.sessionId)).slice(0, 8).join(","),
      traceIds: traceIds.join(","),
      hasMore: payload.hasMore === true,
      valuesRedacted: true
    }
  });
}

export function emitWorkbenchSessionMessagesReadOtel(request, options = {}, fields = {}, error = null) {
  const messages = Array.isArray(fields.messages) ? fields.messages : [];
  const traceIds = uniqueText(messages.map((message) => message?.traceId)).slice(0, 8);
  const businessTraceId = (safeTraceId(traceIds[0]) ?? textValue(fields.sessionId)) || "workbench_session_messages";
  const roleSequence = messagesRoleSequence(messages);
  void emitCodeAgentOtelSpan("session_messages_read", businessTraceId, options.env ?? process.env, {
    startTimeMs: fields.startTimeMs,
    status: error ? "error" : "ok",
    error,
    attributes: {
      "http.method": "GET",
      "http.route": "/v1/workbench/sessions/:id/messages",
      "http.status_code": Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : null,
      sessionId: fields.sessionId ?? null,
      limit: nonNegativeInteger(fields.limit),
      offset: nonNegativeInteger(fields.offset),
      returnedMessages: messages.length,
      totalMessages: nonNegativeInteger(fields.total),
      hasMore: fields.hasMore === true,
      roleSequencePrefix: roleSequence.slice(0, 32),
      consecutiveUserPrefix: consecutiveRolePrefix(messages, "user"),
      adjacentSameRoleCount: adjacentSameRoleCount(messages),
      timelineDigest: workbenchTimelineDigest(messages),
      userCount: messages.filter((message) => normalizeRole(message?.role) === "user").length,
      agentCount: messages.filter((message) => isAssistantLikeRole(message?.role)).length,
      traceIds: traceIds.join(","),
      valuesRedacted: true
    }
  });
}

export function messagesRoleSequence(messages = []) {
  return messages.map((message) => roleSequenceSymbol(message?.role)).join("");
}

export function workbenchTimelineDigest(messages = []) {
  const payload = messages.map((message) => {
    const terminal = isTerminalProjectionStatus(normalizeStatus(message?.status)) || Boolean(message?.finishedAt);
    return {
      messageId: message?.messageId ?? null,
      role: normalizeRole(message?.role),
      sessionId: message?.sessionId ?? null,
      traceId: message?.traceId ?? null,
      turnId: message?.turnId ?? null,
      status: normalizeStatus(message?.status),
      partIds: factArray(message?.parts).map((part) => part?.partId ?? null),
      textHash: textValue(message?.text || message?.textPreview) ? hash(textValue(message?.text || message?.textPreview)).slice(0, 16) : null,
      startedAt: message?.startedAt ?? null,
      finishedAt: terminal ? message?.finishedAt ?? null : null,
      durationMs: terminal && Number.isFinite(Number(message?.durationMs)) ? Math.trunc(Number(message.durationMs)) : null
    };
  });
  return `sha256:${hash(JSON.stringify(payload))}`;
}

export function sessionSummaryNeedsTitleFallback(session) {
  if (!session || typeof session !== "object") return true;
  return !sessionPreviewText(session);
}

export function sessionPreviewText(session) {
  return textValue(
    session?.firstUserMessagePreview
    ?? session?.snapshot?.firstUserMessagePreview
    ?? session?.userPreview
    ?? session?.title
    ?? session?.name
  );
}

export function roleSequenceSymbol(role) {
  const normalized = normalizeRole(role);
  if (normalized === "user") return "U";
  if (normalized === "agent" || normalized === "assistant") return "A";
  if (normalized === "system") return "S";
  return "?";
}

export function normalizeRole(role) {
  return textValue(role).toLowerCase();
}

export function consecutiveRolePrefix(messages = [], role) {
  let count = 0;
  for (const message of messages) {
    if (normalizeRole(message?.role) !== role) break;
    count += 1;
  }
  return count;
}

export function adjacentSameRoleCount(messages = []) {
  let count = 0;
  for (let index = 1; index < messages.length; index += 1) {
    const previous = normalizeRole(messages[index - 1]?.role);
    if (previous && previous === normalizeRole(messages[index]?.role)) count += 1;
  }
  return count;
}

export function traceEventPageFromFacts(sourceEvents, options, metadata = {}) {
  const rows = factArray(sourceEvents).filter((event) => factProjectedSeq(event)).sort(compareFactTraceEventsAsc);
  const collision = projectedSeqCollision(rows);
  if (collision) return blockedTraceEventPage(options, metadata, collision);
  const pageRows = rows.slice(0, options.limit);
  const events = pageRows.map(factTraceEventDto).filter(Boolean);
  const toProjectedSeq = events.length ? events.at(-1).projectedSeq : null;
  const hasMore = rows.length > options.limit;
  const total = Number.isFinite(Number(metadata.total)) ? Math.trunc(Number(metadata.total)) : Math.max(options.afterProjectedSeq, toProjectedSeq ?? options.afterProjectedSeq);
  const traceLastSeq = Number.isFinite(Number(metadata.traceLastSeq)) ? Math.trunc(Number(metadata.traceLastSeq)) : total;
  return {
    events,
    eventCount: total ?? Math.max(options.afterProjectedSeq, toProjectedSeq ?? options.afterProjectedSeq),
    range: {
      afterProjectedSeq: options.afterProjectedSeq,
      fromProjectedSeq: events.length ? events[0].projectedSeq : null,
      toProjectedSeq,
      limit: options.limit,
      returned: events.length,
      total
    },
    hasMore,
    fullTraceLoaded: !hasMore && (toProjectedSeq ?? options.afterProjectedSeq) >= traceLastSeq,
    traceLastSeq,
    nextProjectedSeq: events.length ? toProjectedSeq : options.afterProjectedSeq,
    nextCursor: hasMore && toProjectedSeq ? `projected:${toProjectedSeq}` : null,
    traceStatus: metadata.traceStatus ?? "unknown",
    updatedAt: events.at(-1)?.updatedAt ?? events.at(-1)?.createdAt ?? null
  };
}

export function projectedSeqCollision(rows = []) {
  const seen = new Map();
  for (const row of rows) {
    const projectedSeq = factProjectedSeq(row);
    if (!projectedSeq) continue;
    const id = textValue(row?.id ?? row?.sourceEventId) || null;
    const existing = seen.get(projectedSeq);
    if (existing && existing.id !== id) {
      return {
        code: "projected_seq_collision",
        category: "workbench-projection",
        layer: "workbench-trace-events",
        message: "Trace projection contains duplicate projectedSeq values; canonical event order is blocked until projection is rebuilt.",
        projectedSeq,
        eventIds: [existing.id, id].filter(Boolean),
        retryable: false,
        valuesPrinted: false
      };
    }
    if (!existing) seen.set(projectedSeq, { id });
  }
  return null;
}

export function blockedTraceEventPage(options, metadata = {}, blocker) {
  const total = Number.isFinite(Number(metadata.total)) ? Math.trunc(Number(metadata.total)) : null;
  return {
    events: [],
    eventCount: total ?? options.afterProjectedSeq,
    range: {
      afterProjectedSeq: options.afterProjectedSeq,
      fromProjectedSeq: null,
      toProjectedSeq: null,
      limit: options.limit,
      returned: 0,
      total
    },
    hasMore: false,
    nextProjectedSeq: options.afterProjectedSeq,
    nextCursor: null,
    traceStatus: metadata.traceStatus ?? "unknown",
    updatedAt: null,
    blocker
  };
}

export function blockedTraceEventProjection(projection = {}, blocker) {
  return {
    ...projection,
    projectionStatus: "blocked",
    projectionHealth: "degraded",
    blocker,
    valuesRedacted: true
  };
}

export function catchingUpTraceEventProjection(projection = {}, diagnostic = null) {
  const status = projection.projectionStatus === "caught-up" || !projection.projectionStatus ? "projecting" : projection.projectionStatus;
  const health = projection.projectionHealth === "caught-up" || projection.projectionHealth === "healthy" || !projection.projectionHealth ? "projecting" : projection.projectionHealth;
  return {
    ...projection,
    projectionStatus: status,
    projectionHealth: health,
    blocker: null,
    diagnostic: diagnostic ? { ...diagnostic, projectionStatus: status, projectionHealth: health } : null,
    valuesRedacted: true
  };
}

export function traceEventPageMissing(page = {}, projection = {}, options = {}) {
  if (page.blocker) return false;
  const expectedSeq = Number(projection?.lastProjectedSeq);
  if (!Number.isFinite(expectedSeq) || expectedSeq <= 0) return false;
  return factArray(page.events).length === 0 && expectedSeq > Number(options.afterProjectedSeq ?? 0);
}

export function traceEventsReadModelBlocker(code, message, { traceId, projection = {}, session = null, turn = null, route = null } = {}) {
  return {
    code,
    message,
    userMessage: message,
    layer: "workbench-read-model",
    category: "trace-events",
    retryable: true,
    route,
    traceId,
    sessionId: factSessionId(session) ?? turn?.sessionId ?? null,
    turnId: factTurnId(turn, traceId),
    projectionStatus: projection.projectionStatus ?? null,
    projectionHealth: "degraded",
    lastProjectedSeq: projection.lastProjectedSeq ?? null,
    sourceRunId: projection.sourceRunId ?? null,
    sourceCommandId: projection.sourceCommandId ?? null,
    retryAfterMs: 5000,
    valuesPrinted: false
  };
}

export function workbenchTraceEventsReadModelError(blocker, { traceId, projection = {}, session = null, turn = null } = {}) {
  const responseProjection = blockedTraceEventProjection(projection, blocker);
  return workbenchError(blocker.code, blocker.message, {
    traceId,
    sessionId: blocker.sessionId ?? factSessionId(session) ?? turn?.sessionId ?? null,
    turnId: blocker.turnId ?? factTurnId(turn, traceId),
    route: blocker.route ?? null,
    layer: blocker.layer,
    category: blocker.category,
    retryable: blocker.retryable,
    projection: responseProjection,
    projectionStatus: responseProjection.projectionStatus,
    projectionHealth: responseProjection.projectionHealth,
    lastProjectedSeq: responseProjection.lastProjectedSeq,
    sourceRunId: responseProjection.sourceRunId,
    sourceCommandId: responseProjection.sourceCommandId,
    blocker,
    diagnostic: {
      contractVersion: "hwlab-error-diagnostic-v1",
      route: blocker.route ?? null,
      layer: blocker.layer,
      category: blocker.category,
      code: blocker.code,
      httpStatus: 404,
      source: "server",
      retryable: blocker.retryable,
      valuesPrinted: false
    }
  });
}

export function projectionText(...values) {
  for (const value of values) {
    if (value && typeof value === "object") {
      const nested = messageAuthorityTextValue(value.text ?? value.content ?? value.message ?? value.summary ?? value.preview ?? value.title);
      if (nested) return nested;
      continue;
    }
    const text = messageAuthorityTextValue(value);
    if (text) return text;
  }
  return null;
}

export function partFact(part, index, messageId, traceId) {
  const type = textValue(part?.type) || "text";
  const text = messageAuthorityTextValue(part?.text ?? part?.content ?? part?.message);
  return {
    partId: safePartId(part?.partId ?? part?.id) || `prt_${hash(`${messageId}:${index}:${type}`).slice(0, 24)}`,
    messageId,
    traceId,
    type,
    text: text || null,
    status: normalizeStatus(part?.status ?? "completed"),
    toolName: textValue(part?.toolName ?? part?.name) || null,
    createdAt: textValue(part?.createdAt ?? part?.timestamp) || null,
    valuesRedacted: part?.valuesRedacted !== false
  };
}

export function tracePageOptions(url) {
  const cursor = textValue(url.searchParams.get("cursor"));
  const cursorProjectedSeq = cursor.startsWith("projected:") ? Number.parseInt(cursor.slice("projected:".length), 10) : NaN;
  const afterProjectedSeq = Number.isInteger(cursorProjectedSeq) && cursorProjectedSeq >= 0
    ? cursorProjectedSeq
    : nonNegativeInteger(url.searchParams.get("afterProjectedSeq"), 0);
  return { afterProjectedSeq, limit: boundedLimit(url.searchParams.get("limit")) };
}


export function handleWorkbenchReadModelFailure(response, url, options = {}, error) {
  const classified = classifyWorkbenchReadModelFailure(error);
  const route = workbenchReadRouteTemplate(url?.pathname ?? "/v1/workbench");
  logWorkbenchReadModelFailure(options.logger ?? console, {
    event: "workbench_read_model_route_failed",
    ok: false,
    route,
    statusCode: classified.statusCode,
    errorCode: classified.code,
    errorName: error?.name ?? "Error",
    message: error instanceof Error ? error.message : String(error ?? "unknown"),
    valuesRedacted: true
  });
  if (response.headersSent || response.destroyed) return;
  sendJson(response, classified.statusCode, workbenchError(classified.code, classified.message, {
    route,
    errorName: error?.name ?? "Error",
    retryable: classified.retryable === true,
    transient: classified.transient === true,
    serviceId: classified.serviceId ?? null,
    runtimeStatus: classified.runtimeStatus ?? null
  }));
}

export function classifyWorkbenchReadModelFailure(error) {
  const message = String(error?.message ?? error ?? "");
  const code = String(error?.code ?? "").toLowerCase();
  const runtimeStatus = Number(error?.data?.status ?? 0);
  const runtimePayloadError = error?.data?.payload?.error ?? null;
  if (error?.name === "WorkbenchRuntimeDependencyError" || code.startsWith("workbench_runtime_")) {
    const retryable = error?.data?.retryable === true || error?.data?.transient === true || runtimePayloadError?.retryable === true;
    return {
      statusCode: retryable || (Number.isFinite(runtimeStatus) && runtimeStatus >= 500) ? 503 : 500,
      code: String(runtimePayloadError?.code ?? error?.code ?? "workbench_runtime_dependency_failed"),
      message: String(runtimePayloadError?.message ?? error?.message ?? "Workbench runtime dependency failed."),
      retryable,
      transient: retryable,
      serviceId: String(error?.data?.serviceId ?? "hwlab-workbench-runtime"),
      runtimeStatus: Number.isFinite(runtimeStatus) && runtimeStatus > 0 ? runtimeStatus : null
    };
  }
  if (/timeout|connection terminated|terminating connection|econnreset|econnrefused|etimedout/iu.test(message) || /timeout|econnreset|econnrefused|etimedout/u.test(code)) {
    return {
      statusCode: 503,
      code: "workbench_read_model_store_unavailable",
      message: "Workbench read model store is temporarily unavailable."
    };
  }
  return {
    statusCode: 500,
    code: "workbench_read_model_failed",
    message: "Workbench read model failed."
  };
}

export function workbenchReadRouteTemplate(pathname) {
  const path = String(pathname ?? "");
  if (path === "/v1/workbench/sessions") return path;
  if (/^\/v1\/workbench\/sessions\/[^/]+\/messages$/u.test(path)) return "/v1/workbench/sessions/:id/messages";
  if (/^\/v1\/workbench\/sessions\/[^/]+$/u.test(path)) return "/v1/workbench/sessions/:id";
  if (/^\/v1\/workbench\/turns\/[^/]+$/u.test(path)) return "/v1/workbench/turns/:id";
  if (/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(path)) return "/v1/workbench/traces/:id/events";
  return path || "/v1/workbench";
}

export function logWorkbenchReadModelFailure(logger, payload) {
  try {
    const line = JSON.stringify(payload);
    if (typeof logger?.error === "function") logger.error(line);
    else if (typeof logger?.warn === "function") logger.warn(line);
  } catch {
    // Read-model failure logging must not affect response generation.
  }
}

export function methodNotAllowed(response, allowed) {
  sendJson(response, 405, workbenchError("method_not_allowed", `Use ${allowed} for this Workbench read model route.`));
}

export function workbenchError(code, message, extra = {}) {
  return {
    ok: false,
    status: "failed",
    error: { code, message, ...extra },
    valuesRedacted: true,
    secretMaterialStored: false
  };
}

export function boundedLimit(value) {
  return Math.min(MAX_PAGE_LIMIT, parsePositiveInteger(value, DEFAULT_PAGE_LIMIT));
}

export function boundedSessionListLimit(value) {
  return Math.min(MAX_PAGE_LIMIT, parsePositiveInteger(value, DEFAULT_SESSION_LIST_LIMIT));
}

export function cursorOffset(value) {
  const text = textValue(value);
  if (!text) return 0;
  if (text.startsWith("idx:")) return nonNegativeInteger(text.slice(4), 0);
  return nonNegativeInteger(text, 0);
}

export function cursorFromOffset(offset) {
  return `idx:${Math.max(0, Math.trunc(Number(offset) || 0))}`;
}

export function nonNegativeInteger(value, fallback) {
  const parsed = Number.parseInt(value ?? "", 10);
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
}


export function nowMs() {
  if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
  return Date.now();
}
