/* * 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 { handleWorkbenchSyncHttp, workbenchRealtimeEntityDelta } from "./workbench-realtime-authority.ts"; import { durableTraceStatus, RUNNING_STATUSES, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts"; import { emitCodeAgentOtelSpan, emitHttpServerRequestSpan } from "./otel-trace.ts"; const DEFAULT_PAGE_LIMIT = 50; const DEFAULT_SESSION_LIST_LIMIT = 20; const MAX_PAGE_LIMIT = 100; const DEFAULT_WORKBENCH_SSE_HEARTBEAT_MS = 15000; const WORKBENCH_REALTIME_DRAIN_TIMEOUT_MS = 2500; 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"]); const activeWorkbenchRealtimeConnections = new Set(); export async function drainWorkbenchRealtimeConnections(options = {}) { const reason = textValue(options.reason) || "server_shutdown"; const signal = textValue(options.signal) || null; const timeoutMs = parsePositiveInteger(options.timeoutMs, WORKBENCH_REALTIME_DRAIN_TIMEOUT_MS); const connections = [...activeWorkbenchRealtimeConnections].filter((connection) => connection?.isActive?.()); for (const connection of connections) { try { connection.close({ reason, signal }); } catch { // Best effort: one broken client must not block shutdown drain for the rest. } } const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const activeAfterClose = connections.filter((connection) => activeWorkbenchRealtimeConnections.has(connection) && connection?.isActive?.()).length; if (activeAfterClose === 0) break; await sleepMs(Math.min(50, Math.max(1, deadline - Date.now()))); } const activeAfter = connections.filter((connection) => activeWorkbenchRealtimeConnections.has(connection) && connection?.isActive?.()).length; return { ok: activeAfter === 0, activeBefore: connections.length, closed: Math.max(0, connections.length - activeAfter), activeAfter, timeout: activeAfter > 0, reason, signal }; } 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/sync") { await (perf ? perf.measure("workbench_sync", () => handleWorkbenchSyncHttp(request, response, url, options, auth.actor)) : handleWorkbenchSyncHttp(request, response, url, options, auth.actor)); 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 async function handleWorkbenchRealtimeHttp(request, response, url, options = {}) { try { if (request.method !== "GET") return methodNotAllowed(response, "GET"); const requestedSessionId = safeSessionId(url.searchParams.get("sessionId") ?? url.searchParams.get("includeSessionId")); const requestedTraceId = safeTraceId(url.searchParams.get("traceId")); const heartbeatMs = parsePositiveInteger(options.env?.HWLAB_WORKBENCH_SSE_HEARTBEAT_MS, DEFAULT_WORKBENCH_SSE_HEARTBEAT_MS); const realtimeOutboxReader = workbenchRuntimeOutboxReader(request, options); attachWorkbenchRealtimeOtelContext(request, { sessionId: requestedSessionId, traceId: requestedTraceId, heartbeatMs, outboxMode: typeof realtimeOutboxReader?.readWorkbenchProjectionOutbox === "function" }); 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.searchParams.has("projectId") || url.searchParams.has("workspaceId")) { sendJson(response, 400, workbenchError("workbench_authority_removed", "Workbench read model is keyed by sessionId/threadId/traceId only.")); return; } const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const readModel = createWorkbenchReadModel(options, auth.actor); const requestedAfterSeq = workbenchRealtimeAfterSeq(request, url); let closed = false; let realtimeCloseReason = "client_close"; let realtimeCloseSignal = null; let realtimeSessionId = requestedSessionId ?? null; let realtimeTraceId = requestedTraceId ?? null; let realtimeThreadId = null; const realtimeStartedAtMs = Date.now(); const cleanup = []; response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store, no-transform", connection: "keep-alive", "x-accel-buffering": "no", "x-content-type-options": "nosniff" }); if (typeof response.flushHeaders === "function") response.flushHeaders(); const writeEvent = (name, payload = {}) => { if (closed || response.destroyed) return; const startedAt = nowMs(); const eventCreatedAt = realtimeEventCreatedAt(payload); const traceSeq = realtimeTraceSeq(payload); const eventId = realtimeEventId(payload); response.write(`event: ${name}\n`); if (eventId) response.write(`id: ${eventId}\n`); response.write(`data: ${JSON.stringify({ contractVersion: "workbench-events-v1", serverSentAt: new Date().toISOString(), eventCreatedAt, traceSeq, ...payload })}\n\n`); perf?.recordPhase({ phase: "sse_write", durationMs: nowMs() - startedAt, outcome: "ok" }); }; const realtimeConnection = { close(fields = {}) { if (closed || response.destroyed || response.writableEnded) return false; const reason = textValue(fields.reason) || "server_shutdown"; realtimeCloseReason = reason; realtimeCloseSignal = textValue(fields.signal) || null; try { writeEvent("workbench.server_draining", { type: "server.draining", status: "closing", reason, signal: realtimeCloseSignal, sessionId: realtimeSessionId, threadId: realtimeThreadId, traceId: realtimeTraceId, observedAt: new Date().toISOString() }); response.end(); return true; } catch { try { response.destroy(); } catch {} return false; } }, isActive() { return !closed && !response.destroyed && !response.writableEnded; } }; activeWorkbenchRealtimeConnections.add(realtimeConnection); cleanup.push(() => activeWorkbenchRealtimeConnections.delete(realtimeConnection)); writeEvent("workbench.connected", { type: "connected", status: "connected", heartbeatMs, cursor: { outboxSeq: requestedAfterSeq }, filters: { sessionId: requestedSessionId, traceId: requestedTraceId } }); const resolveInitialSession = requestedSessionId && requestedAfterSeq <= 0; const initialContext = resolveInitialSession ? await realtimeInitialSessionContext(readModel, auth.actor, requestedSessionId) : { facts: emptyFactSet(), session: null, blocker: null }; if (initialContext.blocker) { writeEvent("workbench.error", { type: "error", reason: "initial-session", sessionId: requestedSessionId, traceId: requestedTraceId, error: initialContext.blocker }); } const initialFactSession = initialContext.session; const streamSessionId = factSessionId(initialFactSession) ?? requestedSessionId ?? null; const streamThreadId = safeOpaqueId(initialFactSession?.threadId) ?? (textValue(initialFactSession?.threadId) || null); const activeTraceId = requestedTraceId ?? factLastTraceId(initialFactSession) ?? factLatestTraceIdForSession(initialContext.facts, streamSessionId) ?? null; realtimeSessionId = streamSessionId ?? requestedSessionId ?? null; realtimeTraceId = activeTraceId ?? requestedTraceId ?? null; realtimeThreadId = streamThreadId ?? null; attachWorkbenchRealtimeOtelContext(request, { sessionId: streamSessionId ?? requestedSessionId, traceId: activeTraceId, threadId: streamThreadId, heartbeatMs, outboxMode: typeof realtimeOutboxReader?.readWorkbenchProjectionOutbox === "function" }); emitWorkbenchRealtimeAcceptedOtelSpan(request, options.env); if (activeTraceId && requestedAfterSeq <= 0) { await (perf ? perf.measure("workbench_initial_trace", () => writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" })) : writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" })); } const outboxPollMs = parsePositiveInteger(options.env?.HWLAB_WORKBENCH_SSE_OUTBOX_POLL_MS, 500); let outboxCursor = requestedAfterSeq; const outboxTraceSnapshots = new Set(activeTraceId ? [activeTraceId] : []); if (typeof realtimeOutboxReader?.readWorkbenchProjectionOutbox === "function" && (streamSessionId || activeTraceId)) { const pollOutbox = async () => { if (closed || response.destroyed) return; try { const query = { afterSeq: outboxCursor, limit: 50 }; if (streamSessionId) query.sessionId = streamSessionId; else query.traceId = activeTraceId; const rows = await realtimeOutboxReader.readWorkbenchProjectionOutbox(query); for (const row of rows) { if (closed || response.destroyed) break; outboxCursor = row.outboxSeq; const rowTraceId = safeTraceId(row.traceId) ?? activeTraceId; const rowSessionId = safeSessionId(row.sessionId ?? streamSessionId) ?? streamSessionId; if (requestedAfterSeq <= 0 && rowTraceId && !outboxTraceSnapshots.has(rowTraceId)) { outboxTraceSnapshots.add(rowTraceId); await writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: rowTraceId, reason: "outbox-session" }); } if (row.commitType === "message") { await writeMessageRealtimeSnapshotSafe({ writeEvent, readModel, actor: auth.actor, sessionId: rowSessionId, threadId: streamThreadId, traceId: rowTraceId, reason: "outbox-message", cursor: { traceSeq: row.projectedSeq, outboxSeq: row.outboxSeq }, entity: workbenchRealtimeEntityDelta(row) }); continue; } if (row.commitType === "terminal") { writeEvent("workbench.trace.snapshot", { type: "trace.snapshot", sessionId: rowSessionId, threadId: streamThreadId, traceId: rowTraceId, reason: "outbox-terminal", cursor: { traceSeq: row.projectedSeq, outboxSeq: row.outboxSeq }, entity: workbenchRealtimeEntityDelta(row), projectionStatus: { terminal: row.terminal, sealed: row.sealed } }); await writeMessageRealtimeSnapshotSafe({ writeEvent, readModel, actor: auth.actor, sessionId: rowSessionId, threadId: streamThreadId, traceId: rowTraceId, reason: "outbox-terminal", cursor: { traceSeq: row.projectedSeq, outboxSeq: row.outboxSeq }, entity: workbenchRealtimeEntityDelta(row) }); if (rowTraceId) void writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: rowTraceId, reason: "terminal" }); } else { writeEvent("workbench.trace.event", { type: "trace.event", sessionId: rowSessionId, threadId: streamThreadId, traceId: rowTraceId, cursor: { traceSeq: row.projectedSeq, outboxSeq: row.outboxSeq }, entity: workbenchRealtimeEntityDelta(row) }); } } } catch (outboxError) { writeEvent("workbench.error", { type: "error", error: { code: "workbench_outbox_poll_failed", message: outboxError?.message ?? "Workbench outbox poll failed." } }); } }; await pollOutbox(); const outboxTimer = setInterval(() => { void pollOutbox(); }, Math.max(500, outboxPollMs)); cleanup.push(() => clearInterval(outboxTimer)); } else if (activeTraceId) { const unsubscribe = traceStore.subscribe(activeTraceId, (event, snapshot) => { if (event) { writeEvent("workbench.trace.event", { type: "trace.event", sessionId: streamSessionId, threadId: streamThreadId, traceId: activeTraceId, event, snapshot: { ...traceSnapshotSummary(snapshot), sessionId: streamSessionId, threadId: streamThreadId }, cursor: { traceSeq: eventSeq(event, Number(snapshot?.eventCount ?? 1) - 1) } }); } else { writeEvent("workbench.trace.snapshot", { type: "trace.snapshot", sessionId: streamSessionId, threadId: streamThreadId, traceId: activeTraceId, reason: "trace-store-update", snapshot: { ...traceSnapshotForRealtime(snapshot), sessionId: streamSessionId, threadId: streamThreadId }, cursor: { traceSeq: traceSnapshotLastSeq(snapshot) } }); } if (event?.terminal === true || TERMINAL_STATUSES.has(normalizeStatus(event?.status))) { void writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "terminal" }); } }); cleanup.push(unsubscribe); } const heartbeat = setInterval(async () => { try { writeEvent("workbench.heartbeat", { type: "heartbeat", sessionId: requestedSessionId ?? null, traceId: activeTraceId ?? null, observedAt: new Date().toISOString() }); } catch (error) { writeEvent("workbench.error", { type: "error", error: { code: "workbench_realtime_heartbeat_failed", message: error?.message ?? "Workbench realtime heartbeat failed." } }); } }, Math.max(1000, heartbeatMs)); cleanup.push(() => clearInterval(heartbeat)); response.on("close", () => { if (closed) return; closed = true; emitWorkbenchRealtimeClosedOtelSpan(request, options.env, { reason: realtimeCloseReason, signal: realtimeCloseSignal, startedAtMs: realtimeStartedAtMs, sessionId: realtimeSessionId, threadId: realtimeThreadId, traceId: realtimeTraceId, activeConnectionCount: activeWorkbenchRealtimeConnections.size }); for (const item of cleanup.splice(0)) item(); }); } catch (error) { handleWorkbenchRealtimeFailure(request, response, url, options, error); } } function attachWorkbenchRealtimeOtelContext(request, fields = {}) { const context = request?.hwlabHttpRequestContext; if (!context) return; context.route = "/v1/workbench/events"; context.otelAttributes = { ...(context.otelAttributes ?? {}), "workbench.route": "events", "workbench.session_id": fields.sessionId ?? null, traceId: fields.traceId ?? null, "workbench.trace_id": fields.traceId ?? null, "workbench.thread_id": fields.threadId ?? null, "workbench.sse.heartbeat_ms": fields.heartbeatMs ?? null, "workbench.sse.outbox_mode": fields.outboxMode === true }; } 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" }; } function emitWorkbenchRealtimeAcceptedOtelSpan(request, env = process.env) { const context = request?.hwlabHttpRequestContext; if (!context?.traceId) return; const endedAtMs = Date.now(); void emitHttpServerRequestSpan(context, env, { startTimeMs: context.startedAtMs, endTimeMs: endedAtMs, statusCode: 200, method: "GET", route: "/v1/workbench/events", attributes: { ...(context.otelAttributes ?? {}), "http.closed_by": "sse-accepted", "workbench.sse.lifecycle": "accepted", "workbench.sse.accept_latency_ms": Math.max(0, endedAtMs - Number(context.startedAtMs ?? endedAtMs)) } }).catch(() => undefined); } function emitWorkbenchRealtimeClosedOtelSpan(request, env = process.env, fields = {}) { const context = request?.hwlabHttpRequestContext; if (!context?.traceId) return; const endedAtMs = Date.now(); const startedAtMs = Number(fields.startedAtMs ?? context.startedAtMs ?? endedAtMs); const reason = textValue(fields.reason) || "client_close"; void emitHttpServerRequestSpan(context, env, { startTimeMs: Number.isFinite(startedAtMs) ? startedAtMs : endedAtMs, endTimeMs: endedAtMs, statusCode: 200, method: "GET", route: "/v1/workbench/events", attributes: { ...(context.otelAttributes ?? {}), "http.closed_by": reason, "workbench.sse.lifecycle": "closed", "workbench.sse.close_reason": reason, "workbench.sse.close_signal": fields.signal ?? null, "workbench.sse.duration_ms": Math.max(0, endedAtMs - (Number.isFinite(startedAtMs) ? startedAtMs : endedAtMs)), "workbench.sse.session_id": fields.sessionId ?? null, "workbench.sse.thread_id": fields.threadId ?? null, "workbench.sse.trace_id": fields.traceId ?? null, "workbench.sse.active_connection_count": Number.isFinite(Number(fields.activeConnectionCount)) ? Number(fields.activeConnectionCount) : null } }).catch(() => undefined); } function handleWorkbenchRealtimeFailure(request, response, url, options = {}, error) { const classified = classifyWorkbenchReadModelFailure(error); const context = request?.hwlabHttpRequestContext; if (context) { context.route = "/v1/workbench/events"; context.otelAttributes = { ...(context.otelAttributes ?? {}), "workbench.route": "events", "workbench.sse.lifecycle": "failed", "error.code": classified.code, "error.layer": "workbench.realtime" }; } logWorkbenchReadModelFailure(options.logger ?? console, { event: "workbench_realtime_route_failed", ok: false, route: "/v1/workbench/events", 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) { try { response.write(`event: workbench.error\n`); response.write(`data: ${JSON.stringify({ contractVersion: "workbench-events-v1", serverSentAt: new Date().toISOString(), type: "error", error: { code: classified.code, message: classified.message, valuesRedacted: true } })}\n\n`); response.end(); } catch { try { response.destroy(); } catch {} } return; } if (!response.destroyed) sendJson(response, classified.statusCode, workbenchError(classified.code, classified.message, { route: "/v1/workbench/events", errorName: error?.name ?? "Error" })); } async function writeMessageRealtimeSnapshotSafe({ writeEvent, readModel, actor, sessionId, threadId = null, traceId = null, reason = "message", cursor = null } = {}) { const safeSession = safeSessionId(sessionId); if (!safeSession || typeof readModel?.queryFacts !== "function") return; try { const result = await queryFactsByRouteId(readModel, safeSession, { families: WORKBENCH_SESSION_MESSAGE_PAGE_FAMILIES }); if (result.error) return; const session = visibleFactSessions(result.facts, actor)[0] ?? null; if (!session) return; const messages = factMessagesForSession(session, result.facts); const message = [...messages].reverse().find((item) => { if (!isAssistantLikeRole(item?.role)) return false; return !traceId || item.traceId === traceId; }) ?? null; if (!message) return; writeEvent("workbench.message.snapshot", { type: "message.snapshot", sessionId: factSessionId(session), threadId: threadId ?? session.threadId ?? null, traceId: message.traceId ?? traceId ?? null, reason, cursor, message }); } catch (error) { writeEvent("workbench.error", { type: "error", error: { code: "workbench_message_snapshot_failed", message: error?.message ?? "Workbench realtime message snapshot failed." } }); } } function realtimeEventCreatedAt(payload) { const candidates = [payload?.event?.createdAt, payload?.snapshot?.lastEvent?.createdAt, payload?.turn?.updatedAt]; for (const value of candidates) { const text = textValue(value); if (text) return text; } return null; } function realtimeTraceSeq(payload) { const seq = Number(payload?.cursor?.traceSeq ?? payload?.event?.seq ?? payload?.snapshot?.lastEvent?.seq); return Number.isFinite(seq) && seq >= 0 ? seq : null; } function realtimeEventId(payload) { const raw = payload?.cursor?.outboxSeq ?? payload?.outboxSeq ?? payload?.id; const text = textValue(raw); if (!text || /[\r\n]/u.test(text)) return null; return text.slice(0, 128); } function workbenchRealtimeAfterSeq(request, url) { for (const value of [url.searchParams.get("afterSeq"), url.searchParams.get("afterOutboxSeq"), request?.headers?.["last-event-id"]]) { const parsed = nonNegativeInteger(value, 0); if (parsed > 0) return parsed; } return 0; } function nowMs() { if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now(); return Date.now(); } function sleepMs(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } 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" }); } 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 }); } 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" }); } 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; } 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; } function workbenchTerminalSourceAt(result = null, projection = null) { if (projection?.terminal !== true) return null; return result?.traceSummary?.updatedAt ?? result?.agentRun?.updatedAt ?? result?.updatedAt ?? projection?.updatedAt ?? null; } 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"; } 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"; } function responseBodyBytes(body = {}) { try { return Buffer.byteLength(JSON.stringify(body)); } catch { return 0; } } function statusClassLabel(value) { const status = Number(value); if (!Number.isFinite(status) || status <= 0) return "unknown"; return `${Math.floor(status / 100)}xx`; } 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; } 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); } 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 }; } 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; } 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 }); } 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 }; } } }; } 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; } function workbenchRuntimeOutboxReader(request, options) { return workbenchRuntimeReader(request, options); } 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 })); } 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 }; } function queryRequestsOnlySessionFacts(query = {}) { const families = Array.isArray(query.families) ? query.families : []; return families.length === 1 && families[0] === "sessions"; } 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 }; } function summaryTraceIds(sessions = [], facts = {}) { return uniqueText([ ...sessions.map(factLastTraceId), ...sessions.map((session) => factCurrentTraceForSession(facts, factSessionId(session), session)?.traceId) ]); } function visibleFactSessions(facts, actor) { return hideShadowedAgentRunAliasSessions(factArray(facts?.sessions) .filter((session) => canActorReadFactSession(session, actor)) .sort(compareFactSessionsDesc)); } 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))); } function isAgentRunAliasSessionId(sessionId) { return /^ses_agentrun_/u.test(textValue(sessionId)); } 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; } 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 }; } 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); } function factSessionSummary(session, facts = {}) { const sessionId = factSessionId(session); if (!sessionId) return null; const currentTrace = factCurrentTraceForSession(facts, sessionId, session); const traceId = currentTrace?.traceId ?? factLastTraceId(session) ?? factLatestTraceIdForSession(facts, sessionId); const projection = traceId ? factProjectionForTrace(facts, traceId) : null; const turn = traceId ? factTurnForTrace(facts, traceId) : null; const checkpoint = traceId ? factCheckpointForTrace(facts, traceId) : null; const trace = traceId ? factTraceSnapshot(facts, traceId) : null; const messages = factMessagesForSession(session, facts); const terminalMessage = traceId ? factTerminalMessageForTrace(messages, traceId) : null; const traceStatus = normalizeTerminalStatus(trace?.status); const checkpointStatus = normalizeTerminalStatus(checkpoint?.status); const messageTerminalStatus = factTerminalStatusFromMessageProjection(terminalMessage); const turnStatus = normalizeStatus(turn?.status); const launchContext = compactLaunchContext(session?.sessionJson?.launchContext); const status = normalizeStatus(checkpointStatus ?? traceStatus ?? messageTerminalStatus ?? nonUnknownStatus(turnStatus) ?? nonUnknownStatus(currentTrace?.status) ?? session?.status); const timing = traceId ? factCombinedTimingProjection(status, checkpoint, messageTerminalStatus ? terminalMessage : null, turn, trace) : factTimingProjection(session, status); const title = sessionTitleFromMessages(messages); const preview = sessionPreviewFromMessages(messages); return { sessionId, threadId: safeOpaqueId(session?.threadId) ?? (textValue(session?.threadId) || null), agentId: session?.agentId ?? "hwlab-code-agent", title, preview, titleSource: title ? "message-projection" : null, previewSource: preview ? "message-projection" : null, status, running: RUNNING_STATUSES.has(status), terminal: TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status), lastTraceId: traceId, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, staleMs: projection?.staleMs ?? null, blocker: projection?.blocker ?? null, providerProfile: textValue(session?.providerProfile ?? session?.sessionJson?.providerProfile) || null, launchContext, messageCount: messages.length, firstUserMessagePreview: firstUserPreview(messages), updatedAt: factUpdatedAt(session), turnSummary: turn ? { turnId: factTurnId(turn, traceId), traceId, status, running: RUNNING_STATUSES.has(status), terminal: TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status), eventCount: projection?.lastProjectedSeq ?? trace?.eventCount, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, staleMs: projection?.staleMs ?? null, blocker: projection?.blocker ?? null, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, updatedAt: factUpdatedAt(turn) } : null, valuesRedacted: true }; } function factSessionDetail(session, facts = {}, options = {}) { const sessionId = factSessionId(session); return buildWorkbenchSessionDetail({ summary: factSessionSummary(session, facts), session, sessionId, includeMessages: options.includeMessages === true, messages: factMessagesForSession(session, facts) }); } function factMessagesForSession(session, facts = {}) { const sessionId = factSessionId(session); if (!sessionId) return []; const partsByMessageId = new Map(); for (const part of factArray(facts.parts)) { if (part.sessionId !== sessionId) continue; const messageId = textValue(part.messageId); if (!messageId) continue; const existing = partsByMessageId.get(messageId) ?? []; existing.push(part); partsByMessageId.set(messageId, existing); } const messageGroups = canonicalFactMessageGroupsForSession( factArray(facts.messages) .filter((message) => message.sessionId === sessionId) .sort((left, right) => compareFactMessagesForSessionAsc(left, right, facts)), partsByMessageId, facts ); return messageGroups .sort((left, right) => compareFactMessagesForSessionAsc(left.message, right.message, facts)) .map((group) => factMessageDto(group.message, group.parts, facts)) .filter(Boolean); } function canonicalFactMessageGroupsForSession(messages = [], partsByMessageId = new Map(), facts = {}) { const groups = new Map(); for (const message of messages) { const key = factMessageCanonicalGroupKey(message); const existing = groups.get(key) ?? []; existing.push(message); groups.set(key, existing); } return [...groups.values()].map((groupMessages) => { const message = selectCanonicalFactMessage(groupMessages, partsByMessageId, facts); const aliasIds = uniqueText(groupMessages.map((item) => item?.messageId)); const parts = aliasIds.flatMap((messageId) => partsByMessageId.get(messageId) ?? []); return { message, parts }; }); } function factMessageCanonicalGroupKey(message) { const messageId = textValue(message?.messageId) || textValue(message?.id); const role = textValue(message?.role) || "agent"; const traceId = safeTraceId(message?.traceId); const sessionId = factSessionId(message) ?? textValue(message?.sessionId) ?? "session"; if (traceId && isAssistantLikeRole(role)) return `assistant:${sessionId}:${traceId}`; return `message:${messageId || sessionId}:${role}:${traceId || "none"}`; } function selectCanonicalFactMessage(messages = [], partsByMessageId = new Map(), facts = {}) { if (messages.length <= 1) return messages[0] ?? null; const traceId = safeTraceId(messages[0]?.traceId); const preferredMessageId = traceId ? workbenchLifecycleMessageId(traceId, "agent") : null; const turnMessageId = traceId ? textValue(factTurnForTrace(facts, traceId)?.messageId) || null : null; return [...messages].sort((left, right) => { const score = factMessageCanonicalScore(right, partsByMessageId, preferredMessageId, turnMessageId) - factMessageCanonicalScore(left, partsByMessageId, preferredMessageId, turnMessageId); return score || compareFactRecordsDesc(left, right); })[0] ?? messages[0] ?? null; } function factMessageCanonicalScore(message, partsByMessageId = new Map(), preferredMessageId = null, turnMessageId = null) { const messageId = textValue(message?.messageId) || textValue(message?.id); let score = 0; if (preferredMessageId && messageId === preferredMessageId) score += 10000; if (turnMessageId && messageId === turnMessageId) score += 1000; if (factMessageHasFinalResponsePart(message, partsByMessageId)) score += 100; if (message?.sealed === true || message?.terminal === true) score += 50; if (isTerminalProjectionStatus(normalizeStatus(message?.status))) score += 25; score += Math.min(10, factSeq(message) ?? 0); return score; } function factMessageHasFinalResponsePart(message, partsByMessageId = new Map()) { const messageId = textValue(message?.messageId) || textValue(message?.id); if (!messageId) return false; return (partsByMessageId.get(messageId) ?? []).some((part) => (textValue(part?.partType ?? part?.type) === "final_response") && Boolean(projectionText(part?.text, part?.content, part?.message))); } function workbenchLifecycleMessageId(traceId, role = "agent") { const traceSuffix = (safeTraceId(traceId) || String(traceId || "trace")) .replace(/^trc_/u, "") .replace(/[^A-Za-z0-9_.:-]/gu, "_") .slice(0, 48) || "trace"; return `msg_${traceSuffix}_${role === "user" ? "user" : "agent"}`; } function factMessageDto(message, parts = [], facts = {}) { const messageId = safeMessageId(message?.messageId) || textValue(message?.messageId); if (!messageId) return null; const traceId = safeTraceId(message?.traceId) ?? null; const role = textValue(message?.role) || "agent"; const turn = traceId ? factTurnForTrace(facts, traceId) : null; const checkpoint = traceId ? factCheckpointForTrace(facts, traceId) : null; const messageStatus = normalizeStatus(message?.status); const assistantLike = isAssistantLikeRole(role); const legacyText = projectionText(message?.text, message?.content, message?.message, message?.finalResponse); const baseParts = parts.length > 0 ? [...parts].sort(compareFactPartsAsc).map((part) => factPartDto(part, messageId, traceId)).filter(Boolean) : !assistantLike && legacyText ? [partFact({ type: "text", text: legacyText, status: message?.status }, 0, messageId, traceId)] : []; const checkpointStatus = assistantLike ? normalizeTerminalStatus(checkpoint?.status) : null; const preliminaryStatus = normalizeStatus(checkpointStatus ?? (assistantLike ? turn?.status : null) ?? messageStatus); const syntheticTerminalFinalResponse = assistantLike && !factPartsHaveFinalResponse(baseParts) ? factSyntheticTerminalFinalResponse(preliminaryStatus, traceId, message, checkpoint, turn) : null; const normalizedParts = syntheticTerminalFinalResponse ? [...baseParts, partFact({ type: "final_response", text: syntheticTerminalFinalResponse.text, status: syntheticTerminalFinalResponse.status }, baseParts.length, messageId, traceId)] : baseParts; const messageTerminalStatus = assistantLike ? factTerminalStatusFromMessageProjection(message, normalizedParts) : null; const status = normalizeStatus(checkpointStatus ?? messageTerminalStatus ?? (assistantLike ? turn?.status : null) ?? messageStatus); const timing = assistantLike ? factCombinedTimingProjection(status, checkpoint, messageTerminalStatus ? { ...message, parts: normalizedParts } : null, turn, message) : factTimingProjection(message, status); const text = factMessageAuthorityText({ role, status, parts: normalizedParts, legacyText }); return { messageId, role, sessionId: (safeSessionId(message?.sessionId) ?? textValue(message?.sessionId)) || null, traceId, turnId: safeTurnId(message?.turnId) || traceId, status, parts: normalizedParts, text: text || "", textPreview: text ? text.slice(0, 240) : null, createdAt: textValue(message?.createdAt) || null, updatedAt: factUpdatedAt(message), timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, projectionStatus: null, projectionHealth: null, valuesRedacted: message?.valuesRedacted !== false }; } function factMessageAuthorityText({ role, status, parts = [], legacyText = null } = {}) { if (!isAssistantLikeRole(role)) return firstFactPartText(parts) ?? legacyText ?? ""; if (!isTerminalProjectionStatus(status)) return ""; return firstFactPartText(parts, "final_response") ?? ""; } function factPartsHaveFinalResponse(parts = []) { return parts.some((part) => part?.type === "final_response" && Boolean(projectionText(part?.text))); } function factSyntheticTerminalFinalResponse(status, traceId = null, ...records) { const terminalStatus = normalizeTerminalStatus(status); if (!terminalStatus || terminalStatus === "completed") return null; return terminalFinalResponse(terminalStatus, { traceId, status: terminalStatus, records }, { evidence: records }); } function isTerminalProjectionStatus(status) { return TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status); } function firstFactPartText(parts = [], type = null) { for (const part of parts) { if (type && part?.type !== type) continue; const text = projectionText(part?.text); if (text) return text; } return null; } function factMessageFinalResponseText(message) { if (!message || !isAssistantLikeRole(message.role) || !isTerminalProjectionStatus(message.status)) return null; return firstFactPartText(message.parts, "final_response"); } function factTerminalMessageForTrace(messages = [], traceId = null) { const safeTrace = safeTraceId(traceId); return [...factArray(messages)].reverse().find((message) => { if (!message || !isAssistantLikeRole(message.role)) return false; if (safeTrace && message.traceId !== safeTrace) return false; return Boolean(factTerminalStatusFromMessageProjection(message)); }) ?? null; } function factTerminalStatusFromMessageProjection(message = null, parts = null) { if (!message || !isAssistantLikeRole(message.role)) return null; const sourceParts = Array.isArray(parts) ? parts : factArray(message.parts); if (!firstFactPartText(sourceParts, "final_response")) return null; const messageStatus = normalizeTerminalStatus(message.status); if (messageStatus) return messageStatus; for (const part of sourceParts) { if (textValue(part?.partType ?? part?.type) !== "final_response") continue; if (!projectionText(part?.text, part?.content, part?.message)) continue; const partStatus = normalizeTerminalStatus(part?.status); if (partStatus) return partStatus; if (part?.terminal === true || part?.sealed === true) return "completed"; } return message.terminal === true || message.sealed === true ? "completed" : null; } function factPartDto(part, messageId, traceId) { const partId = safePartId(part?.partId) || textValue(part?.partId) || `prt_${hash(`${messageId}:${part?.partIndex ?? 0}:${part?.partType ?? part?.type ?? "text"}`).slice(0, 24)}`; const text = projectionText(part?.text, part?.content, part?.message); return { partId, messageId, traceId: safeTraceId(part?.traceId) ?? traceId, type: textValue(part?.partType ?? part?.type) || "text", text: text || null, status: normalizeStatus(part?.status), toolName: textValue(part?.toolName ?? part?.name) || null, createdAt: textValue(part?.createdAt ?? part?.occurredAt) || null, valuesRedacted: part?.valuesRedacted !== false }; } function factTurnForTrace(facts = {}, traceId, turnId = null) { const safeTrace = safeTraceId(traceId); if (!safeTrace) return null; const requestedTurn = safeTurnId(turnId); const matches = factArray(facts.turns) .filter((turn) => turn.traceId === safeTrace && (!requestedTurn || turn.turnId === requestedTurn || turn.turnId === safeTrace)) .sort(compareFactRecordsDesc); return matches[0] ?? null; } function factTurnSnapshot({ turn = null, session = null, facts = {}, traceId, turnId = null } = {}) { const safeTrace = safeTraceId(traceId); const resolvedTurnId = factTurnId(turn, safeTrace) ?? safeTurnId(turnId) ?? safeTrace; const messages = session ? factMessagesForSession(session, facts) : []; const userMessage = messages.find((message) => message.role === "user") ?? null; const assistantMessage = [...messages].reverse().find((message) => isAssistantLikeRole(message.role)) ?? null; const terminalMessage = safeTrace ? factTerminalMessageForTrace(messages, safeTrace) : null; const checkpoint = safeTrace ? factCheckpointForTrace(facts, safeTrace) : null; const trace = factTraceSnapshot(facts, safeTrace); const checkpointStatus = normalizeTerminalStatus(checkpoint?.status); const traceStatus = normalizeTerminalStatus(trace.status); const messageTerminalStatus = factTerminalStatusFromMessageProjection(terminalMessage); const status = normalizeStatus(checkpointStatus ?? traceStatus ?? messageTerminalStatus ?? turn?.status ?? session?.status); const terminal = isTerminalProjectionStatus(status); const assistantText = terminal ? factMessageFinalResponseText(assistantMessage) : null; const timing = factCombinedTimingProjection(status, checkpoint, messageTerminalStatus ? terminalMessage : null, turn, trace); return { turnId: resolvedTurnId, traceId: safeTrace, status, running: RUNNING_STATUSES.has(status), terminal, sessionId: factSessionId(session) ?? turn?.sessionId ?? checkpoint?.sessionId ?? null, threadId: safeOpaqueId(session?.threadId) ?? (textValue(session?.threadId) || null), userMessageId: userMessage?.messageId ?? null, assistantMessageId: assistantMessage?.messageId ?? turn?.messageId ?? null, assistantText: assistantText ?? null, finalResponse: assistantText ? { text: assistantText, sealed: true, source: "message-part" } : null, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, agentRun: checkpoint ? { runId: textValue(checkpoint.runId) || null, commandId: textValue(checkpoint.commandId) || null, status, lastSeq: factSeq(checkpoint), valuesRedacted: true } : null, trace: { traceId: safeTrace, status: trace.status, eventCount: trace.eventCount, timing: trace.timing, startedAt: trace.startedAt, lastEventAt: trace.lastEventAt, finishedAt: trace.finishedAt, durationMs: trace.durationMs, updatedAt: trace.updatedAt }, urls: { self: resolvedTurnId ? `/v1/workbench/turns/${encodeURIComponent(resolvedTurnId)}` : null, traceEvents: safeTrace ? `/v1/workbench/traces/${encodeURIComponent(safeTrace)}/events` : null } }; } function factTraceSnapshot(facts = {}, traceId) { const safeTrace = safeTraceId(traceId); const events = factArray(facts.traceEvents) .filter((event) => !safeTrace || event.traceId === safeTrace) .sort(compareFactTraceEventsAsc) .map(factTraceEventDto) .filter(Boolean); const status = durableTraceStatus(events); const lastEvent = events.at(-1) ?? null; const checkpoint = factCheckpointForTrace(facts, safeTrace); const timing = factTraceTimingProjection(events, checkpoint, status); return { traceId: safeTrace, status, eventCount: events.length, events, lastEvent, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, updatedAt: lastEvent?.updatedAt ?? lastEvent?.createdAt ?? checkpoint?.updatedAt ?? null, valuesRedacted: true, secretMaterialStored: false }; } function factTraceTimingProjection(events = [], checkpoint = null, status = null) { const checkpointTiming = factTimingProjection(checkpoint, status); const normalizedStatus = normalizeStatus(status ?? checkpoint?.status); const terminal = TERMINAL_STATUSES.has(normalizedStatus) && !RUNNING_STATUSES.has(normalizedStatus); const observedAt = new Date().toISOString(); const eventStartTimes = factArray(events).flatMap((event) => [event?.createdAt, event?.occurredAt]); const eventActivityTimes = factArray(events).flatMap((event) => [event?.createdAt, event?.occurredAt, event?.updatedAt]); const terminalEventTimes = factArray(events) .filter(factTraceEventIsTerminalAuthority) .flatMap(factTraceEventTerminalTimes); const startedAt = firstTimestampIso(checkpointTiming.startedAt, ...eventStartTimes); const finishedAt = terminal ? latestTimestampIso(checkpointTiming.finishedAt, ...terminalEventTimes) : null; const lastEventAt = terminal && finishedAt ? finishedAt : latestTimestampIso(checkpointTiming.lastEventAt, ...eventActivityTimes); const durationMs = elapsedFactMs(startedAt, terminal ? finishedAt : observedAt); const lastEventAgeMs = terminal ? null : elapsedFactMs(lastEventAt, observedAt); return { ...checkpointTiming, startedAt, lastEventAt, finishedAt, durationMs, observedAt: terminal ? null : observedAt, lastEventAgeMs, valuesRedacted: true }; } function factTraceEventIsTerminalAuthority(event = null) { const eventType = textValue(event?.eventType ?? event?.type); return event?.terminal === true || event?.sealed === true || eventType === "terminal"; } function factTraceEventTerminalTimes(event = null) { const source = objectValue(event?.timing); return [source?.finishedAt, event?.finishedAt, source?.lastEventAt, event?.lastEventAt, event?.occurredAt, event?.createdAt]; } function factTraceEventDto(event, index) { const seq = factProjectedSeq(event); if (!seq) return null; const sourceSeq = Number.isFinite(Number(event?.sourceSeq)) && Number(event.sourceSeq) >= 0 ? Math.trunc(Number(event.sourceSeq)) : null; return { ...event, id: textValue(event?.id) || textValue(event?.sourceEventId) || null, seq, projectedSeq: seq, sourceSeq, type: textValue(event?.type ?? event?.eventType) || "event", label: textValue(event?.label ?? event?.eventType ?? event?.type) || null, status: normalizeStatus(event?.status), createdAt: textValue(event?.createdAt ?? event?.occurredAt) || null, updatedAt: factUpdatedAt(event), valuesRedacted: event?.valuesRedacted !== false }; } function factProjectionForTrace(facts = {}, traceId) { const checkpoint = factCheckpointForTrace(facts, traceId); if (!checkpoint) { return { projectionStatus: "unknown", projectionHealth: "unknown", lastProjectedSeq: null, sourceRunId: null, sourceCommandId: null, staleMs: null, blocker: null, updatedAt: null, valuesRedacted: true }; } const projectionStatus = normalizeProjectionStatus(checkpoint.projectionStatus); const projectionHealth = normalizeProjectionHealth(checkpoint.projectionHealth, projectionStatus); const diagnostic = objectValue(checkpoint.diagnostic); const timing = factTimingProjection(checkpoint, normalizeStatus(checkpoint.status)); return { projectionStatus, projectionHealth, lastProjectedSeq: factSeq(checkpoint), sourceRunId: textValue(checkpoint.runId ?? checkpoint.sourceRunId) || null, sourceCommandId: textValue(checkpoint.commandId ?? checkpoint.sourceCommandId) || null, staleMs: null, blocker: diagnostic.blocker ?? checkpoint.blocker ?? null, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, updatedAt: factUpdatedAt(checkpoint), valuesRedacted: true }; } function factTimingProjection(record = null, status = null) { const source = objectValue(record?.timing); const observedAt = new Date().toISOString(); const startedAt = timestampIso(source?.startedAt ?? record?.startedAt ?? record?.createdAt); const lastEventAt = timestampIso(source?.lastEventAt ?? record?.lastEventAt ?? record?.updatedAt ?? record?.createdAt); const normalizedStatus = normalizeStatus(status ?? record?.status); const terminal = record?.terminal === true || TERMINAL_STATUSES.has(normalizedStatus) && !RUNNING_STATUSES.has(normalizedStatus); const finishedAt = terminal ? timestampIso(source?.finishedAt ?? record?.finishedAt ?? record?.completedAt) : null; const durationMs = elapsedFactMs(startedAt, terminal ? finishedAt : observedAt); const lastEventAgeMs = terminal ? null : elapsedFactMs(lastEventAt, observedAt); return { startedAt, lastEventAt, finishedAt, durationMs, observedAt: terminal ? null : observedAt, lastEventAgeMs, valuesRedacted: source?.valuesRedacted !== false }; } function factCombinedTimingProjection(status = null, ...records) { const normalizedStatus = normalizeStatus(status ?? records.find((record) => record?.status)?.status); const terminal = TERMINAL_STATUSES.has(normalizedStatus) && !RUNNING_STATUSES.has(normalizedStatus); const observedAt = new Date().toISOString(); const timings = records.map((record) => factTimingSource(record)).filter(Boolean); const startedAt = firstTimestampIso(...timings.map((timing) => timing.startedAt)); const finishedAt = terminal ? latestTimestampIso(...timings.map((timing) => timing.finishedAt)) : null; const lastEventAt = terminal && finishedAt ? finishedAt : latestTimestampIso(...timings.map((timing) => timing.lastEventAt)); const durationMs = elapsedFactMs(startedAt, terminal ? finishedAt : observedAt); const lastEventAgeMs = terminal ? null : elapsedFactMs(lastEventAt, observedAt); return { startedAt, lastEventAt, finishedAt, durationMs, observedAt: terminal ? null : observedAt, lastEventAgeMs, valuesRedacted: true }; } function factTimingSource(record = null) { if (!record) return null; const source = objectValue(record?.timing); return { startedAt: timestampIso(source?.startedAt ?? record?.startedAt ?? record?.createdAt), lastEventAt: timestampIso(source?.lastEventAt ?? record?.lastEventAt ?? record?.updatedAt ?? record?.createdAt), finishedAt: timestampIso(source?.finishedAt ?? record?.finishedAt ?? record?.completedAt) }; } function firstTimestampIso(...values) { for (const value of values) { const timestamp = timestampIso(value); if (timestamp) return timestamp; } return null; } function latestTimestampIso(...values) { let latest = null; let latestMs = Number.NEGATIVE_INFINITY; for (const value of values) { const timestamp = timestampIso(value); if (!timestamp) continue; const ms = Date.parse(timestamp); if (!Number.isFinite(ms) || ms < latestMs) continue; latest = timestamp; latestMs = ms; } return latest; } function elapsedFactMs(startedAt, endedAt) { const start = Date.parse(String(startedAt ?? "")); const end = Date.parse(String(endedAt ?? "")); if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null; return Math.trunc(end - start); } function timestampIso(value) { const ms = Date.parse(String(value ?? "")); return Number.isFinite(ms) ? new Date(ms).toISOString() : null; } function factCheckpointForTrace(facts = {}, traceId) { const safeTrace = safeTraceId(traceId); if (!safeTrace) return null; return factArray(facts.checkpoints) .filter((checkpoint) => checkpoint.traceId === safeTrace) .sort(compareFactRecordsDesc)[0] ?? null; } function workbenchProjectionStoreError(error) { const data = objectValue(error?.data) ?? {}; const retryAfterNumber = Number(data.retryAfterMs); const retryAfterMs = Number.isFinite(retryAfterNumber) && retryAfterNumber >= 0 ? Math.trunc(retryAfterNumber) : null; const retryAttempt = nonNegativeIntegerOrNull(data.retryAttempt ?? data.retryAttempts); const retryMax = nonNegativeIntegerOrNull(data.retryMax); const retryDelaysMs = Array.isArray(data.retryDelaysMs) ? data.retryDelaysMs.map(nonNegativeIntegerOrNull).filter((value) => value !== null) : []; return workbenchError("projection_store_unavailable", "Workbench durable projection store is unavailable.", { causeCode: error?.code ?? null, queryResult: textValue(data.queryResult) || null, blockedLayer: textValue(data.blockedLayer) || null, retryable: data.retryable !== false, transient: data.transient === true || data.retryable === true, retryAfterMs: retryAfterMs ?? null, retryAttempt, retryMax, retryAttempts: retryAttempt, retryExhausted: data.retryExhausted === true, retryDelaysMs: retryDelaysMs.length > 0 ? retryDelaysMs : null }); } function nonNegativeIntegerOrNull(value) { const parsed = Number(value); return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : null; } function factSessionId(session) { return safeSessionId(session?.sessionId ?? session?.id) ?? null; } function factLastTraceId(session) { return safeTraceId(session?.lastTraceId ?? session?.traceId) ?? null; } function factLatestTraceIdForSession(facts = {}, sessionId) { const turn = factArray(facts.turns).filter((item) => item.sessionId === sessionId).sort(compareFactRecordsDesc)[0] ?? null; return safeTraceId(turn?.traceId) ?? null; } function factCurrentTraceForSession(facts = {}, sessionId, session = null) { const candidates = []; const sessionTraceId = factLastTraceId(session); if (sessionTraceId) candidates.push(factTraceCandidate(session, sessionTraceId, normalizeStatus(session?.status), 0)); for (const message of factArray(facts.messages)) { if (message?.sessionId !== sessionId || !safeTraceId(message?.traceId)) continue; candidates.push(factMessageTraceCandidate(message)); } for (const turn of factArray(facts.turns)) { if (turn?.sessionId !== sessionId || !safeTraceId(turn?.traceId)) continue; candidates.push(factTraceCandidate(turn, safeTraceId(turn.traceId), normalizeStatus(turn?.status), 3)); } return candidates.sort(compareTraceCandidatesDesc)[0] ?? null; } function factMessageTraceCandidate(message) { const role = textValue(message?.role); let status = normalizeStatus(message?.status); let priority = 1; if (isAssistantLikeRole(role)) { priority = 2; if (status === "unknown") status = "running"; } else if (safeTraceId(message?.traceId)) { status = "running"; } return factTraceCandidate(message, safeTraceId(message?.traceId), status, priority); } function factTraceCandidate(record, traceId, status, priority) { return { traceId: safeTraceId(traceId) ?? null, status: normalizeStatus(status), seq: factSeq(record), updatedAt: factUpdatedAt(record), priority }; } function compareTraceCandidatesDesc(left, right) { return compareNumberDesc(left?.seq, right?.seq) || compareTimestampDesc(left?.updatedAt, right?.updatedAt) || compareNumberDesc(left?.priority, right?.priority); } function nonUnknownStatus(status) { return normalizeStatus(status) === "unknown" ? null : status; } function factTurnId(turn, fallbackTraceId = null) { return safeTurnId(turn?.turnId) ?? safeTraceId(turn?.turnId) ?? safeTraceId(fallbackTraceId) ?? null; } function factUpdatedAt(record) { return textValue(record?.updatedAt ?? record?.occurredAt ?? record?.createdAt) || null; } function factSeq(record) { for (const value of [record?.projectedSeq, record?.sourceSeq, record?.seq]) { const parsed = Number(value); if (Number.isFinite(parsed) && parsed >= 0) return Math.trunc(parsed); } return null; } function factProjectedSeq(record) { const parsed = Number(record?.projectedSeq); return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : null; } function normalizeProjectionStatus(value) { const status = normalizeStatus(value); if (status === "caught-up") return "caught-up"; if (status === "caughtup") return "caught-up"; return ["projecting", "blocked", "stalled", "unknown"].includes(status) ? status : "unknown"; } function normalizeProjectionHealth(value, projectionStatus = "unknown") { const health = normalizeStatus(value); if (health === "healthy" && projectionStatus === "caught-up") return "caught-up"; if (health === "healthy" && projectionStatus === "projecting") return "projecting"; if (["caught-up", "projecting", "degraded", "stalled", "unavailable", "unknown"].includes(health)) return health; return projectionStatus === "caught-up" || projectionStatus === "projecting" ? projectionStatus : "unknown"; } function compareFactSessionsDesc(left, right) { return compareTimestampDesc(factUpdatedAt(left), factUpdatedAt(right)) || compareText(factSessionId(left), factSessionId(right)); } function compareFactMessagesAsc(left, right) { return compareNumberAsc(factSeq(left), factSeq(right)) || compareTimestampAsc(left?.createdAt ?? left?.updatedAt, right?.createdAt ?? right?.updatedAt) || compareText(left?.messageId, right?.messageId); } function compareFactMessagesForSessionAsc(left, right, facts = {}) { const leftKey = factMessageTimelineKey(left, facts); const rightKey = factMessageTimelineKey(right, facts); return compareOptionalTimestampAsc(leftKey.timelineAnchorAt, rightKey.timelineAnchorAt) || compareOptionalNumberAsc(leftKey.turnSeq, rightKey.turnSeq) || compareTimestampAsc(leftKey.turnStartedAt, rightKey.turnStartedAt) || compareText(leftKey.traceId, rightKey.traceId) || compareOptionalNumberAsc(leftKey.roleRank, rightKey.roleRank) || compareTimestampAsc(leftKey.messageCreatedAt, rightKey.messageCreatedAt) || compareOptionalNumberAsc(leftKey.messageSeq, rightKey.messageSeq) || compareText(left?.messageId, right?.messageId); } function factMessageTimelineKey(message, facts = {}) { const traceId = safeTraceId(message?.traceId) ?? null; const turn = traceId ? factTurnForTrace(facts, traceId) : null; const checkpoint = traceId ? factCheckpointForTrace(facts, traceId) : null; const anchorMessage = traceId ? factTimelineAnchorMessageForTrace(facts, traceId) : null; return { traceId: traceId ?? "", timelineAnchorAt: firstTimestampIso(anchorMessage?.createdAt, anchorMessage?.updatedAt), turnSeq: firstFiniteNumber(factSeq(turn), factSeq(checkpoint)), turnStartedAt: firstTimestampIso(turn?.startedAt, checkpoint?.startedAt, message?.createdAt, message?.updatedAt), roleRank: factMessageTimelineRoleRank(message?.role), messageCreatedAt: timestampIso(message?.createdAt ?? message?.updatedAt), messageSeq: factSeq(message) }; } function factTimelineAnchorMessageForTrace(facts = {}, traceId) { const safeTrace = safeTraceId(traceId); if (!safeTrace) return null; const messages = factArray(facts.messages).filter((message) => message.traceId === safeTrace); const userMessages = messages.filter((message) => normalizeRole(message?.role) === "user"); const candidates = userMessages.length > 0 ? userMessages : messages; return [...candidates].sort((left, right) => compareOptionalTimestampAsc(left?.createdAt ?? left?.updatedAt, right?.createdAt ?? right?.updatedAt) || compareOptionalNumberAsc(factSeq(left), factSeq(right)) || compareText(left?.messageId, right?.messageId))[0] ?? null; } function factMessageTimelineRoleRank(role) { const normalized = normalizeRole(role); if (normalized === "user") return 0; if (isAssistantLikeRole(normalized)) return 1; return 2; } function firstFiniteNumber(...values) { for (const value of values) { const parsed = Number(value); if (Number.isFinite(parsed)) return Math.trunc(parsed); } return null; } function compareOptionalNumberAsc(left, right) { const leftNumber = Number(left); const rightNumber = Number(right); const leftComparable = Number.isFinite(leftNumber) ? leftNumber : Number.MAX_SAFE_INTEGER; const rightComparable = Number.isFinite(rightNumber) ? rightNumber : Number.MAX_SAFE_INTEGER; return leftComparable - rightComparable; } function compareFactPartsAsc(left, right) { return compareNumberAsc(left?.partIndex, right?.partIndex) || compareNumberAsc(factSeq(left), factSeq(right)) || compareText(left?.partId, right?.partId); } function compareFactTraceEventsAsc(left, right) { return compareNumberAsc(factProjectedSeq(left), factProjectedSeq(right)); } function compareFactRecordsDesc(left, right) { return compareNumberDesc(factSeq(left), factSeq(right)) || compareTimestampDesc(factUpdatedAt(left), factUpdatedAt(right)); } function compareTimestampAsc(left, right) { return timestampMs(left) - timestampMs(right); } function compareTimestampDesc(left, right) { return timestampMs(right) - timestampMs(left); } function compareOptionalTimestampAsc(left, right) { const leftMs = optionalTimestampMs(left); const rightMs = optionalTimestampMs(right); return (leftMs ?? Number.MAX_SAFE_INTEGER) - (rightMs ?? Number.MAX_SAFE_INTEGER); } function compareNumberAsc(left, right) { return numericValue(left, Number.MAX_SAFE_INTEGER) - numericValue(right, Number.MAX_SAFE_INTEGER); } function compareNumberDesc(left, right) { return numericValue(right, -1) - numericValue(left, -1); } function compareText(left, right) { return textValue(left).localeCompare(textValue(right)); } function timestampMs(value) { const parsed = Date.parse(String(value ?? "")); return Number.isFinite(parsed) ? parsed : 0; } function optionalTimestampMs(value) { const parsed = Date.parse(String(value ?? "")); return Number.isFinite(parsed) ? parsed : null; } function numericValue(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function mergeFactSets(...sets) { const merged = emptyFactSet(); const keys = { sessions: "sessionId", messages: "messageId", parts: "partId", turns: "turnId", traceEvents: "id", checkpoints: "traceId" }; for (const set of sets) { for (const [key, idKey] of Object.entries(keys)) { const seen = new Set(merged[key].map((item) => textValue(item?.[idKey]))); for (const item of factArray(set?.[key])) { const id = textValue(item?.[idKey]); if (id && seen.has(id)) continue; merged[key].push(item); if (id) seen.add(id); } } } return merged; } function emptyFactSet() { return { sessions: [], messages: [], parts: [], turns: [], traceEvents: [], checkpoints: [] }; } function factArray(value) { return Array.isArray(value) ? value : []; } function uniqueText(values = []) { return [...new Set(values.map((value) => textValue(value)).filter(Boolean))]; } 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 }); } 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); } 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); } 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); } 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 } }); } 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 } }); } 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 } }); } 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 } }); } function messagesRoleSequence(messages = []) { return messages.map((message) => roleSequenceSymbol(message?.role)).join(""); } 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))}`; } function sessionSummaryNeedsTitleFallback(session) { if (!session || typeof session !== "object") return true; return !sessionPreviewText(session); } function sessionPreviewText(session) { return textValue( session?.firstUserMessagePreview ?? session?.snapshot?.firstUserMessagePreview ?? session?.userPreview ?? session?.title ?? session?.name ); } 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 "?"; } function normalizeRole(role) { return textValue(role).toLowerCase(); } function consecutiveRolePrefix(messages = [], role) { let count = 0; for (const message of messages) { if (normalizeRole(message?.role) !== role) break; count += 1; } return count; } 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; } 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 }; } 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; } 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 }; } function blockedTraceEventProjection(projection = {}, blocker) { return { ...projection, projectionStatus: "blocked", projectionHealth: "degraded", blocker, valuesRedacted: true }; } 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 }; } 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); } 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 }; } 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 } }); } 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; } 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 }; } 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")) }; } async function realtimeInitialSessionContext(readModel, actor, sessionId) { const safeId = safeSessionId(sessionId); if (!safeId || typeof readModel?.queryFacts !== "function") { const blocker = traceEventsReadModelBlocker("workbench_facts_query_unavailable", "Workbench realtime initial session must be read from durable facts; the facts query interface is unavailable.", { session: { sessionId: safeId }, route: "/v1/workbench/events" }); return { facts: emptyFactSet(), session: null, blocker }; } try { const result = await readModel.queryFacts({ sessionId: safeId, families: WORKBENCH_SESSION_DETAIL_FAMILIES, limit: MAX_PAGE_LIMIT }); if (result.error || result.durable === false) { const blocker = traceEventsReadModelBlocker("workbench_facts_query_failed", "Workbench realtime initial session facts query failed; realtime must surface the read-model error instead of falling back to legacy session snapshots.", { session: { sessionId: safeId }, route: "/v1/workbench/events" }); blocker.causeCode = result.error?.code ?? null; blocker.causeName = result.error?.name ?? null; blocker.message = result.error?.message ? `${blocker.message} Cause: ${result.error.message}` : result.durable === false ? `${blocker.message} Cause: durable facts reader unavailable.` : blocker.message; return { facts: emptyFactSet(), session: null, blocker }; } const facts = result.facts ?? emptyFactSet(); const session = visibleFactSessions(facts, actor).find((item) => factSessionId(item) === safeId) ?? null; return { facts, session, blocker: null }; } catch (error) { const blocker = traceEventsReadModelBlocker("workbench_facts_query_failed", "Workbench realtime initial session facts query failed; realtime must surface the read-model error instead of falling back to legacy session snapshots.", { session: { sessionId: safeId }, route: "/v1/workbench/events" }); blocker.causeCode = error?.code ?? null; blocker.causeName = error?.name ?? null; blocker.message = error?.message ? `${blocker.message} Cause: ${error.message}` : blocker.message; return { facts: emptyFactSet(), session: null, blocker }; } } async function writeTraceRealtimeSnapshot({ writeEvent, options, actor, traceId, reason }) { const context = await visibleTraceContext(options, actor, traceId); if (!context.visible) { writeEvent("workbench.trace.unavailable", { type: "trace.unavailable", reason, traceId, error: { code: "workbench_trace_not_found", message: "Workbench trace is not visible to the current actor." } }); return; } if (context.factsReadBlocker) { writeEvent("workbench.error", { type: "error", traceId, reason, error: context.factsReadBlocker }); } const sessionId = factSessionId(context.factSession) ?? safeSessionId(context.session?.id) ?? null; const threadId = safeOpaqueId(context.factSession?.threadId ?? context.session?.threadId) ?? (textValue(context.factSession?.threadId ?? context.session?.threadId) || null); const realtimeTraceSnapshot = context.factsReadBlocker ? blockedRealtimeTraceSnapshot(context) : traceSnapshotForRealtime(context.trace); const traceSeq = context.factsReadBlocker ? 0 : traceSnapshotLastSeq(context.trace); writeEvent("workbench.trace.snapshot", { type: "trace.snapshot", reason, sessionId, threadId, traceId, snapshot: { ...realtimeTraceSnapshot, sessionId, threadId }, cursor: { traceSeq } }); writeEvent("workbench.turn.snapshot", { type: "turn.snapshot", reason, sessionId, threadId, traceId, turn: realtimeTurnSnapshot(context), cursor: { traceSeq } }); } function realtimeTurnSnapshot(context) { if (context?.factsReadBlocker) return blockedRealtimeTurnSnapshot(context, context.factsReadBlocker); const facts = context?.facts; const traceId = safeTraceId(context?.traceId); if (traceId && facts && context?.factSession) { const factTurn = factTurnForTrace(facts, traceId); const projected = factTurnSnapshot({ turn: factTurn, session: context.factSession, facts, traceId, turnId: context.turnId }); if (projected) return projected; } const blocker = traceEventsReadModelBlocker("workbench_facts_turn_missing", "Workbench durable facts did not contain a turn snapshot; realtime must surface the read-model gap instead of falling back to legacy trace projection.", { traceId, session: context?.factSession, route: "/v1/workbench/events" }); return blockedRealtimeTurnSnapshot(context, blocker); } function blockedRealtimeTraceSnapshot(context) { const traceId = safeTraceId(context?.traceId ?? context?.factsReadBlocker?.traceId) ?? null; const blocker = context?.factsReadBlocker ?? traceEventsReadModelBlocker("workbench_facts_unavailable", "Workbench durable facts are unavailable for realtime trace snapshot.", { traceId, route: "/v1/workbench/events" }); const projection = blockedTraceEventProjection({}, blocker); return { traceId, status: "unknown", traceStatus: "unknown", events: [], eventCount: 0, fullTraceLoaded: true, hasMore: false, nextProjectedSeq: 0, updatedAt: null, projection, projectionStatus: projection.projectionStatus, projectionHealth: projection.projectionHealth, staleMs: projection.staleMs ?? null, blocker, valuesRedacted: true, secretMaterialStored: false }; } function blockedRealtimeTurnSnapshot(context, blocker) { const traceId = safeTraceId(context?.traceId ?? blocker?.traceId) ?? null; const turnId = safeTurnId(context?.turnId ?? blocker?.turnId) || traceId; const sessionId = safeSessionId(blocker?.sessionId ?? factSessionId(context?.factSession) ?? context?.session?.id) ?? null; const threadId = safeOpaqueId(context?.factSession?.threadId ?? context?.session?.threadId) ?? (textValue(context?.session?.threadId) || null); const projection = blockedTraceEventProjection({}, blocker); return { turnId, traceId, status: "unknown", running: false, terminal: false, sessionId, threadId, userMessageId: null, assistantMessageId: null, assistantText: null, finalResponse: null, timing: null, startedAt: null, lastEventAt: null, finishedAt: null, durationMs: null, agentRun: null, trace: { traceId, status: "unknown", eventCount: 0, updatedAt: null }, urls: { self: turnId ? `/v1/workbench/turns/${encodeURIComponent(turnId)}` : null, traceEvents: traceId ? `/v1/workbench/traces/${encodeURIComponent(traceId)}/events` : null }, projectionStatus: projection.projectionStatus, projectionHealth: projection.projectionHealth, staleMs: projection.staleMs ?? null, blocker, projection, valuesRedacted: true, secretMaterialStored: false }; } async function writeTraceRealtimeSnapshotSafe(input) { try { await writeTraceRealtimeSnapshot(input); } catch (error) { input.writeEvent("workbench.error", { type: "error", traceId: input.traceId, reason: input.reason, error: { code: "workbench_trace_snapshot_failed", causeCode: error?.code ?? null, retryable: error?.data?.retryable === true, message: error?.message ?? "Workbench trace snapshot failed.", valuesRedacted: true } }); } } async function visibleTraceContext(options, actor, traceId) { const readModel = createWorkbenchReadModel(options, actor); if (typeof readModel.queryFacts !== "function") { const projection = factProjectionForTrace(emptyFactSet(), traceId); const factsReadBlocker = traceEventsReadModelBlocker("workbench_facts_query_unavailable", "Workbench durable facts query is unavailable; realtime must surface the read-model gap instead of using legacy trace projection.", { traceId, projection, route: "/v1/workbench/events" }); return { visible: true, turnId: traceId, traceId, status: "unknown", projection, result: null, session: null, trace: factTraceSnapshot(emptyFactSet(), traceId), facts: emptyFactSet(), factSession: null, factsReadBlocker }; } let facts = emptyFactSet(); let factsReadBlocker = null; try { const factResult = await readModel.queryFacts({ traceId, families: [...WORKBENCH_SESSION_DETAIL_FAMILIES, ...WORKBENCH_TRACE_EVENT_PAGE_FAMILIES], limit: MAX_PAGE_LIMIT }); if (factResult.error || factResult.durable === false) { const projection = factProjectionForTrace(facts, traceId); factsReadBlocker = traceEventsReadModelBlocker("workbench_facts_query_failed", "Workbench durable facts query failed; realtime must surface the read-model error instead of silently falling back to legacy trace projection.", { traceId, projection, route: "/v1/workbench/events" }); factsReadBlocker.causeCode = factResult.error?.code ?? null; factsReadBlocker.causeName = factResult.error?.name ?? null; factsReadBlocker.message = factResult.error?.message ? `${factsReadBlocker.message} Cause: ${factResult.error.message}` : factResult.durable === false ? `${factsReadBlocker.message} Cause: durable facts reader unavailable.` : factsReadBlocker.message; } else { facts = factResult?.facts ?? facts; } } catch (error) { const projection = factProjectionForTrace(facts, traceId); factsReadBlocker = traceEventsReadModelBlocker("workbench_facts_query_failed", "Workbench durable facts query failed; realtime must surface the read-model error instead of silently falling back to legacy trace projection.", { traceId, projection, route: "/v1/workbench/events" }); factsReadBlocker.causeCode = error?.code ?? null; factsReadBlocker.causeName = error?.name ?? null; factsReadBlocker.message = error?.message ? `${factsReadBlocker.message} Cause: ${error.message}` : factsReadBlocker.message; } const resolved = factsReadBlocker ? { session: null, facts } : await visibleFactSessionForTrace(readModel, facts, actor, traceId); if (resolved.error) { const projection = factProjectionForTrace(facts, traceId); factsReadBlocker = traceEventsReadModelBlocker("workbench_facts_query_failed", "Workbench durable facts session lookup failed; realtime must surface the read-model error instead of using legacy trace projection.", { traceId, projection, route: "/v1/workbench/events" }); factsReadBlocker.causeCode = resolved.error?.code ?? null; factsReadBlocker.causeName = resolved.error?.name ?? null; factsReadBlocker.message = resolved.error?.message ? `${factsReadBlocker.message} Cause: ${resolved.error.message}` : factsReadBlocker.message; } else if (resolved.facts) { facts = resolved.facts; } const factSession = resolved.session ?? null; const trace = factTraceSnapshot(facts, traceId); const turn = factTurnForTrace(facts, traceId, traceId); let projection = factsReadBlocker ? blockedTraceEventProjection(factProjectionForTrace(facts, traceId), factsReadBlocker) : factProjectionForTrace(facts, traceId); const visible = Boolean(factsReadBlocker || factSession || turn || factCheckpointForTrace(facts, traceId) || trace.eventCount > 0); if (!visible) { factsReadBlocker = traceEventsReadModelBlocker("workbench_facts_trace_missing", "Workbench realtime could not find durable facts for the requested trace; realtime must surface the read-model gap instead of using legacy trace projection.", { traceId, projection, route: "/v1/workbench/events" }); projection = blockedTraceEventProjection(projection, factsReadBlocker); } if (!factsReadBlocker && !factSession) { factsReadBlocker = traceEventsReadModelBlocker("workbench_facts_session_missing", "Workbench durable facts did not contain the trace session; realtime must surface the read-model gap instead of using legacy trace projection.", { traceId, projection, turn, route: "/v1/workbench/events" }); projection = blockedTraceEventProjection(projection, factsReadBlocker); } const snapshot = factTurnSnapshot({ turn, session: factSession, facts, traceId, turnId: traceId }); return { visible: true, turnId: snapshot.turnId ?? traceId, traceId, status: snapshot.status, projection, result: null, session: null, trace, facts, factSession, factsReadBlocker }; } function traceSnapshotForRealtime(snapshot) { const events = Array.isArray(snapshot?.events) ? snapshot.events : []; return { ...snapshot, events, eventCount: Number.isFinite(Number(snapshot?.eventCount)) ? Number(snapshot.eventCount) : events.length, lastEvent: snapshot?.lastEvent ?? events.at(-1) ?? null, valuesRedacted: true, secretMaterialStored: false }; } function traceSnapshotSummary(snapshot) { const events = Array.isArray(snapshot?.events) ? snapshot.events : []; return { traceId: snapshot?.traceId ?? null, status: snapshot?.status ?? "missing", eventCount: Number.isFinite(Number(snapshot?.eventCount)) ? Number(snapshot.eventCount) : events.length, lastEvent: snapshot?.lastEvent ?? events.at(-1) ?? null, updatedAt: snapshot?.updatedAt ?? null, valuesRedacted: true }; } function traceSnapshotLastSeq(snapshot) { const events = Array.isArray(snapshot?.events) ? snapshot.events : []; return events.reduce((max, event, index) => Math.max(max, eventSeq(event, index)), 0); } 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." }; } 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"; } 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. } } function methodNotAllowed(response, allowed) { sendJson(response, 405, workbenchError("method_not_allowed", `Use ${allowed} for this Workbench read model route.`)); } function workbenchError(code, message, extra = {}) { return { ok: false, status: "failed", error: { code, message, ...extra }, valuesRedacted: true, secretMaterialStored: false }; } function boundedLimit(value) { return Math.min(MAX_PAGE_LIMIT, parsePositiveInteger(value, DEFAULT_PAGE_LIMIT)); } function boundedSessionListLimit(value) { return Math.min(MAX_PAGE_LIMIT, parsePositiveInteger(value, DEFAULT_SESSION_LIST_LIMIT)); } 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); } function cursorFromOffset(offset) { return `idx:${Math.max(0, Math.trunc(Number(offset) || 0))}`; } function nonNegativeInteger(value, fallback) { const parsed = Number.parseInt(value ?? "", 10); return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback; } function eventSeq(event, index) { const seq = Number(event?.seq); return Number.isFinite(seq) && seq > 0 ? Math.trunc(seq) : index + 1; } function normalizeStatus(value) { const text = textValue(value).toLowerCase().replace(/_/gu, "-"); if (text === "cancelled") return "canceled"; return text || "unknown"; } function normalizeTerminalStatus(value) { const status = normalizeStatus(value); return TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status) ? status : null; } function firstUserPreview(messages) { return messages.find((message) => message.role === "user")?.textPreview ?? null; } function latestUserPreview(messages) { return [...messages].reverse().find((message) => message.role === "user")?.textPreview ?? null; } function latestValidMessagePreview(messages) { return [...messages].reverse().find((message) => message.role === "user" || isAssistantLikeRole(message.role))?.textPreview ?? null; } function latestAssistantPreview(messages) { return [...messages].reverse().find((message) => isAssistantLikeRole(message.role))?.textPreview ?? null; } function sessionTitleFromMessages(messages = []) { return boundedPreviewText(latestUserPreview(messages) ?? firstUserPreview(messages) ?? latestAssistantPreview(messages)); } function sessionPreviewFromMessages(messages = []) { return boundedPreviewText(latestValidMessagePreview(messages) ?? firstUserPreview(messages) ?? latestAssistantPreview(messages)); } function boundedPreviewText(value, maxLength = 160) { const text = textValue(value).replace(/\s+/gu, " ").trim(); if (!text) return null; return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text; } function isAssistantLikeRole(role) { return role === "assistant" || role === "agent"; } function objectValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } function textValue(value) { return String(value ?? "").trim(); } function messageAuthorityTextValue(value) { const text = String(value ?? "").replace(/\r\n?/gu, "\n"); if (!text.trim() || text.trim() === "[object Object]") return ""; return text; } function safeTurnId(value) { const text = textValue(value); return /^turn_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null; } function safeMessageId(value) { const text = textValue(value); return /^msg_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null; } function safePartId(value) { const text = textValue(value); return /^prt_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null; } function hash(value) { return createHash("sha256").update(String(value)).digest("hex"); }