/* * 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(); }