Files
pikasTech-HWLAB/internal/cloud/workbench-projection-writer.ts
T

604 lines
30 KiB
TypeScript

/*
* SPEC: PJ2026-0104010803 Workbench唯一投影; HWLAB#2464 transactional Kafka projector.
* Responsibility: atomically promote durable admission into initial nonterminal Workbench facts and build pure Kafka projection facts.
* Local admission may create the first running session/message/turn snapshot; all later lifecycle and terminal authority comes only from commitAgentRunKafkaProjection.
*/
import { createHash } from "node:crypto";
import { safeTraceId } from "./server-http-utils.ts";
import { normalizeWorkbenchStatus, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
export async function recordWorkbenchSessionOwner({ accessController, traceId, ownerUserId, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, preserveLastTraceId = false } = {}) {
if (!ownerUserId || typeof accessController?.recordAgentSessionOwner !== "function") return null;
return accessController.recordAgentSessionOwner({
ownerUserId,
ownerRole,
sessionId,
projectId,
conversationId,
threadId,
traceId: preserveLastTraceId ? undefined : safeTraceId(traceId),
status,
session
});
}
export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null, session = null, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, status = "idle", now = new Date().toISOString() } = {}) {
if (typeof runtimeStore?.writeWorkbenchSessionAdmissionFact !== "function") {
throw workbenchProjectionError("workbench_admission_store_unconfigured", "Workbench admission requires the durable session admission store.");
}
const fact = buildWorkbenchSessionAdmissionFact({ session, ownerUserId, ownerRole, projectId, conversationId, threadId, status, now });
return runtimeStore.writeWorkbenchSessionAdmissionFact({ fact }, {
sessionId: fact.sessionId,
ownerUserId: fact.ownerUserId,
projectId: fact.projectId,
valuesPrinted: false
});
}
export async function promoteWorkbenchTurnAdmission({ runtimeStore = null, session = null, inputFact = null, payload = {}, params = {}, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, now = new Date().toISOString() } = {}) {
if (typeof runtimeStore?.promoteWorkbenchTurnAdmission !== "function") {
throw workbenchProjectionError("workbench_admission_promotion_store_unconfigured", "Workbench admission promotion requires the durable atomic promotion store.");
}
if (!inputFact || typeof inputFact !== "object") {
throw workbenchProjectionError("workbench_admission_input_required", "Workbench admission promotion requires an input fact.");
}
const facts = buildWorkbenchAdmissionPromotionFacts({ session, inputFact, payload, params, ownerUserId, ownerRole, projectId, conversationId, threadId, now });
const sessionFact = facts.sessions[0];
return runtimeStore.promoteWorkbenchTurnAdmission({ facts }, {
sessionId: sessionFact.sessionId,
traceId: inputFact.traceId ?? null,
turnId: inputFact.turnId ?? null,
messageId: inputFact.messageId ?? null,
ownerUserId: sessionFact.ownerUserId,
projectId: sessionFact.projectId,
valuesPrinted: false
});
}
export function buildWorkbenchAdmissionPromotionFacts({ session = null, inputFact = null, payload = {}, params = {}, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, now = new Date().toISOString() } = {}) {
const sessionFact = buildWorkbenchSessionAdmissionFact({ session, ownerUserId, ownerRole, projectId, conversationId, threadId, status: "running", now });
const traceId = safeTraceId(payload.traceId ?? params.traceId ?? inputFact?.traceId);
if (!traceId) throw workbenchProjectionError("workbench_admission_trace_required", "Workbench admission promotion requires traceId.");
const turnId = textValue(payload.turnId ?? params.turnId ?? inputFact?.turnId) || traceId;
const userMessageId = textValue(payload.userMessageId ?? params.userMessageId ?? inputFact?.messageId) || eventProjectionUserMessageId(traceId, {});
const assistantMessageId = textValue(payload.assistantMessageId ?? params.assistantMessageId) || eventProjectionAssistantMessageId(traceId, {});
const sourceEventId = `${traceId}:admission-promotion`;
const occurredAt = timestampValue(payload.createdAt ?? now);
const timing = eventTimingProjection({ startedAt: occurredAt, lastEventAt: occurredAt, terminal: false });
const userText = textValue(params.message ?? params.prompt ?? params.text);
const common = {
sessionId: sessionFact.sessionId,
turnId,
traceId,
projectedSeq: 0,
sourceSeq: 0,
sourceEventId,
terminal: false,
sealed: false,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
finishedAt: null,
durationMs: null,
createdAt: occurredAt,
updatedAt: occurredAt,
valuesPrinted: false
};
const userMessage = { ...common, messageId: userMessageId, role: "user", status: "sent", text: userText };
const assistantMessage = { ...common, messageId: assistantMessageId, role: "agent", status: "running", text: "" };
const turn = {
...common,
messageId: assistantMessageId,
status: "running",
finalResponse: null,
assistantText: "",
failureKind: null,
diagnostic: {
authority: "local-admission-promotion",
admissionState: "promoted",
sourceRunId: textValue(payload.agentRun?.runId) || null,
sourceCommandId: textValue(payload.agentRun?.commandId) || null,
dispatchIntentId: textValue(payload.agentRun?.dispatchIntentId) || null,
runnerJobId: textValue(payload.agentRun?.runnerJobId) || null,
valuesRedacted: true
}
};
return {
inputs: [{
...inputFact,
status: "promoted",
commandId: textValue(payload.agentRun?.commandId ?? inputFact.commandId) || null,
runId: textValue(payload.agentRun?.runId) || null,
dispatchIntentId: textValue(payload.agentRun?.dispatchIntentId) || null,
runnerJobId: textValue(payload.agentRun?.runnerJobId) || null,
durableDispatch: payload.agentRun?.durableDispatch === true,
updatedAt: occurredAt,
valuesRedacted: true,
secretMaterialStored: false
}],
sessions: [{
...sessionFact,
status: "running",
lastTraceId: traceId,
sessionJson: {
...sessionFact.sessionJson,
admissionState: "promoted",
agentRun: payload.agentRun ?? sessionFact.sessionJson?.agentRun ?? null,
valuesRedacted: true,
secretMaterialStored: false
},
updatedAt: occurredAt
}],
messages: [userMessage, assistantMessage],
parts: userText ? messagePartFacts(userMessage) : [],
turns: [turn],
traceEvents: [],
checkpoints: []
};
}
export function buildWorkbenchSessionAdmissionFact({ session = null, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, status = "idle", now = new Date().toISOString() } = {}) {
if (!session || typeof session !== "object") {
throw workbenchProjectionError("workbench_admission_session_required", "Workbench admission requires a session record.");
}
const sessionId = textValue(session.id ?? session.sessionId);
if (!sessionId) {
throw workbenchProjectionError("workbench_admission_session_required", "Workbench admission requires sessionId.");
}
const createdAt = timestampValue(session.startedAt ?? session.createdAt ?? session.session?.createdAt ?? now);
const updatedAt = timestampValue(session.updatedAt ?? createdAt);
const normalizedStatus = normalizeWorkbenchStatus(session.status ?? session.session?.sessionStatus ?? status) || "idle";
const sessionJson = workbenchSessionJsonWithLaunchContext(plainObjectValue(session.session) ?? {}, { payload: session });
return {
sessionId,
ownerUserId: textValue(session.ownerUserId ?? ownerUserId) || null,
ownerRole: textValue(session.ownerRole ?? ownerRole) || null,
projectId: textValue(session.projectId ?? projectId) || null,
agentId: textValue(session.agentId) || "hwlab-code-agent",
conversationId: textValue(session.conversationId ?? conversationId) || null,
threadId: textValue(session.threadId ?? threadId) || null,
status: normalizedStatus,
lastTraceId: safeTraceId(session.lastTraceId ?? session.traceId) || null,
projectedSeq: 0,
sourceSeq: 0,
sourceEventId: `${sessionId}:session-admission`,
terminal: false,
sealed: false,
sessionJson: {
...sessionJson,
sessionStatus: normalizedStatus,
source: textValue(sessionJson.source ?? session.session?.source) || "session-admission",
providerProfile: textValue(sessionJson.providerProfile ?? session.session?.providerProfile) || null,
valuesRedacted: true,
secretMaterialStored: false
},
createdAt,
updatedAt,
valuesRedacted: true
};
}
export function buildWorkbenchProjectionEventFacts({ event = {}, requestMeta = {}, previousCheckpoint = null, projectedSeq, projectedAt = new Date().toISOString() } = {}) {
const traceId = safeTraceId(event.traceId ?? requestMeta.traceId);
if (!traceId) throw workbenchProjectionError("workbench_projection_trace_required", "Kafka projection event requires traceId.");
const durableProjectedSeq = nonNegativeInteger(projectedSeq);
if (durableProjectedSeq <= 0) throw workbenchProjectionError("workbench_projection_seq_required", "Kafka projection event requires an allocated projectedSeq.");
const sourceSeq = nonNegativeInteger(event.sourceSeq ?? event.seq);
const sessionId = textValue(event.sessionId ?? requestMeta.sessionId) || null;
const turnId = textValue(event.turnId) || traceId;
const eventType = textValue(event.eventType ?? event.type ?? event.label) || "event";
const sourceTerminal = event.terminal === true || TERMINAL_STATUSES.has(normalizeWorkbenchStatus(event.status));
const sourceStatus = sourceTerminal ? normalizeWorkbenchStatus(event.status) : "running";
const previousTiming = normalizeTimingProjection(previousCheckpoint?.timing) ?? normalizeTimingProjection(previousCheckpoint);
const sourceOccurredAt = timestampValue(event.createdAt ?? event.occurredAt ?? event.updatedAt ?? projectedAt);
const occurredAt = latestTimestamp(previousTiming?.lastEventAt, sourceOccurredAt) ?? sourceOccurredAt;
if (checkpointIsTerminal(previousCheckpoint)) {
return {
written: false,
suppressedAfterSeal: true,
traceId,
sessionId,
sourceSeq,
sourceEventId: workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt }),
projectedSeq: nonNegativeInteger(previousCheckpoint?.projectedSeq) || durableProjectedSeq,
valuesPrinted: false
};
}
const explicitEventTiming = normalizeTimingProjection(event.timing) ?? normalizeTimingProjection(event);
const explicitDurationMs = durationValue(event.durationMs ?? explicitEventTiming?.durationMs);
const explicitStartedAt = optionalTimestampValue(event.startedAt ?? event.traceStartedAt ?? event.runnerStartedAt ?? explicitEventTiming?.startedAt);
const provisionalLastEventAt = latestTimestamp(previousTiming?.lastEventAt, explicitEventTiming?.lastEventAt, occurredAt);
const provisionalFinishedAt = sourceTerminal ? latestTimestamp(explicitEventTiming?.finishedAt, optionalTimestampValue(event.finishedAt), provisionalLastEventAt, occurredAt) : null;
const startedAt = previousTiming?.startedAt ?? explicitStartedAt ?? startedAtFromDuration(provisionalFinishedAt, explicitDurationMs) ?? (sourceSeq <= 1 ? occurredAt : null);
const lastEventAt = provisionalLastEventAt;
const finishedAt = sourceTerminal ? latestTimestamp(provisionalFinishedAt, lastEventAt) : null;
const sourceEventId = workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt });
const eventId = textValue(event.id) || stableFactId("wte", { traceId, sourceEventId });
const previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText);
const liveAssistantText = isAssistantMessageTraceEvent(event) ? finalResponseTextValue(event.assistantText, event.text, event.reply, event.message) : finalResponseTextValue(event.assistantText, event.reply);
const eventTerminalFinalResponse = sourceTerminal ? terminalFinalResponse(sourceStatus, event, { evidence: event, finalResponse: event.finalResponse }) : null;
const messageFinalText = sourceTerminal ? finalResponseTextValue(event.finalResponse, liveAssistantText, previousFinalText, eventTerminalFinalResponse) : null;
const checkpointAssistantText = messageFinalText ?? liveAssistantText ?? previousFinalText ?? null;
const checkpointFinalResponse = sourceTerminal && messageFinalText
? { ...(eventTerminalFinalResponse ?? {}), text: messageFinalText, status: sourceStatus, traceId, source: "agentrun-kafka-projector", valuesPrinted: false }
: previousCheckpoint?.finalResponse ?? null;
const terminalSealBlocked = sourceTerminal && sourceStatus === "completed" && !messageFinalText;
const effectiveTerminal = sourceTerminal && !terminalSealBlocked;
const effectiveStatus = effectiveTerminal ? sourceStatus : "running";
const timing = eventTimingProjection({ startedAt, lastEventAt, finishedAt, terminal: effectiveTerminal, durationMs: explicitDurationMs ?? previousTiming?.durationMs });
const timingAuthorityIssue = terminalTimingAuthorityIssue(timing, { terminal: effectiveTerminal, traceId, source: "kafka", status: effectiveStatus, label: event.label, sourceSeq });
const projectionDiagnostic = { timingAuthorityIssue, terminalSealBlocked, sourceTerminal, sourceStatus, authority: "agentrun-kafka-projector", valuesRedacted: true };
const userMessageFact = sessionId && previousCheckpoint?.userMessage ? normalizeMessageFact(previousCheckpoint.userMessage, 0, { traceId, sessionId, turnId, terminal: false, terminalStatus: "sent", finalText: null, timestamp: projectedAt, timing }) : null;
const messageFact = sessionId ? normalizeMessageFact({
messageId: eventProjectionAssistantMessageId(traceId, event),
role: "agent",
status: effectiveStatus,
traceId,
turnId,
text: messageFinalText ?? liveAssistantText ?? previousFinalText ?? "",
projectedSeq: durableProjectedSeq,
sourceSeq,
sourceEventId,
terminal: effectiveTerminal,
sealed: effectiveTerminal,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
finishedAt: timing.finishedAt,
durationMs: timing.durationMs,
createdAt: occurredAt,
updatedAt: projectedAt,
source: "agentrun-kafka-projector"
}, userMessageFact ? 1 : 0, { traceId, sessionId, turnId, terminal: effectiveTerminal, terminalStatus: effectiveStatus, finalText: messageFinalText, terminalSealBlocked, timestamp: projectedAt, timing }) : null;
const facts = {
sessions: sessionId ? [{ sessionId, status: effectiveStatus, lastTraceId: traceId, projectedSeq: durableProjectedSeq, sourceSeq, sourceEventId, terminal: effectiveTerminal, sealed: effectiveTerminal, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, updatedAt: projectedAt }] : [],
traceEvents: [{ ...event, id: eventId, traceId, sessionId, turnId, messageId: textValue(event.messageId) || null, status: effectiveStatus, sourceSeq, sourceEventId, projectedSeq: durableProjectedSeq, eventType, terminal: effectiveTerminal, sealed: effectiveTerminal, diagnostic: projectionDiagnostic, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, createdAt: occurredAt, occurredAt, updatedAt: projectedAt }],
messages: [userMessageFact, messageFact].filter(Boolean),
parts: [userMessageFact, messageFact].filter(Boolean).flatMap((message) => messagePartFacts(message, { finalText: message === messageFact ? messageFinalText : null })),
turns: sessionId ? [{
turnId,
sessionId,
traceId,
messageId: messageFact?.messageId ?? null,
status: effectiveStatus,
projectedSeq: durableProjectedSeq,
sourceSeq,
sourceEventId,
terminal: effectiveTerminal,
sealed: effectiveTerminal,
finalResponse: checkpointFinalResponse,
assistantText: checkpointAssistantText,
failureKind: textValue(event.failureKind ?? event.errorCode) || null,
diagnostic: projectionDiagnostic,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
finishedAt: timing.finishedAt,
durationMs: timing.durationMs,
createdAt: occurredAt,
updatedAt: projectedAt
}] : [],
checkpoints: [{
traceId,
sessionId,
turnId,
runId: textValue(event.runId) || null,
commandId: textValue(event.commandId) || null,
projectedSeq: durableProjectedSeq,
sourceSeq,
sourceEventId,
projectionStatus: effectiveTerminal ? "caught_up" : "projecting",
projectionHealth: terminalSealBlocked ? "degraded" : "healthy",
terminal: effectiveTerminal,
sealed: effectiveTerminal,
assistantText: checkpointAssistantText,
finalResponse: checkpointFinalResponse,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
finishedAt: timing.finishedAt,
durationMs: timing.durationMs,
diagnostic: { ...projectionDiagnostic, lastEventLabel: textValue(event.label) || null, lastEventStatus: textValue(event.status) || null },
updatedAt: projectedAt
}]
};
return { written: true, suppressedAfterSeal: false, traceId, sessionId, sourceSeq, sourceEventId, projectedSeq: durableProjectedSeq, facts, terminal: effectiveTerminal, sourceTerminal, terminalSealBlocked, valuesPrinted: false };
}
function eventProjectionAssistantMessageId(traceId, event = {}) {
return eventProjectionMessageId(traceId, "agent", event);
}
function eventProjectionUserMessageId(traceId, event = {}) {
return eventProjectionMessageId(traceId, "user", event);
}
function eventProjectionMessageId(traceId, role, event = {}) {
const explicit = role === "user"
? textValue(event.userMessageId ?? event.userMessage?.messageId)
: textValue(event.messageId ?? event.assistantMessageId ?? event.assistantMessage?.messageId);
if (explicit) return explicit;
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 workbenchSessionJsonWithLaunchContext(session = {}, { payload = null, params = null } = {}) {
const base = plainObjectValue(session) ?? {};
const launchContext = firstPlainObjectValue(
base.launchContext,
base.sessionJson?.launchContext,
base.session?.launchContext,
payload?.launchContext,
payload?.sessionJson?.launchContext,
payload?.session?.launchContext,
params?.launchContext,
params?.sessionJson?.launchContext,
params?.session?.launchContext
);
if (!launchContext) return base;
const next = { ...base, launchContext: { ...launchContext, valuesRedacted: true }, valuesRedacted: true };
if (!Object.prototype.hasOwnProperty.call(next, "secretMaterialStored")) next.secretMaterialStored = false;
return next;
}
function firstPlainObjectValue(...values) {
for (const value of values) {
const object = plainObjectValue(value);
if (object) return object;
}
return null;
}
function plainObjectValue(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function workbenchProjectionError(code, message) {
const error = new Error(message);
error.code = code;
error.valuesRedacted = true;
return error;
}
function messagePartFacts(message = {}, { finalText = null } = {}) {
const messageId = textValue(message.messageId);
if (!messageId || !message.sessionId) return [];
const role = textValue(message.role) || "agent";
const terminal = message.terminal === true;
const finalResponseText = role !== "user" && terminal ? finalResponseTextValue(finalText, message.finalResponse) : null;
const text = finalResponseText ?? textValue(message.text);
if (!text) return [];
const partType = finalResponseText ? "final_response" : "text";
return [{
partId: stableFactId("wpt", { messageId, index: 0, partType }),
messageId,
sessionId: message.sessionId,
turnId: message.turnId,
traceId: message.traceId,
partIndex: 0,
partType,
status: message.status,
projectedSeq: message.projectedSeq,
sourceSeq: message.sourceSeq,
sourceEventId: message.sourceEventId,
terminal,
sealed: finalResponseText ? true : message.sealed,
text,
createdAt: message.createdAt,
updatedAt: message.updatedAt,
valuesPrinted: false
}];
}
function normalizeMessageFact(message = {}, index, { traceId, sessionId, conversationId, threadId, terminal, terminalStatus = "completed", finalText = null, terminalSealBlocked = false, timestamp, timing: contextTiming }) {
const role = textValue(message.role) || "agent";
const messageId = textValue(message.messageId ?? message.id) || (role === "user" ? eventProjectionUserMessageId(traceId, message) : eventProjectionAssistantMessageId(traceId, message));
if (!sessionId || !messageId) return null;
const messageTraceId = safeTraceId(message.traceId ?? message.turnId);
const appliesToContextTrace = role !== "user" && Boolean(traceId && (!messageTraceId || messageTraceId === traceId));
const contextTerminalStatus = normalizeWorkbenchStatus(terminalStatus) || "completed";
const status = appliesToContextTrace && terminalSealBlocked ? "running" : appliesToContextTrace && terminal ? contextTerminalStatus : normalizeWorkbenchStatus(message.status ?? (terminal && role !== "user" ? contextTerminalStatus : role === "user" ? "sent" : "running"));
const messageTerminal = role !== "user" && !(appliesToContextTrace && terminalSealBlocked) && TERMINAL_STATUSES.has(status);
const messageTiming = normalizeTimingProjection(message.timing) ?? normalizeTimingProjection(message);
const contextTimingProjection = appliesToContextTrace ? normalizeTimingProjection(contextTiming) : null;
const timing = role === "user" ? messageTiming ?? emptyTimingProjection() : contextTimingProjection ?? messageTiming ?? emptyTimingProjection();
const terminalCompleted = messageTerminal && status === "completed";
const text = appliesToContextTrace && terminal && finalText ? finalText : textValue(message.text ?? message.content ?? message.message);
return {
...message,
messageId,
sessionId,
conversationId,
threadId,
turnId: textValue(message.turnId) || traceId,
traceId: safeTraceId(message.traceId ?? traceId) ?? traceId,
role,
status,
projectedSeq: nonNegativeInteger(message.projectedSeq ?? message.seq ?? index + 1),
sourceSeq: nonNegativeInteger(message.sourceSeq ?? message.seq ?? index + 1),
sourceEventId: textValue(message.sourceEventId) || `${messageId}:0`,
terminal: messageTerminal,
sealed: messageTerminal,
text,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
finishedAt: timing.finishedAt,
durationMs: timing.durationMs,
createdAt: timestampValue(message.createdAt ?? timestamp),
updatedAt: timestampValue(message.updatedAt ?? timestamp),
...(terminalCompleted ? { error: null, blocker: null, projection: null, projectionStatus: null, projectionHealth: null, staleMs: null } : {}),
valuesPrinted: false
};
}
function finalResponseTextValue(...values) {
for (const value of values) {
if (value && typeof value === "object") {
const nested = finalResponseTextValue(value.text, value.content, value.message, value.summary, value.preview);
if (nested) return nested;
continue;
}
const text = textValue(value);
if (text) return text;
}
return null;
}
function checkpointIsTerminal(value) {
const source = value && typeof value === "object" ? value : null;
if (!source) return false;
if (source.terminal === true || source.sealed === true) return true;
const status = normalizeWorkbenchStatus(source.status ?? source.terminalStatus ?? source.traceStatus);
return TERMINAL_STATUSES.has(status);
}
function eventTimingProjection({ startedAt = null, lastEventAt = null, finishedAt = null, terminal = false, durationMs = null } = {}) {
const normalizedStartedAt = optionalTimestampValue(startedAt);
const normalizedLastEventAt = optionalTimestampValue(lastEventAt);
const normalizedFinishedAt = terminal ? optionalTimestampValue(finishedAt ?? normalizedLastEventAt) : null;
const normalizedDurationMs = durationValue(durationMs);
return {
startedAt: normalizedStartedAt,
lastEventAt: normalizedLastEventAt,
finishedAt: normalizedFinishedAt,
durationMs: terminal ? normalizedDurationMs ?? elapsedBetween(normalizedStartedAt, normalizedFinishedAt ?? normalizedLastEventAt) : null,
valuesRedacted: true
};
}
function normalizeTimingProjection(value) {
const source = value && typeof value === "object" ? value : null;
if (!source) return null;
const startedAt = optionalTimestampValue(source.startedAt);
const lastEventAt = optionalTimestampValue(source.lastEventAt);
const finishedAt = optionalTimestampValue(source.finishedAt);
const durationMs = durationValue(source.durationMs);
if (!startedAt && !lastEventAt && !finishedAt && durationMs === null) return null;
return { ...source, startedAt, lastEventAt, finishedAt, durationMs, valuesRedacted: source.valuesRedacted !== false };
}
function emptyTimingProjection() {
return { startedAt: null, lastEventAt: null, finishedAt: null, durationMs: null, valuesRedacted: true };
}
function durationValue(value) {
if (value === null || value === undefined || value === "") return null;
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? Math.trunc(number) : null;
}
function elapsedBetween(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 startedAtFromDuration(finishedAt, durationMs) {
const end = Date.parse(String(finishedAt ?? ""));
const duration = durationValue(durationMs);
if (!Number.isFinite(end) || duration === null) return null;
return new Date(Math.max(0, end - duration)).toISOString();
}
function terminalTimingAuthorityIssue(timing, { terminal = false, traceId = null, source = null, status = null, label = null, sourceSeq = null } = {}) {
if (!terminal) return null;
const startedAt = optionalTimestampValue(timing?.startedAt);
const finishedAt = optionalTimestampValue(timing?.finishedAt);
const durationMs = durationValue(timing?.durationMs);
if (startedAt && finishedAt && durationMs !== null) return null;
return {
code: "workbench_terminal_timing_authority_missing",
layer: "workbench-projection",
category: "timing-authority",
traceId,
source,
status,
label: textValue(label) || null,
sourceSeq: nonNegativeInteger(sourceSeq),
missingStartedAt: !startedAt,
missingFinishedAt: !finishedAt,
missingDurationMs: durationMs === null,
retryable: true,
message: "Workbench terminal projection is missing durable timing authority; the UI must surface this diagnostic instead of hiding elapsed time.",
valuesPrinted: false
};
}
function latestTimestamp(...values) {
let latest = null;
let latestMs = Number.NEGATIVE_INFINITY;
for (const value of values) {
const timestamp = optionalTimestampValue(value);
const ms = Date.parse(String(timestamp ?? ""));
if (!Number.isFinite(ms) || ms < latestMs) continue;
latest = timestamp;
latestMs = ms;
}
return latest;
}
function stableFactId(prefix, value) {
return `${prefix}_${createHash("sha256").update(JSON.stringify(sortJson(value))).digest("hex").slice(0, 32)}`;
}
function workbenchSourceEventId(event = {}, { traceId, sourceSeq, occurredAt } = {}) {
const explicit = textValue(event.sourceEventId ?? event.id);
if (explicit) return explicit;
if (isAssistantMessageTraceEvent(event)) {
const messageId = eventProjectionAssistantMessageId(traceId, event);
return `${safeTraceId(traceId) ?? traceId}:assistant-message:${messageId}`;
}
const source = textValue(event.source);
const label = textValue(event.label ?? event.type ?? event.eventType) || "event";
if (source && sourceSeq > 0) return `${source}:${sourceSeq}:${label}`;
return stableFactId("wte_source", {
traceId,
source: source || null,
sourceSeq,
label,
eventType: textValue(event.eventType ?? event.type) || null,
status: textValue(event.status) || null,
itemId: textValue(event.itemId ?? event.messageId) || null,
runId: textValue(event.runId) || null,
commandId: textValue(event.commandId) || null,
occurredAt
});
}
function isAssistantMessageTraceEvent(event = {}) {
const type = textValue(event?.eventType ?? event?.type).toLowerCase();
const label = textValue(event?.label).toLowerCase();
return type === "assistant" || /assistant:message|assistant_message/u.test(label);
}
function sortJson(value) {
if (Array.isArray(value)) return value.map(sortJson);
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]));
return value;
}
function nonNegativeInteger(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
}
function timestampValue(value) {
const ms = Date.parse(String(value ?? ""));
return Number.isFinite(ms) ? new Date(ms).toISOString() : new Date().toISOString();
}
function optionalTimestampValue(value) {
const ms = Date.parse(String(value ?? ""));
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
}
function textValue(value) {
return String(value ?? "").trim();
}