Files
pikasTech-HWLAB/internal/cloud/server-workbench-read-http.ts
T

1431 lines
63 KiB
TypeScript

/*
* 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 } from "./workbench-realtime-authority.ts";
import { workbenchRealtimeCapabilities } from "./workbench-realtime-capabilities.ts";
import { durableTraceStatus, RUNNING_STATUSES, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
import { emitCodeAgentOtelSpan, emitHttpServerRequestSpan } from "./otel-trace.ts";
import * as workbenchFacts from "./server-workbench-facts.ts";
const DEFAULT_PAGE_LIMIT = 50;
const DEFAULT_SESSION_LIST_LIMIT = 20;
const MAX_PAGE_LIMIT = 100;
const WORKBENCH_SESSION_LIST_PAGE_FAMILIES = Object.freeze(["sessions"]);
const WORKBENCH_SESSION_DETAIL_FAMILIES = Object.freeze(["sessions", "messages", "parts", "turns", "checkpoints"]);
const WORKBENCH_SESSION_MESSAGE_PAGE_FAMILIES = Object.freeze(["sessions", "messages", "parts", "turns", "checkpoints"]);
const WORKBENCH_TRACE_EVENT_METADATA_FAMILIES = Object.freeze(["sessions", "messages", "parts", "turns", "checkpoints"]);
const WORKBENCH_TRACE_EVENT_PAGE_FAMILIES = Object.freeze(["traceEvents"]);
function attachWorkbenchReadModelDiagnosticOtel(request, fields = {}) {
const context = request?.hwlabHttpRequestContext;
if (!context) return;
const code = textValue(fields.code) || "workbench_read_model_not_found";
const route = textValue(fields.route) || context.route || "/v1/workbench";
context.route = route;
context.lastHttpError = { code, category: "workbench_read_model", layer: "workbench.read_model" };
context.otelAttributes = {
...(context.otelAttributes ?? {}),
"workbench.route": route,
"workbench.read_model.route": route,
"workbench.read_model.result": code,
"workbench.read_model.count": Number.isFinite(Number(fields.count)) ? Math.trunc(Number(fields.count)) : null,
"workbench.session_id": fields.sessionId ?? null,
"workbench.trace_id": fields.traceId ?? null,
"workbench.turn_id": fields.turnId ?? null,
traceId: fields.traceId ?? null,
"error.code": code,
"error.category": "workbench_read_model",
"error.layer": "workbench.read_model"
};
}
const {
factSessionSummary,
factSessionDetail,
factMessagesForSession,
canonicalFactMessageGroupsForSession,
factMessageCanonicalGroupKey,
selectCanonicalFactMessage,
factMessageCanonicalScore,
factMessageHasFinalResponsePart,
workbenchLifecycleMessageId,
factMessageDto,
factMessageAuthorityText,
factPartsHaveFinalResponse,
factSyntheticTerminalFinalResponse,
isTerminalProjectionStatus,
firstFactPartText,
factMessageFinalResponseText,
factTerminalMessageForTrace,
factTerminalStatusFromMessageProjection,
factPartDto,
factTurnForTrace,
factTurnSnapshot,
factTraceSnapshot,
factTraceTimingProjection,
factTraceEventIsTerminalAuthority,
factTraceEventTerminalTimes,
factTraceEventDto,
factProjectionForTrace,
factTimingProjection,
factCombinedTimingProjection,
factTimingSource,
firstTimestampIso,
latestTimestampIso,
elapsedFactMs,
timestampIso,
factCheckpointForTrace,
workbenchProjectionStoreError,
nonNegativeIntegerOrNull,
factSessionId,
factLastTraceId,
factLatestTraceIdForSession,
factCurrentTraceForSession,
factMessageTraceCandidate,
factTraceCandidate,
compareTraceCandidatesDesc,
nonUnknownStatus,
factTurnId,
factUpdatedAt,
factSeq,
factProjectedSeq,
normalizeProjectionStatus,
normalizeProjectionHealth,
compareFactSessionsDesc,
compareFactMessagesAsc,
compareFactMessagesForSessionAsc,
factMessageTimelineKey,
factTimelineAnchorMessageForTrace,
factMessageTimelineRoleRank,
firstFiniteNumber,
compareOptionalNumberAsc,
compareFactPartsAsc,
compareFactTraceEventsAsc,
compareFactRecordsDesc,
compareTimestampAsc,
compareTimestampDesc,
compareOptionalTimestampAsc,
compareNumberAsc,
compareNumberDesc,
compareText,
timestampMs,
optionalTimestampMs,
numericValue,
mergeFactSets,
emptyFactSet,
factArray,
uniqueText,
eventSeq,
normalizeStatus,
normalizeTerminalStatus,
firstUserPreview,
latestUserPreview,
latestValidMessagePreview,
latestAssistantPreview,
sessionTitleFromMessages,
sessionPreviewFromMessages,
boundedPreviewText,
isAssistantLikeRole,
objectValue,
textValue,
messageAuthorityTextValue,
safeTurnId,
safeMessageId,
safePartId,
hash
} = workbenchFacts;
export async function handleWorkbenchReadModelHttp(request, response, url, options = {}) {
try {
const perf = options.backendPerformance;
const auth = perf ? await perf.measure("workbench_auth", () => authenticateWorkbenchRead(request, response, options)) : await authenticateWorkbenchRead(request, response, options);
if (!auth) return;
if (url.pathname === "/v1/workbench/sync") {
const capabilities = workbenchRealtimeCapabilities(options.env ?? process.env);
if (!capabilities.projectionRealtime) {
sendJson(response, 503, {
ok: false,
error: {
code: "workbench_projection_realtime_disabled",
message: "Workbench projection sync/replay capability is disabled.",
capabilities,
valuesRedacted: true
}
});
return;
}
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 function recordWorkbenchTurnReadMetric(options = {}, url, statusCode, body = {}, startedAt = nowMs()) {
const performanceStore = options.backendPerformanceStore ?? options.backendPerformance;
if (!performanceStore?.recordWorkbenchTurnRead) return;
performanceStore.recordWorkbenchTurnRead({
route: url?.pathname ?? "/v1/workbench/turns/:id",
status: body?.status ?? statusClassLabel(statusCode),
degradedReason: workbenchTurnDegradedReason(body),
durationMs: nowMs() - startedAt,
responseBytes: responseBodyBytes(body),
timedOut: statusCode === 504 || body?.error?.code === "timeout"
});
}
export function recordWorkbenchCacheMetric(options = {}, url, body = {}, startedAt = nowMs()) {
const performanceStore = options.backendPerformanceStore ?? options.backendPerformance;
if (!performanceStore?.recordWorkbenchCache) return;
const cache = body?.cache && typeof body.cache === "object" ? body.cache : null;
if (!cache) return;
performanceStore.recordWorkbenchCache({
route: url?.pathname ?? "/v1/workbench",
cacheKeyClass: cache.class,
cacheStatus: cache.status,
operation: "read",
durationMs: nowMs() - startedAt,
payloadBytes: cache.payloadBytes,
cacheAgeMs: cache.cacheAgeMs,
dbQueryAvoided: cache.dbQueryAvoided,
freshnessSloMs: cache.freshnessSloMs,
projectionSeq: cache.projectionSeq
});
}
export function recordWorkbenchProjectionMetric(options = {}, { result = null, trace = null, projection = null, diagnostic = null } = {}) {
const performanceStore = options.backendPerformanceStore ?? options.backendPerformance;
if (!performanceStore?.recordWorkbenchProjection || !projection) return;
const sourceLatestSeq = workbenchProjectionSourceLatestSeq(result, trace, projection);
const lastProjectedSeq = Number(diagnostic?.lastProjectedSeq ?? projection?.lastProjectedSeq ?? 0);
if (!Number.isFinite(sourceLatestSeq) && !Number.isFinite(lastProjectedSeq)) return;
performanceStore.recordWorkbenchProjection({
sourceLatestSeq: Number.isFinite(sourceLatestSeq) ? sourceLatestSeq : lastProjectedSeq,
lastProjectedSeq: Number.isFinite(lastProjectedSeq) ? lastProjectedSeq : 0,
sourceLatestEventAt: workbenchProjectionSourceLatestAt(result, trace, projection),
projectedLatestEventAt: diagnostic?.updatedAt ?? projection?.updatedAt ?? trace?.updatedAt ?? result?.updatedAt ?? null,
terminalSourceAt: workbenchTerminalSourceAt(result, projection),
terminalProjectedAt: diagnostic?.projectionStatus === "caught-up" ? diagnostic?.updatedAt ?? projection?.updatedAt ?? trace?.updatedAt ?? result?.updatedAt ?? null : null,
status: projection?.status ?? "unknown",
reason: workbenchProjectionReason(diagnostic),
projectionStatus: diagnostic?.projectionStatus ?? "unknown",
source: "workbench_read_model"
});
}
export function workbenchProjectionSourceLatestSeq(result = null, trace = null, projection = null) {
const candidates = [
result?.agentRun?.lastSeq,
result?.traceSummary?.agentRun?.lastSeq,
result?.traceSummary?.lastSeq,
result?.providerTrace?.lastSeq,
result?.runnerTrace?.lastEvent?.sourceSeq,
result?.runnerTrace?.lastEvent?.seq,
result?.runnerTrace?.eventCount,
trace?.lastSeq,
trace?.lastEvent?.sourceSeq,
trace?.lastEvent?.seq,
trace?.eventCount,
projection?.lastProjectedSeq,
projection?.eventCount
];
for (const value of candidates) {
const seq = Number(value);
if (Number.isFinite(seq) && seq >= 0) return Math.floor(seq);
}
return NaN;
}
export function workbenchProjectionSourceLatestAt(result = null, trace = null, projection = null) {
return result?.traceSummary?.updatedAt
?? result?.agentRun?.updatedAt
?? result?.providerTrace?.updatedAt
?? result?.runnerTrace?.updatedAt
?? trace?.updatedAt
?? result?.updatedAt
?? projection?.updatedAt
?? null;
}
export function workbenchTerminalSourceAt(result = null, projection = null) {
if (projection?.terminal !== true) return null;
return result?.traceSummary?.updatedAt
?? result?.agentRun?.updatedAt
?? result?.updatedAt
?? projection?.updatedAt
?? null;
}
export function workbenchProjectionReason(diagnostic = null) {
if (diagnostic?.blocker) return diagnostic.blocker.code ?? "projection_blocked";
if (diagnostic?.projectionStatus === "projecting") return "projecting";
if (diagnostic?.projectionStatus === "caught-up") return "none";
return diagnostic?.projectionStatus ?? "unknown";
}
export function workbenchTurnDegradedReason(body = {}) {
if (body?.projection?.blocker?.code) return body.projection.blocker.code;
if (body?.blocker?.code) return body.blocker.code;
if (body?.error?.code) return body.error.code;
if (body?.projectionStatus === "projecting") return "projecting";
if (body?.projectionStatus === "blocked") return "projection_blocked";
return "none";
}
export function responseBodyBytes(body = {}) {
try { return Buffer.byteLength(JSON.stringify(body)); } catch { return 0; }
}
export function statusClassLabel(value) {
const status = Number(value);
if (!Number.isFinite(status) || status <= 0) return "unknown";
return `${Math.floor(status / 100)}xx`;
}
export async function authenticateWorkbenchRead(request, response, options) {
const access = options.accessController;
if (!access?.authenticate) {
sendJson(response, 503, workbenchError("workbench_access_controller_missing", "Workbench read model requires access controller."));
return null;
}
await access.ensureBootstrap?.();
const auth = await access.authenticate(request, { required: true });
if (!auth?.ok) {
sendJson(response, auth?.status ?? 401, auth ?? workbenchError("auth_required", "Authentication is required."));
return null;
}
return auth;
}
export async function handleWorkbenchSessionList(request, response, url, options, actor) {
const startedAt = nowMs();
if (url.searchParams.has("projectId") || url.searchParams.has("workspaceId")) return sendJson(response, 400, workbenchError("workbench_authority_removed", "Workbench session list is keyed by sessionId only."));
const limit = boundedSessionListLimit(url.searchParams.get("limit"));
const offset = cursorOffset(url.searchParams.get("cursor") ?? url.searchParams.get("after"));
const includeSessionId = safeSessionId(url.searchParams.get("includeSessionId"));
const includeRouteId = includeSessionId ?? safeConversationId(url.searchParams.get("includeSessionId"));
const runtime = workbenchRuntimeReader(request, options);
if (typeof runtime?.listWorkbenchSessions !== "function") {
if (typeof runtime?.queryWorkbenchFacts !== "function") {
throw runtimeDependencyError("workbench_runtime_unconfigured", "Workbench runtime service is required for session list.", false);
}
const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
const payload = await handleWorkbenchSessionListFromFacts(readModel, url, options, actor, { limit, offset, includeRouteId });
recordWorkbenchCacheMetric(options, url, payload, startedAt);
emitWorkbenchSessionListReadOtel(request, options, {
startTimeMs: startedAt,
statusCode: 200,
source: "facts",
limit,
offset,
includeSessionId: includeRouteId,
payload
});
return sendJson(response, 200, payload);
}
const payload = await runtime.listWorkbenchSessions({
limit,
offset,
cursor: url.searchParams.get("cursor") ?? url.searchParams.get("after") ?? null,
includeSessionId: includeRouteId ?? null,
actor: { id: actor.id, role: actor.role ?? "user", valuesRedacted: true }
});
recordWorkbenchCacheMetric(options, url, payload, startedAt);
emitWorkbenchSessionListReadOtel(request, options, {
startTimeMs: startedAt,
statusCode: 200,
source: "runtime",
limit,
offset,
includeSessionId: includeRouteId,
payload
});
return sendJson(response, 200, payload);
}
export async function handleWorkbenchSessionListFromFacts(readModel, url, options, actor, { limit, offset, includeRouteId }) {
const perf = options.backendPerformance;
const pageQuery = { ownerUserId: actor.role === "admin" ? undefined : actor.id, limit: limit + offset + 1, families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES, sessionsOrder: "updated_desc", sessionProjection: "summary" };
const includeQueryPromise = includeRouteId ? queryWorkbenchSessionInclude(readModel, includeRouteId, perf) : null;
const pageResult = await (perf ? perf.measure("workbench_session_page_query", () => readModel.queryFacts(pageQuery)) : readModel.queryFacts(pageQuery));
if (pageResult.error) throw pageResult.error;
let pageFacts = pageResult.facts;
let naturalPage = visibleFactSessions(pageFacts, actor).slice(offset, offset + limit + 1);
if (includeRouteId && !naturalPage.some((session) => factSessionMatchesRouteId(session, includeRouteId))) {
const includeResult = await includeQueryPromise;
if (includeResult.error) throw includeResult.error;
const included = visibleFactSessions(includeResult.facts, actor)[0] ?? null;
if (included) {
pageFacts = mergeFactSets(pageFacts, includeResult.facts);
naturalPage = [included, ...naturalPage];
}
}
const pageSessions = naturalPage.slice(0, limit);
const summaryResult = await (perf ? perf.measure("workbench_session_summary_query", () => queryFactsForSessionSummaries(readModel, pageSessions)) : queryFactsForSessionSummaries(readModel, pageSessions));
if (summaryResult.error) throw summaryResult.error;
pageFacts = mergeFactSets(pageFacts, summaryResult.facts);
const summaries = pageSessions.map((session) => factSessionSummary(session, pageFacts)).filter(Boolean);
const hasMore = naturalPage.length > limit;
return {
ok: true,
status: "succeeded",
contractVersion: "workbench-sessions-v1",
sessions: summaries,
count: summaries.length,
cursor: offset > 0 ? cursorFromOffset(offset) : null,
hasMore,
nextCursor: hasMore ? cursorFromOffset(offset + limit) : null,
cache: pageResult.cache ?? summaryResult.cache ?? null,
valuesRedacted: true,
secretMaterialStored: false
};
}
export function runtimeDependencyError(code, message, retryable = true) {
const error = new Error(message || code);
error.name = "WorkbenchRuntimeDependencyError";
error.code = code;
error.data = { serviceId: "hwlab-workbench-runtime", retryable, transient: retryable, valuesRedacted: true };
return error;
}
export function workbenchRuntimeReader(request, options) {
return options.workbenchRuntime
?? options.runtimeStore
?? createWorkbenchRuntimeClient({ env: options.env ?? process.env, fetch: options.fetch, logger: options.logger, traceparent: request?.hwlabHttpRequestContext?.traceparent });
}
export function workbenchRuntimeFactsReadModel(request, options, actor) {
const runtime = workbenchRuntimeReader(request, options);
if (typeof runtime?.queryWorkbenchFacts !== "function") {
throw runtimeDependencyError("workbench_runtime_unconfigured", "Workbench runtime service is required for Workbench facts reads.", false);
}
return {
async queryFacts(query = {}) {
try {
return await runtime.queryWorkbenchFacts({
...query,
actor: { id: actor.id, role: actor.role ?? "user", valuesRedacted: true }
});
} catch (error) {
return { facts: emptyFactSet(), count: 0, durable: true, error: workbenchRuntimeFactsQueryError(error), persistence: null };
}
}
};
}
export function workbenchRuntimeFactsQueryError(error) {
const normalized = error instanceof Error ? error : new Error(String(error ?? "Workbench facts query failed."));
const data = objectValue(normalized.data);
normalized.data = {
...data,
retryable: data.retryable !== false,
transient: data.transient !== false,
retryAfterMs: data.retryAfterMs ?? 5000,
valuesRedacted: true
};
return normalized;
}
export function queryWorkbenchSessionInclude(readModel, includeRouteId, perf = null) {
const run = () => queryFactsByRouteId(readModel, includeRouteId, { families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES });
const promise = perf ? perf.measure("workbench_session_include_query", run) : run();
return promise.catch((error) => ({ error }));
}
export async function queryFactsByRouteId(readModel, routeId, query = {}) {
const baseQuery = { ...query, limit: query.limit ?? MAX_PAGE_LIMIT };
const sessionId = safeSessionId(routeId);
if (sessionId) return readModel.queryFacts({ ...baseQuery, sessionId });
const conversationId = safeConversationId(routeId);
if (conversationId) {
const sessionResult = await readModel.queryFacts({ ...baseQuery, families: WORKBENCH_SESSION_LIST_PAGE_FAMILIES, conversationId });
if (sessionResult.error || queryRequestsOnlySessionFacts(baseQuery)) return sessionResult;
const resolvedSessionId = factSessionId(factArray(sessionResult.facts?.sessions)[0] ?? null);
if (!resolvedSessionId) return sessionResult;
const relatedResult = await readModel.queryFacts({ ...baseQuery, sessionId: resolvedSessionId });
if (relatedResult.error) return relatedResult;
const facts = mergeFactSets(sessionResult.facts, relatedResult.facts);
return { facts, count: Object.values(facts).reduce((sum, rows) => sum + rows.length, 0), durable: relatedResult.durable ?? sessionResult.durable, persistence: relatedResult.persistence ?? sessionResult.persistence ?? null };
}
return { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
}
export function queryRequestsOnlySessionFacts(query = {}) {
const families = Array.isArray(query.families) ? query.families : [];
return families.length === 1 && families[0] === "sessions";
}
export async function queryFactsForSessionSummaries(readModel, sessions = []) {
const sessionIds = uniqueText(sessions.map(factSessionId));
if (sessionIds.length === 0 && uniqueText(sessions.map(factLastTraceId)).length === 0) return { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
// Session list summary is hot and is also sampled by the two-page web-probe
// observer. Avoid parallel fan-out here so one list request cannot consume
// multiple durable-read connections while another page is refreshing.
const bySession = sessionIds.length > 0
? await readModel.queryFacts({ sessionIds, families: ["messages", "parts", "turns"] })
: { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
if (bySession.error) return bySession;
const traceIds = summaryTraceIds(sessions, bySession.facts);
const byTrace = traceIds.length > 0
? await readModel.queryFacts({ traceIds, families: ["checkpoints"] })
: { facts: emptyFactSet(), count: 0, durable: false, persistence: null };
if (byTrace.error) return byTrace;
const facts = mergeFactSets(bySession.facts, byTrace.facts);
return { facts, count: Object.values(facts).reduce((sum, rows) => sum + rows.length, 0), durable: bySession.durable ?? byTrace.durable, persistence: bySession.persistence ?? byTrace.persistence ?? null };
}
export function summaryTraceIds(sessions = [], facts = {}) {
return uniqueText([
...sessions.map(factLastTraceId),
...sessions.map((session) => factCurrentTraceForSession(facts, factSessionId(session), session)?.traceId)
]);
}
export function visibleFactSessions(facts, actor) {
return hideShadowedAgentRunAliasSessions(factArray(facts?.sessions)
.filter((session) => canActorReadFactSession(session, actor))
.sort(compareFactSessionsDesc));
}
export function hideShadowedAgentRunAliasSessions(sessions = []) {
const canonicalTraceIds = new Set(sessions
.filter((session) => !isAgentRunAliasSessionId(factSessionId(session)))
.map(factLastTraceId)
.filter(Boolean));
if (canonicalTraceIds.size === 0) return sessions;
return sessions.filter((session) => !isAgentRunAliasSessionId(factSessionId(session)) || !canonicalTraceIds.has(factLastTraceId(session)));
}
export function isAgentRunAliasSessionId(sessionId) {
return /^ses_agentrun_/u.test(textValue(sessionId));
}
export function canActorReadFactSession(session, actor) {
if (!session || !actor) return false;
if (normalizeStatus(session.status) === "archived") return false;
if (actor.role === "admin") return true;
return session.ownerUserId === actor.id;
}
export async function visibleFactSessionForTrace(readModel, facts, actor, traceId) {
const visible = visibleFactSessions(facts, actor);
const byLastTrace = visible.find((item) => item.lastTraceId === traceId) ?? null;
if (byLastTrace) return { session: byLastTrace, facts };
const relatedSessionIds = uniqueText([
factTurnForTrace(facts, traceId)?.sessionId,
factArray(facts.messages).find((message) => message.traceId === traceId)?.sessionId,
factCheckpointForTrace(facts, traceId)?.sessionId
]);
const alreadyLoaded = visible.find((item) => relatedSessionIds.includes(factSessionId(item))) ?? null;
if (alreadyLoaded) return { session: alreadyLoaded, facts };
for (const sessionId of relatedSessionIds) {
const result = await queryFactsByRouteId(readModel, sessionId, { families: ["sessions"], limit: 1 });
if (result.error) return { error: result.error };
const session = visibleFactSessions(result.facts, actor)[0] ?? null;
if (session) return { session, facts: mergeFactSets(facts, result.facts) };
}
return { session: null, facts };
}
export function factSessionMatchesRouteId(session, routeId) {
const sessionId = safeSessionId(routeId);
if (sessionId && factSessionId(session) === sessionId) return true;
const conversationId = safeConversationId(routeId);
return Boolean(conversationId && session?.conversationId === conversationId);
}
export async function handleWorkbenchSessionDetail(request, response, url, options, actor, sessionId) {
const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
const includeMessages = includeMessagesForSessionDetail(url.searchParams);
const result = await queryFactsByRouteId(readModel, sessionId, { families: WORKBENCH_SESSION_DETAIL_FAMILIES });
if (result.error) return sendJson(response, 503, workbenchProjectionStoreError(result.error));
const session = visibleFactSessions(result.facts, actor)[0] ?? null;
if (!session) {
const body = workbenchError("workbench_session_not_found", "Workbench session is not visible to the current actor.", { sessionId });
attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/sessions/:id", code: "workbench_session_not_found", sessionId, count: result.count });
return sendJson(response, 404, body);
}
sendJson(response, 200, {
ok: true,
status: "found",
contractVersion: "workbench-session-detail-v1",
session: factSessionDetail(session, result.facts, { includeMessages }),
detailMode: includeMessages ? "full" : "metadata-only",
messagesIncluded: includeMessages,
valuesRedacted: true,
secretMaterialStored: false
});
}
export async function handleWorkbenchMessagePage(request, response, url, options, actor, sessionId) {
const startedAt = nowMs();
const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
const limit = boundedLimit(url.searchParams.get("limit"));
const cursorParam = url.searchParams.get("cursor") ?? url.searchParams.get("after");
const hasCursor = Boolean(textValue(cursorParam));
const result = await queryFactsByRouteId(readModel, sessionId, {
families: WORKBENCH_SESSION_MESSAGE_PAGE_FAMILIES,
...(hasCursor ? {} : { limit: limit + 1, messagesOrder: "updated_desc", partsOrder: "updated_desc", turnsOrder: "updated_desc", checkpointsOrder: "updated_desc" })
});
if (result.error) return sendJson(response, 503, workbenchProjectionStoreError(result.error));
const session = visibleFactSessions(result.facts, actor)[0] ?? null;
if (!session) {
const body = workbenchError("workbench_session_not_found", "Workbench session is not visible to the current actor.", { sessionId });
attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/sessions/:id/messages", code: "workbench_session_not_found", sessionId, count: result.count });
return sendJson(response, 404, body);
}
const messages = factMessagesForSession(session, result.facts);
const offset = hasCursor ? cursorOffset(cursorParam) : 0;
const page = messages.slice(offset, offset + limit);
const nextOffset = offset + page.length;
const resolvedSessionId = factSessionId(session);
const body = {
ok: true,
status: "succeeded",
contractVersion: "workbench-message-page-v1",
sessionId: resolvedSessionId,
messages: page,
count: page.length,
total: messages.length,
cursor: offset > 0 ? cursorFromOffset(offset) : null,
nextCursor: nextOffset < messages.length ? cursorFromOffset(nextOffset) : null,
hasMore: nextOffset < messages.length,
timelineDigest: workbenchTimelineDigest(page),
roleSequencePrefix: messagesRoleSequence(page).slice(0, 32),
adjacentSameRoleCount: adjacentSameRoleCount(page),
traceIds: uniqueText(page.map((message) => message?.traceId)).slice(0, 16),
valuesRedacted: true,
secretMaterialStored: false
};
emitWorkbenchSessionMessagesReadOtel(request, options, {
startTimeMs: startedAt,
statusCode: 200,
sessionId: resolvedSessionId,
limit,
offset,
messages: page,
total: messages.length,
hasMore: body.hasMore
});
sendJson(response, 200, body);
}
export async function handleWorkbenchTurnSnapshot(request, response, url, options, actor, rawTurnId) {
const startedAt = nowMs();
const queryTraceId = safeTraceId(url.searchParams.get("traceId"));
const traceId = safeTraceId(rawTurnId) ?? queryTraceId;
const turnId = safeTurnId(rawTurnId) || traceId || rawTurnId;
if (!traceId) {
const body = workbenchError("invalid_turn_id", "turnId must currently be a trace id or include traceId query.", { turnId });
recordWorkbenchTurnReadMetric(options, url, 400, body, startedAt);
return sendJson(response, 400, body);
}
const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
const result = await readModel.queryFacts({ traceId, limit: MAX_PAGE_LIMIT, families: [...WORKBENCH_TRACE_EVENT_METADATA_FAMILIES, ...WORKBENCH_TRACE_EVENT_PAGE_FAMILIES] });
if (result.error) {
const body = workbenchProjectionStoreError(result.error);
emitWorkbenchTurnStatusReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 503,
phase: "query_failed",
turnId,
errorCode: result.error?.code ?? "workbench_turn_query_failed"
}, result.error);
recordWorkbenchTurnReadMetric(options, url, 503, body, startedAt);
return sendJson(response, 503, body);
}
const session = visibleFactSessions(result.facts, actor).find((item) => item.lastTraceId === traceId) ?? visibleFactSessions(result.facts, actor)[0] ?? null;
const turn = factTurnForTrace(result.facts, traceId, turnId);
const found = Boolean(session || turn);
if (!found) {
const body = workbenchError("workbench_turn_not_found", "Workbench turn is not visible to the current actor.", { turnId, traceId });
attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/turns/:id", code: "workbench_turn_not_found", traceId, turnId, count: result.count });
emitWorkbenchTurnStatusReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 404,
phase: "not_found",
turnId,
readCount: result.count,
errorCode: "workbench_turn_not_found"
});
recordWorkbenchTurnReadMetric(options, url, 404, body, startedAt);
return sendJson(response, 404, body);
}
const projection = factProjectionForTrace(result.facts, traceId);
const snapshot = factTurnSnapshot({ turn, session, facts: result.facts, traceId, turnId });
const status = snapshot.status;
const body = {
ok: true,
status,
contractVersion: "workbench-turn-snapshot-v1",
turn: snapshot,
projection,
cache: result.cache ?? null,
projectionStatus: projection.projectionStatus,
projectionHealth: projection.projectionHealth,
lastProjectedSeq: projection.lastProjectedSeq,
sourceRunId: projection.sourceRunId,
sourceCommandId: projection.sourceCommandId,
staleMs: projection.staleMs,
blocker: projection.blocker,
valuesRedacted: true,
secretMaterialStored: false
};
recordWorkbenchProjectionMetric(options, {
projection: { ...projection, status, eventCount: snapshot.trace?.eventCount ?? projection.lastProjectedSeq ?? 0 },
diagnostic: projection
});
emitWorkbenchTurnStatusReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 200,
phase: "succeeded",
status,
terminal: snapshot.terminal,
sessionId: snapshot.sessionId,
turnId: snapshot.turnId,
traceEventCount: snapshot.trace?.eventCount ?? null,
latestProjectedSeq: projection.lastProjectedSeq,
projectionStatus: projection.projectionStatus,
projectionHealth: projection.projectionHealth,
sourceRunId: projection.sourceRunId,
sourceCommandId: projection.sourceCommandId,
readCount: result.count
});
recordWorkbenchTurnReadMetric(options, url, 200, body, startedAt);
recordWorkbenchCacheMetric(options, url, body, startedAt);
sendJson(response, 200, body);
}
export async function handleWorkbenchTraceEventPage(request, response, url, options, actor, rawTraceId) {
const startedAt = nowMs();
const traceId = safeTraceId(rawTraceId);
if (!traceId) return sendJson(response, 400, workbenchError("invalid_trace_id", "traceId must start with trc_.", { traceId: rawTraceId }));
const readModel = workbenchRuntimeFactsReadModel(request, options, actor);
const pageOptions = tracePageOptions(url);
const metadata = await readModel.queryFacts({ traceId, families: WORKBENCH_TRACE_EVENT_METADATA_FAMILIES, limit: 1 });
if (metadata.error) {
emitWorkbenchTraceEventsReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 503,
phase: "metadata_query_failed",
pageOptions,
errorCode: metadata.error?.code ?? "workbench_trace_metadata_query_failed"
}, metadata.error);
return sendJson(response, 503, workbenchProjectionStoreError(metadata.error));
}
const resolved = await visibleFactSessionForTrace(readModel, metadata.facts, actor, traceId);
if (resolved.error) {
emitWorkbenchTraceEventsReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 503,
phase: "session_lookup_failed",
pageOptions,
metadataCount: metadata.count,
errorCode: resolved.error?.code ?? "workbench_trace_session_lookup_failed"
}, resolved.error);
return sendJson(response, 503, workbenchProjectionStoreError(resolved.error));
}
const session = resolved.session;
const metadataFacts = resolved.facts ?? metadata.facts;
if (!session) {
const context = await readModel.queryFacts({ traceId, families: ["sessions", "turns", "checkpoints"], limit: MAX_PAGE_LIMIT });
if (context.error) {
emitWorkbenchTraceEventsReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 503,
phase: "context_query_failed",
pageOptions,
metadataCount: metadata.count,
errorCode: context.error?.code ?? "workbench_trace_context_query_failed"
}, context.error);
return sendJson(response, 503, workbenchProjectionStoreError(context.error));
}
const contextSessions = visibleFactSessions(context.facts, actor);
const contextSession = contextSessions.find((item) => item.lastTraceId === traceId) ?? contextSessions[0] ?? null;
const contextTurn = factTurnForTrace(context.facts, traceId, traceId);
if (contextSession || contextTurn) {
const projection = factProjectionForTrace(context.facts, traceId);
const blocker = traceEventsReadModelBlocker("workbench_trace_metadata_missing", "Workbench trace metadata is missing from the trace events read model.", { traceId, projection, session: contextSession, turn: contextTurn, route: url.pathname });
attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/traces/:id/events", code: "workbench_trace_metadata_missing", sessionId: factSessionId(contextSession), traceId, count: context.count });
emitWorkbenchTraceEventsReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 404,
phase: "metadata_missing",
pageOptions,
projection,
sessionId: factSessionId(contextSession),
turnId: factTurnId(contextTurn, traceId),
readCount: context.count,
metadataCount: metadata.count,
errorCode: "workbench_trace_metadata_missing"
});
return sendJson(response, 404, workbenchTraceEventsReadModelError(blocker, { traceId, projection, session: contextSession, turn: contextTurn }));
}
attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/traces/:id/events", code: "workbench_trace_not_found", traceId, count: context.count });
emitWorkbenchTraceEventsReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 404,
phase: "not_found",
pageOptions,
readCount: context.count,
metadataCount: metadata.count,
errorCode: "workbench_trace_not_found"
});
return sendJson(response, 404, workbenchError("workbench_trace_not_found", "Workbench trace is not visible to the current actor.", { traceId }));
}
const projection = factProjectionForTrace(metadataFacts, traceId);
const pageResult = await readModel.queryFacts({ traceId, families: WORKBENCH_TRACE_EVENT_PAGE_FAMILIES, afterProjectedSeq: pageOptions.afterProjectedSeq, limit: pageOptions.limit + 1 });
if (pageResult.error) {
emitWorkbenchTraceEventsReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 503,
phase: "events_query_failed",
pageOptions,
projection,
sessionId: factSessionId(session),
metadataCount: metadata.count,
errorCode: pageResult.error?.code ?? "workbench_trace_events_query_failed"
}, pageResult.error);
return sendJson(response, 503, workbenchProjectionStoreError(pageResult.error));
}
const traceTurn = factTurnForTrace(metadataFacts, traceId, traceId);
const turnTraceStatus = normalizeStatus(traceTurn?.status);
const traceStatus = turnTraceStatus !== "unknown" ? turnTraceStatus : durableTraceStatus(pageResult.facts.traceEvents);
const page = traceEventPageFromFacts(pageResult.facts.traceEvents, pageOptions, { total: projection.lastProjectedSeq, traceLastSeq: projection.lastProjectedSeq, traceStatus });
const missingTraceEvents = traceEventPageMissing(page, projection, pageOptions);
const missingTraceEventsDiagnostic = missingTraceEvents
? traceEventsReadModelBlocker("workbench_trace_events_missing", "Workbench trace events are still catching up in the durable read model.", { traceId, projection, session, turn: traceTurn, route: url.pathname })
: null;
if (missingTraceEvents) {
attachWorkbenchReadModelDiagnosticOtel(request, { route: "/v1/workbench/traces/:id/events", code: "workbench_trace_events_missing", sessionId: factSessionId(session), traceId, count: pageResult.count });
}
const responseProjection = page.blocker ? blockedTraceEventProjection(projection, page.blocker) : missingTraceEvents ? catchingUpTraceEventProjection(projection, missingTraceEventsDiagnostic) : projection;
const body = {
ok: true,
status: page.blocker ? "blocked" : missingTraceEvents ? "projecting" : "succeeded",
contractVersion: "workbench-trace-events-v1",
traceId,
sessionId: factSessionId(session),
threadId: safeOpaqueId(session?.threadId) ?? (textValue(session?.threadId) || null),
...page,
events: factArray(page.events).map((event) => ({ ...event, detailProjection: true, authority: "trace-detail-only" })),
projection: responseProjection,
detailProjection: true,
authority: "trace-detail-only",
realtimeAuthority: "workbench-realtime-authority-v2",
cache: pageResult.cache ?? metadata.cache ?? null,
projectionStatus: responseProjection.projectionStatus,
projectionHealth: responseProjection.projectionHealth,
lastProjectedSeq: responseProjection.lastProjectedSeq,
sourceRunId: responseProjection.sourceRunId,
sourceCommandId: responseProjection.sourceCommandId,
staleMs: responseProjection.staleMs,
blocker: responseProjection.blocker,
timing: responseProjection.timing,
startedAt: responseProjection.startedAt,
lastEventAt: responseProjection.lastEventAt,
finishedAt: responseProjection.finishedAt,
durationMs: responseProjection.durationMs,
diagnostic: responseProjection.diagnostic ?? null,
valuesRedacted: true,
secretMaterialStored: false
};
recordWorkbenchCacheMetric(options, url, body, startedAt);
emitWorkbenchTraceEventsReadOtel(traceId, options, {
startTimeMs: startedAt,
statusCode: 200,
phase: page.blocker ? "blocked" : missingTraceEvents ? "events_catching_up" : "succeeded",
pageOptions,
page,
projection: responseProjection,
sessionId: body.sessionId,
turnId: factTurnId(traceTurn, traceId),
readCount: pageResult.count,
rawEventCount: factArray(pageResult.facts?.traceEvents).length,
metadataCount: metadata.count,
traceStatus
});
sendJson(response, 200, body);
}
export function emitWorkbenchTurnStatusReadOtel(traceId, options = {}, fields = {}, error = null) {
const safeId = safeTraceId(traceId);
if (!safeId) return;
const statusCode = Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : 0;
void emitCodeAgentOtelSpan("turn_status_read", safeId, options.env ?? process.env, {
startTimeMs: fields.startTimeMs,
status: statusCode >= 500 || error ? "error" : "ok",
error,
attributes: {
"http.method": "GET",
"http.route": "/v1/workbench/turns/:id",
"http.status_code": statusCode || null,
phase: fields.phase ?? null,
status: fields.status ?? null,
terminal: typeof fields.terminal === "boolean" ? fields.terminal : null,
sessionId: fields.sessionId ?? null,
turnId: fields.turnId ?? safeId,
readCount: nonNegativeInteger(fields.readCount),
traceEventCount: nonNegativeInteger(fields.traceEventCount),
latestProjectedSeq: nonNegativeInteger(fields.latestProjectedSeq),
projectionStatus: fields.projectionStatus ?? null,
projectionHealth: fields.projectionHealth ?? null,
sourceRunId: fields.sourceRunId ?? null,
sourceCommandId: fields.sourceCommandId ?? null,
errorCode: fields.errorCode ?? null,
valuesRedacted: true
}
});
}
export function emitWorkbenchTraceEventsReadOtel(traceId, options = {}, fields = {}, error = null) {
const safeId = safeTraceId(traceId);
if (!safeId) return;
const page = fields.page ?? {};
const range = page.range ?? {};
const projection = fields.projection ?? {};
const pageOptions = fields.pageOptions ?? {};
const statusCode = Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : 0;
const returnedEvents = Array.isArray(page.events) ? page.events.length : nonNegativeInteger(fields.returnedEvents);
const afterProjectedSeq = nonNegativeInteger(pageOptions.afterProjectedSeq ?? range.afterProjectedSeq);
const limit = nonNegativeInteger(pageOptions.limit ?? range.limit);
const hasMore = typeof page.hasMore === "boolean" ? page.hasMore : null;
void emitCodeAgentOtelSpan("trace_events_read", safeId, options.env ?? process.env, {
startTimeMs: fields.startTimeMs,
status: statusCode >= 500 || error ? "error" : "ok",
error,
attributes: {
"http.method": "GET",
"http.route": "/v1/workbench/traces/:id/events",
"http.status_code": statusCode || null,
phase: fields.phase ?? null,
sessionId: fields.sessionId ?? null,
turnId: fields.turnId ?? safeId,
returnedEvents,
rawEventCount: nonNegativeInteger(fields.rawEventCount),
readCount: nonNegativeInteger(fields.readCount),
metadataCount: nonNegativeInteger(fields.metadataCount),
sinceSeq: afterProjectedSeq,
afterProjectedSeq,
limit,
fromSeq: nonNegativeInteger(range.fromProjectedSeq),
toSeq: nonNegativeInteger(range.toProjectedSeq),
totalEvents: nonNegativeInteger(range.total ?? page.eventCount),
hasMore,
fullTraceLoaded: typeof page.fullTraceLoaded === "boolean" ? page.fullTraceLoaded : hasMore === null ? null : !hasMore && !page.blocker,
traceLastSeq: nonNegativeInteger(page.traceLastSeq ?? projection.lastProjectedSeq),
projectionStatus: projection.projectionStatus ?? null,
projectionHealth: projection.projectionHealth ?? null,
sourceRunId: projection.sourceRunId ?? null,
sourceCommandId: projection.sourceCommandId ?? null,
traceStatus: fields.traceStatus ?? page.traceStatus ?? null,
errorCode: fields.errorCode ?? null,
valuesRedacted: true
}
});
}
export function emitWorkbenchSessionListReadOtel(request, options = {}, fields = {}, error = null) {
const payload = fields.payload && typeof fields.payload === "object" ? fields.payload : {};
const sessions = Array.isArray(payload.sessions) ? payload.sessions : [];
const fallbackTitleCount = sessions.filter(sessionSummaryNeedsTitleFallback).length;
const traceIds = uniqueText(sessions.map((session) => session?.lastTraceId)).slice(0, 8);
const businessTraceId = (safeTraceId(traceIds[0]) ?? textValue(fields.includeSessionId)) || "workbench_session_list";
void emitCodeAgentOtelSpan("session_list_read", businessTraceId, options.env ?? process.env, {
startTimeMs: fields.startTimeMs,
status: error ? "error" : "ok",
error,
attributes: {
"http.method": "GET",
"http.route": "/v1/workbench/sessions",
"http.status_code": Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : null,
source: fields.source ?? null,
limit: nonNegativeInteger(fields.limit),
offset: nonNegativeInteger(fields.offset),
includeSessionId: fields.includeSessionId ?? null,
sessionCount: sessions.length,
fallbackTitleCount,
fallbackTitleRatio: sessions.length > 0 ? Number((fallbackTitleCount / sessions.length).toFixed(4)) : 0,
emptyPreviewCount: sessions.filter((session) => !sessionPreviewText(session)).length,
sessionIds: uniqueText(sessions.map((session) => session?.sessionId)).slice(0, 8).join(","),
traceIds: traceIds.join(","),
hasMore: payload.hasMore === true,
valuesRedacted: true
}
});
}
export function emitWorkbenchSessionMessagesReadOtel(request, options = {}, fields = {}, error = null) {
const messages = Array.isArray(fields.messages) ? fields.messages : [];
const traceIds = uniqueText(messages.map((message) => message?.traceId)).slice(0, 8);
const businessTraceId = (safeTraceId(traceIds[0]) ?? textValue(fields.sessionId)) || "workbench_session_messages";
const roleSequence = messagesRoleSequence(messages);
void emitCodeAgentOtelSpan("session_messages_read", businessTraceId, options.env ?? process.env, {
startTimeMs: fields.startTimeMs,
status: error ? "error" : "ok",
error,
attributes: {
"http.method": "GET",
"http.route": "/v1/workbench/sessions/:id/messages",
"http.status_code": Number.isInteger(Number(fields.statusCode)) ? Number(fields.statusCode) : null,
sessionId: fields.sessionId ?? null,
limit: nonNegativeInteger(fields.limit),
offset: nonNegativeInteger(fields.offset),
returnedMessages: messages.length,
totalMessages: nonNegativeInteger(fields.total),
hasMore: fields.hasMore === true,
roleSequencePrefix: roleSequence.slice(0, 32),
consecutiveUserPrefix: consecutiveRolePrefix(messages, "user"),
adjacentSameRoleCount: adjacentSameRoleCount(messages),
timelineDigest: workbenchTimelineDigest(messages),
userCount: messages.filter((message) => normalizeRole(message?.role) === "user").length,
agentCount: messages.filter((message) => isAssistantLikeRole(message?.role)).length,
traceIds: traceIds.join(","),
valuesRedacted: true
}
});
}
export function messagesRoleSequence(messages = []) {
return messages.map((message) => roleSequenceSymbol(message?.role)).join("");
}
export function workbenchTimelineDigest(messages = []) {
const payload = messages.map((message) => {
const terminal = isTerminalProjectionStatus(normalizeStatus(message?.status)) || Boolean(message?.finishedAt);
return {
messageId: message?.messageId ?? null,
role: normalizeRole(message?.role),
sessionId: message?.sessionId ?? null,
traceId: message?.traceId ?? null,
turnId: message?.turnId ?? null,
status: normalizeStatus(message?.status),
partIds: factArray(message?.parts).map((part) => part?.partId ?? null),
textHash: textValue(message?.text || message?.textPreview) ? hash(textValue(message?.text || message?.textPreview)).slice(0, 16) : null,
startedAt: message?.startedAt ?? null,
finishedAt: terminal ? message?.finishedAt ?? null : null,
durationMs: terminal && Number.isFinite(Number(message?.durationMs)) ? Math.trunc(Number(message.durationMs)) : null
};
});
return `sha256:${hash(JSON.stringify(payload))}`;
}
export function sessionSummaryNeedsTitleFallback(session) {
if (!session || typeof session !== "object") return true;
return !sessionPreviewText(session);
}
export function sessionPreviewText(session) {
return textValue(
session?.firstUserMessagePreview
?? session?.snapshot?.firstUserMessagePreview
?? session?.userPreview
?? session?.title
?? session?.name
);
}
export function roleSequenceSymbol(role) {
const normalized = normalizeRole(role);
if (normalized === "user") return "U";
if (normalized === "agent" || normalized === "assistant") return "A";
if (normalized === "system") return "S";
return "?";
}
export function normalizeRole(role) {
return textValue(role).toLowerCase();
}
export function consecutiveRolePrefix(messages = [], role) {
let count = 0;
for (const message of messages) {
if (normalizeRole(message?.role) !== role) break;
count += 1;
}
return count;
}
export function adjacentSameRoleCount(messages = []) {
let count = 0;
for (let index = 1; index < messages.length; index += 1) {
const previous = normalizeRole(messages[index - 1]?.role);
if (previous && previous === normalizeRole(messages[index]?.role)) count += 1;
}
return count;
}
export function traceEventPageFromFacts(sourceEvents, options, metadata = {}) {
const rows = factArray(sourceEvents).filter((event) => factProjectedSeq(event)).sort(compareFactTraceEventsAsc);
const collision = projectedSeqCollision(rows);
if (collision) return blockedTraceEventPage(options, metadata, collision);
const pageRows = rows.slice(0, options.limit);
const events = pageRows.map(factTraceEventDto).filter(Boolean);
const toProjectedSeq = events.length ? events.at(-1).projectedSeq : null;
const hasMore = rows.length > options.limit;
const total = Number.isFinite(Number(metadata.total)) ? Math.trunc(Number(metadata.total)) : Math.max(options.afterProjectedSeq, toProjectedSeq ?? options.afterProjectedSeq);
const traceLastSeq = Number.isFinite(Number(metadata.traceLastSeq)) ? Math.trunc(Number(metadata.traceLastSeq)) : total;
return {
events,
eventCount: total ?? Math.max(options.afterProjectedSeq, toProjectedSeq ?? options.afterProjectedSeq),
range: {
afterProjectedSeq: options.afterProjectedSeq,
fromProjectedSeq: events.length ? events[0].projectedSeq : null,
toProjectedSeq,
limit: options.limit,
returned: events.length,
total
},
hasMore,
fullTraceLoaded: !hasMore && (toProjectedSeq ?? options.afterProjectedSeq) >= traceLastSeq,
traceLastSeq,
nextProjectedSeq: events.length ? toProjectedSeq : options.afterProjectedSeq,
nextCursor: hasMore && toProjectedSeq ? `projected:${toProjectedSeq}` : null,
traceStatus: metadata.traceStatus ?? "unknown",
updatedAt: events.at(-1)?.updatedAt ?? events.at(-1)?.createdAt ?? null
};
}
export function projectedSeqCollision(rows = []) {
const seen = new Map();
for (const row of rows) {
const projectedSeq = factProjectedSeq(row);
if (!projectedSeq) continue;
const id = textValue(row?.id ?? row?.sourceEventId) || null;
const existing = seen.get(projectedSeq);
if (existing && existing.id !== id) {
return {
code: "projected_seq_collision",
category: "workbench-projection",
layer: "workbench-trace-events",
message: "Trace projection contains duplicate projectedSeq values; canonical event order is blocked until projection is rebuilt.",
projectedSeq,
eventIds: [existing.id, id].filter(Boolean),
retryable: false,
valuesPrinted: false
};
}
if (!existing) seen.set(projectedSeq, { id });
}
return null;
}
export function blockedTraceEventPage(options, metadata = {}, blocker) {
const total = Number.isFinite(Number(metadata.total)) ? Math.trunc(Number(metadata.total)) : null;
return {
events: [],
eventCount: total ?? options.afterProjectedSeq,
range: {
afterProjectedSeq: options.afterProjectedSeq,
fromProjectedSeq: null,
toProjectedSeq: null,
limit: options.limit,
returned: 0,
total
},
hasMore: false,
nextProjectedSeq: options.afterProjectedSeq,
nextCursor: null,
traceStatus: metadata.traceStatus ?? "unknown",
updatedAt: null,
blocker
};
}
export function blockedTraceEventProjection(projection = {}, blocker) {
return {
...projection,
projectionStatus: "blocked",
projectionHealth: "degraded",
blocker,
valuesRedacted: true
};
}
export function catchingUpTraceEventProjection(projection = {}, diagnostic = null) {
const status = projection.projectionStatus === "caught-up" || !projection.projectionStatus ? "projecting" : projection.projectionStatus;
const health = projection.projectionHealth === "caught-up" || projection.projectionHealth === "healthy" || !projection.projectionHealth ? "projecting" : projection.projectionHealth;
return {
...projection,
projectionStatus: status,
projectionHealth: health,
blocker: null,
diagnostic: diagnostic ? { ...diagnostic, projectionStatus: status, projectionHealth: health } : null,
valuesRedacted: true
};
}
export function traceEventPageMissing(page = {}, projection = {}, options = {}) {
if (page.blocker) return false;
const expectedSeq = Number(projection?.lastProjectedSeq);
if (!Number.isFinite(expectedSeq) || expectedSeq <= 0) return false;
return factArray(page.events).length === 0 && expectedSeq > Number(options.afterProjectedSeq ?? 0);
}
export function traceEventsReadModelBlocker(code, message, { traceId, projection = {}, session = null, turn = null, route = null } = {}) {
return {
code,
message,
userMessage: message,
layer: "workbench-read-model",
category: "trace-events",
retryable: true,
route,
traceId,
sessionId: factSessionId(session) ?? turn?.sessionId ?? null,
turnId: factTurnId(turn, traceId),
projectionStatus: projection.projectionStatus ?? null,
projectionHealth: "degraded",
lastProjectedSeq: projection.lastProjectedSeq ?? null,
sourceRunId: projection.sourceRunId ?? null,
sourceCommandId: projection.sourceCommandId ?? null,
retryAfterMs: 5000,
valuesPrinted: false
};
}
export function workbenchTraceEventsReadModelError(blocker, { traceId, projection = {}, session = null, turn = null } = {}) {
const responseProjection = blockedTraceEventProjection(projection, blocker);
return workbenchError(blocker.code, blocker.message, {
traceId,
sessionId: blocker.sessionId ?? factSessionId(session) ?? turn?.sessionId ?? null,
turnId: blocker.turnId ?? factTurnId(turn, traceId),
route: blocker.route ?? null,
layer: blocker.layer,
category: blocker.category,
retryable: blocker.retryable,
projection: responseProjection,
projectionStatus: responseProjection.projectionStatus,
projectionHealth: responseProjection.projectionHealth,
lastProjectedSeq: responseProjection.lastProjectedSeq,
sourceRunId: responseProjection.sourceRunId,
sourceCommandId: responseProjection.sourceCommandId,
blocker,
diagnostic: {
contractVersion: "hwlab-error-diagnostic-v1",
route: blocker.route ?? null,
layer: blocker.layer,
category: blocker.category,
code: blocker.code,
httpStatus: 404,
source: "server",
retryable: blocker.retryable,
valuesPrinted: false
}
});
}
export function projectionText(...values) {
for (const value of values) {
if (value && typeof value === "object") {
const nested = messageAuthorityTextValue(value.text ?? value.content ?? value.message ?? value.summary ?? value.preview ?? value.title);
if (nested) return nested;
continue;
}
const text = messageAuthorityTextValue(value);
if (text) return text;
}
return null;
}
export function partFact(part, index, messageId, traceId) {
const type = textValue(part?.type) || "text";
const text = messageAuthorityTextValue(part?.text ?? part?.content ?? part?.message);
return {
partId: safePartId(part?.partId ?? part?.id) || `prt_${hash(`${messageId}:${index}:${type}`).slice(0, 24)}`,
messageId,
traceId,
type,
text: text || null,
status: normalizeStatus(part?.status ?? "completed"),
toolName: textValue(part?.toolName ?? part?.name) || null,
createdAt: textValue(part?.createdAt ?? part?.timestamp) || null,
valuesRedacted: part?.valuesRedacted !== false
};
}
export function tracePageOptions(url) {
const cursor = textValue(url.searchParams.get("cursor"));
const cursorProjectedSeq = cursor.startsWith("projected:") ? Number.parseInt(cursor.slice("projected:".length), 10) : NaN;
const afterProjectedSeq = Number.isInteger(cursorProjectedSeq) && cursorProjectedSeq >= 0
? cursorProjectedSeq
: nonNegativeInteger(url.searchParams.get("afterProjectedSeq"), 0);
return { afterProjectedSeq, limit: boundedLimit(url.searchParams.get("limit")) };
}
export function handleWorkbenchReadModelFailure(response, url, options = {}, error) {
const classified = classifyWorkbenchReadModelFailure(error);
const route = workbenchReadRouteTemplate(url?.pathname ?? "/v1/workbench");
logWorkbenchReadModelFailure(options.logger ?? console, {
event: "workbench_read_model_route_failed",
ok: false,
route,
statusCode: classified.statusCode,
errorCode: classified.code,
errorName: error?.name ?? "Error",
message: error instanceof Error ? error.message : String(error ?? "unknown"),
valuesRedacted: true
});
if (response.headersSent || response.destroyed) return;
sendJson(response, classified.statusCode, workbenchError(classified.code, classified.message, {
route,
errorName: error?.name ?? "Error",
retryable: classified.retryable === true,
transient: classified.transient === true,
serviceId: classified.serviceId ?? null,
runtimeStatus: classified.runtimeStatus ?? null
}));
}
export function classifyWorkbenchReadModelFailure(error) {
const message = String(error?.message ?? error ?? "");
const code = String(error?.code ?? "").toLowerCase();
const runtimeStatus = Number(error?.data?.status ?? 0);
const runtimePayloadError = error?.data?.payload?.error ?? null;
if (error?.name === "WorkbenchRuntimeDependencyError" || code.startsWith("workbench_runtime_")) {
const retryable = error?.data?.retryable === true || error?.data?.transient === true || runtimePayloadError?.retryable === true;
return {
statusCode: retryable || (Number.isFinite(runtimeStatus) && runtimeStatus >= 500) ? 503 : 500,
code: String(runtimePayloadError?.code ?? error?.code ?? "workbench_runtime_dependency_failed"),
message: String(runtimePayloadError?.message ?? error?.message ?? "Workbench runtime dependency failed."),
retryable,
transient: retryable,
serviceId: String(error?.data?.serviceId ?? "hwlab-workbench-runtime"),
runtimeStatus: Number.isFinite(runtimeStatus) && runtimeStatus > 0 ? runtimeStatus : null
};
}
if (/timeout|connection terminated|terminating connection|econnreset|econnrefused|etimedout/iu.test(message) || /timeout|econnreset|econnrefused|etimedout/u.test(code)) {
return {
statusCode: 503,
code: "workbench_read_model_store_unavailable",
message: "Workbench read model store is temporarily unavailable."
};
}
return {
statusCode: 500,
code: "workbench_read_model_failed",
message: "Workbench read model failed."
};
}
export function workbenchReadRouteTemplate(pathname) {
const path = String(pathname ?? "");
if (path === "/v1/workbench/sessions") return path;
if (/^\/v1\/workbench\/sessions\/[^/]+\/messages$/u.test(path)) return "/v1/workbench/sessions/:id/messages";
if (/^\/v1\/workbench\/sessions\/[^/]+$/u.test(path)) return "/v1/workbench/sessions/:id";
if (/^\/v1\/workbench\/turns\/[^/]+$/u.test(path)) return "/v1/workbench/turns/:id";
if (/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(path)) return "/v1/workbench/traces/:id/events";
return path || "/v1/workbench";
}
export function logWorkbenchReadModelFailure(logger, payload) {
try {
const line = JSON.stringify(payload);
if (typeof logger?.error === "function") logger.error(line);
else if (typeof logger?.warn === "function") logger.warn(line);
} catch {
// Read-model failure logging must not affect response generation.
}
}
export function methodNotAllowed(response, allowed) {
sendJson(response, 405, workbenchError("method_not_allowed", `Use ${allowed} for this Workbench read model route.`));
}
export function workbenchError(code, message, extra = {}) {
return {
ok: false,
status: "failed",
error: { code, message, ...extra },
valuesRedacted: true,
secretMaterialStored: false
};
}
export function boundedLimit(value) {
return Math.min(MAX_PAGE_LIMIT, parsePositiveInteger(value, DEFAULT_PAGE_LIMIT));
}
export function boundedSessionListLimit(value) {
return Math.min(MAX_PAGE_LIMIT, parsePositiveInteger(value, DEFAULT_SESSION_LIST_LIMIT));
}
export function cursorOffset(value) {
const text = textValue(value);
if (!text) return 0;
if (text.startsWith("idx:")) return nonNegativeInteger(text.slice(4), 0);
return nonNegativeInteger(text, 0);
}
export function cursorFromOffset(offset) {
return `idx:${Math.max(0, Math.trunc(Number(offset) || 0))}`;
}
export function nonNegativeInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
}
export function nowMs() {
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
return Date.now();
}