604 lines
30 KiB
TypeScript
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();
|
|
}
|