584 lines
22 KiB
TypeScript
584 lines
22 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
const DEFAULT_MAX_TRACES = 256;
|
|
const DEFAULT_MAX_EVENTS = 6000;
|
|
const TEXT_LIMIT = 1200;
|
|
const SECRET_PATTERN = /\b(?:sk-[A-Za-z0-9_-]+|OPENAI_API_KEY|DATABASE_URL|secretRef:[^\s,;]+|secret|token|password|passwd|credential|private[_ -]?key|kubeconfig|BEGIN [A-Z ]*PRIVATE KEY)\b/giu;
|
|
|
|
export function createCodeAgentTraceStore(options = {}) {
|
|
const maxTraces = positiveInteger(options.maxTraces, DEFAULT_MAX_TRACES);
|
|
const maxEvents = positiveInteger(options.maxEvents, DEFAULT_MAX_EVENTS);
|
|
const traces = new Map();
|
|
|
|
function ensure(traceId, meta = {}) {
|
|
const id = cleanTraceId(traceId) || `trc_${randomUUID()}`;
|
|
let trace = traces.get(id);
|
|
if (!trace) {
|
|
const timestamp = timestampFor(meta.now);
|
|
trace = {
|
|
traceId: id,
|
|
status: "running",
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp,
|
|
startedAt: timestamp,
|
|
finishedAt: null,
|
|
nextSeq: 1,
|
|
events: [],
|
|
assistantStreams: new Map(),
|
|
listeners: new Set(),
|
|
meta: {
|
|
runnerKind: meta.runnerKind ?? null,
|
|
workspace: meta.workspace ?? null,
|
|
sandbox: meta.sandbox ?? null,
|
|
sessionMode: meta.sessionMode ?? null,
|
|
implementationType: meta.implementationType ?? null
|
|
}
|
|
};
|
|
traces.set(id, trace);
|
|
prune();
|
|
} else {
|
|
trace.meta = {
|
|
...trace.meta,
|
|
...dropEmpty({
|
|
runnerKind: meta.runnerKind,
|
|
workspace: meta.workspace,
|
|
sandbox: meta.sandbox,
|
|
sessionMode: meta.sessionMode,
|
|
implementationType: meta.implementationType
|
|
})
|
|
};
|
|
}
|
|
return trace;
|
|
}
|
|
|
|
function append(traceId, event = {}, meta = {}) {
|
|
const trace = ensure(traceId, meta);
|
|
const normalized = normalizeTraceEvent(event, {
|
|
traceId: trace.traceId,
|
|
seq: trace.nextSeq,
|
|
now: meta.now,
|
|
fallbackRunnerKind: trace.meta.runnerKind
|
|
});
|
|
trace.nextSeq += 1;
|
|
trace.events.push(normalized);
|
|
if (trace.events.length > maxEvents) {
|
|
trace.events.splice(0, trace.events.length - maxEvents);
|
|
}
|
|
trace.updatedAt = normalized.createdAt;
|
|
if (normalized.terminal === true) {
|
|
trace.status = normalized.status === "completed"
|
|
? "completed"
|
|
: normalized.status === "canceled"
|
|
? "canceled"
|
|
: normalized.type;
|
|
trace.finishedAt = normalized.createdAt;
|
|
} else if (normalized.status === "failed" || normalized.type === "error" || normalized.type === "timeout") {
|
|
trace.status = normalized.type;
|
|
trace.finishedAt = normalized.createdAt;
|
|
}
|
|
notify(trace, normalized);
|
|
return normalized;
|
|
}
|
|
|
|
function appendAssistantDelta(traceId, delta = {}, meta = {}) {
|
|
const trace = ensure(traceId, meta);
|
|
const normalized = normalizeAssistantDelta(delta, {
|
|
traceId: trace.traceId,
|
|
now: meta.now,
|
|
fallbackRunnerKind: trace.meta.runnerKind
|
|
});
|
|
if (!normalized) return null;
|
|
const key = normalized.itemId || "assistant-message";
|
|
const previous = trace.assistantStreams.get(key) ?? {
|
|
traceId: trace.traceId,
|
|
itemId: key,
|
|
type: "assistant_message_stream",
|
|
status: "streaming",
|
|
label: "assistant:stream",
|
|
createdAt: normalized.createdAt,
|
|
updatedAt: normalized.createdAt,
|
|
runnerKind: normalized.runnerKind,
|
|
sessionId: normalized.sessionId,
|
|
sessionStatus: normalized.sessionStatus,
|
|
turn: normalized.turn,
|
|
threadId: normalized.threadId,
|
|
turnId: normalized.turnId,
|
|
waitingFor: normalized.waitingFor,
|
|
chunkCount: 0,
|
|
text: "",
|
|
lastChunk: "",
|
|
valuesPrinted: false
|
|
};
|
|
const text = `${previous.text ?? ""}${normalized.chunk}`;
|
|
const stream = dropUndefined({
|
|
...previous,
|
|
updatedAt: normalized.createdAt,
|
|
runnerKind: normalized.runnerKind ?? previous.runnerKind,
|
|
sessionId: normalized.sessionId ?? previous.sessionId,
|
|
sessionStatus: normalized.sessionStatus ?? previous.sessionStatus,
|
|
turn: normalized.turn ?? previous.turn,
|
|
threadId: normalized.threadId ?? previous.threadId,
|
|
turnId: normalized.turnId ?? previous.turnId,
|
|
waitingFor: normalized.waitingFor ?? previous.waitingFor,
|
|
chunkCount: previous.chunkCount + 1,
|
|
text,
|
|
lastChunk: normalized.chunk,
|
|
valuesPrinted: false
|
|
});
|
|
trace.assistantStreams.set(key, stream);
|
|
trace.updatedAt = normalized.createdAt;
|
|
notify(trace, null);
|
|
return stream;
|
|
}
|
|
|
|
function snapshot(traceId, extra = {}) {
|
|
const trace = traces.get(cleanTraceId(traceId));
|
|
if (!trace) return emptySnapshot(traceId, extra);
|
|
const events = trace.events.map((event) => ({ ...event }));
|
|
const assistantStreams = assistantStreamsSnapshot(trace);
|
|
const lastEvent = events.at(-1) ?? null;
|
|
return {
|
|
traceId: trace.traceId,
|
|
status: trace.status,
|
|
createdAt: trace.createdAt,
|
|
updatedAt: trace.updatedAt,
|
|
startedAt: trace.startedAt,
|
|
finishedAt: trace.finishedAt,
|
|
eventCount: events.length,
|
|
events,
|
|
assistantStreams,
|
|
eventLabels: events.map((event) => event.label).filter(Boolean),
|
|
lastEvent,
|
|
elapsedMs: elapsedMs(trace.startedAt, trace.finishedAt ?? trace.updatedAt),
|
|
waitingFor: snapshotWaitingFor(trace, events, assistantStreams, lastEvent),
|
|
runnerKind: extra.runnerKind ?? trace.meta.runnerKind ?? null,
|
|
workspace: extra.workspace ?? trace.meta.workspace ?? null,
|
|
sandbox: extra.sandbox ?? trace.meta.sandbox ?? null,
|
|
sessionMode: extra.sessionMode ?? trace.meta.sessionMode ?? null,
|
|
implementationType: extra.implementationType ?? trace.meta.implementationType ?? null,
|
|
sessionId: extra.sessionId ?? lastEvent?.sessionId ?? null,
|
|
sessionStatus: extra.sessionStatus ?? lastEvent?.sessionStatus ?? null,
|
|
sessionLifecycleStatus: extra.sessionLifecycleStatus ?? lastEvent?.sessionLifecycleStatus ?? null,
|
|
turn: extra.turn ?? lastEvent?.turn ?? null,
|
|
outputTruncated: events.some((event) => event.outputTruncated === true),
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function subscribe(traceId, listener, meta = {}) {
|
|
const trace = ensure(traceId, meta);
|
|
trace.listeners.add(listener);
|
|
return () => {
|
|
trace.listeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
function clear() {
|
|
traces.clear();
|
|
}
|
|
|
|
function prune() {
|
|
if (traces.size <= maxTraces) return;
|
|
const stale = [...traces.values()]
|
|
.sort((left, right) => String(left.updatedAt).localeCompare(String(right.updatedAt)))
|
|
.slice(0, traces.size - maxTraces);
|
|
for (const trace of stale) {
|
|
traces.delete(trace.traceId);
|
|
}
|
|
}
|
|
|
|
return {
|
|
ensure,
|
|
append,
|
|
appendAssistantDelta,
|
|
snapshot,
|
|
subscribe,
|
|
clear
|
|
};
|
|
}
|
|
|
|
export const defaultCodeAgentTraceStore = createCodeAgentTraceStore();
|
|
|
|
export function createCodeAgentTraceRecorder({
|
|
traceStore = defaultCodeAgentTraceStore,
|
|
traceId,
|
|
now,
|
|
runnerKind = null,
|
|
workspace = null,
|
|
sandbox = null,
|
|
sessionMode = null,
|
|
implementationType = null
|
|
} = {}) {
|
|
const startedAt = timestampFor(now);
|
|
const startedEpochMs = Date.now();
|
|
const baseMeta = { now, runnerKind, workspace, sandbox, sessionMode, implementationType };
|
|
traceStore.ensure(traceId, baseMeta);
|
|
|
|
function append(event = {}) {
|
|
return traceStore.append(traceId, {
|
|
elapsedMs: Date.now() - startedEpochMs,
|
|
...event
|
|
}, baseMeta);
|
|
}
|
|
|
|
function appendAssistantDelta(delta = {}) {
|
|
return traceStore.appendAssistantDelta(traceId, delta, baseMeta);
|
|
}
|
|
|
|
function snapshot(extra = {}) {
|
|
return traceStore.snapshot(traceId, { runnerKind, workspace, sandbox, sessionMode, implementationType, ...extra });
|
|
}
|
|
|
|
function runnerTrace(extra = {}) {
|
|
const current = snapshot(extra);
|
|
return {
|
|
traceId: current.traceId,
|
|
runnerKind: extra.runnerKind ?? current.runnerKind,
|
|
workspace: extra.workspace ?? current.workspace,
|
|
sandbox: extra.sandbox ?? current.sandbox,
|
|
sessionMode: extra.sessionMode ?? current.sessionMode,
|
|
sessionId: extra.sessionId ?? current.sessionId,
|
|
sessionStatus: extra.sessionStatus ?? current.sessionStatus,
|
|
sessionLifecycleStatus: extra.sessionLifecycleStatus ?? current.sessionLifecycleStatus,
|
|
idleTimeoutMs: extra.idleTimeoutMs ?? null,
|
|
lastTraceId: extra.lastTraceId ?? current.traceId,
|
|
turn: extra.turn ?? current.turn,
|
|
sessionReused: extra.sessionReused ?? false,
|
|
implementationType: extra.implementationType ?? current.implementationType,
|
|
limitations: Array.isArray(extra.limitations) ? extra.limitations : [],
|
|
startedAt: extra.startedAt ?? startedAt,
|
|
finishedAt: extra.finishedAt ?? current.finishedAt ?? timestampFor(now),
|
|
updatedAt: current.updatedAt,
|
|
events: current.events,
|
|
assistantStreams: current.assistantStreams,
|
|
eventLabels: current.eventLabels,
|
|
lastEvent: current.lastEvent,
|
|
elapsedMs: current.elapsedMs,
|
|
waitingFor: current.waitingFor,
|
|
outputTruncated: current.outputTruncated || extra.outputTruncated === true,
|
|
valuesPrinted: false,
|
|
note: extra.note ?? "Real-time runnerTrace is appended as Codex stdio/session events are captured."
|
|
};
|
|
}
|
|
|
|
return {
|
|
traceId,
|
|
append,
|
|
appendAssistantDelta,
|
|
snapshot,
|
|
runnerTrace
|
|
};
|
|
}
|
|
|
|
export function runnerTraceFromSnapshot(snapshot = {}, extra = {}) {
|
|
return {
|
|
traceId: snapshot.traceId ?? extra.traceId ?? null,
|
|
runnerKind: extra.runnerKind ?? snapshot.runnerKind ?? null,
|
|
workspace: extra.workspace ?? snapshot.workspace ?? null,
|
|
sandbox: extra.sandbox ?? snapshot.sandbox ?? null,
|
|
sessionMode: extra.sessionMode ?? snapshot.sessionMode ?? null,
|
|
sessionId: extra.sessionId ?? snapshot.sessionId ?? null,
|
|
sessionStatus: extra.sessionStatus ?? snapshot.sessionStatus ?? null,
|
|
sessionLifecycleStatus: extra.sessionLifecycleStatus ?? snapshot.sessionLifecycleStatus ?? null,
|
|
idleTimeoutMs: extra.idleTimeoutMs ?? null,
|
|
lastTraceId: extra.lastTraceId ?? snapshot.traceId ?? null,
|
|
turn: extra.turn ?? snapshot.turn ?? null,
|
|
sessionReused: extra.sessionReused ?? false,
|
|
implementationType: extra.implementationType ?? snapshot.implementationType ?? null,
|
|
limitations: Array.isArray(extra.limitations) ? extra.limitations : [],
|
|
startedAt: extra.startedAt ?? snapshot.startedAt ?? null,
|
|
finishedAt: extra.finishedAt ?? snapshot.finishedAt ?? snapshot.updatedAt ?? null,
|
|
updatedAt: snapshot.updatedAt ?? null,
|
|
events: Array.isArray(snapshot.events) ? snapshot.events : [],
|
|
assistantStreams: Array.isArray(snapshot.assistantStreams) ? snapshot.assistantStreams : [],
|
|
eventLabels: Array.isArray(snapshot.eventLabels) ? snapshot.eventLabels : [],
|
|
lastEvent: snapshot.lastEvent ?? null,
|
|
elapsedMs: snapshot.elapsedMs ?? null,
|
|
waitingFor: snapshot.waitingFor ?? null,
|
|
outputTruncated: snapshot.outputTruncated === true || extra.outputTruncated === true,
|
|
valuesPrinted: false,
|
|
note: extra.note ?? "runnerTrace snapshot"
|
|
};
|
|
}
|
|
|
|
function normalizeTraceEvent(event, { traceId, seq, now, fallbackRunnerKind } = {}) {
|
|
const type = safeToken(event.type ?? event.kind ?? "event");
|
|
const status = safeToken(event.status ?? "observed");
|
|
const toolName = safeText(event.toolName ?? event.name, 120);
|
|
const label = safeText(event.label, 180) || labelFor({ type, status, toolName });
|
|
const eventCreatedAt = safeTimestamp(event.createdAt) ?? timestampFor(now);
|
|
return dropUndefined({
|
|
seq,
|
|
traceId,
|
|
type,
|
|
stage: safeToken(event.stage ?? type),
|
|
status,
|
|
label,
|
|
createdAt: eventCreatedAt,
|
|
elapsedMs: typeof event.elapsedMs === "number" ? Math.max(0, Math.trunc(event.elapsedMs)) : null,
|
|
runnerKind: safeText(event.runnerKind ?? fallbackRunnerKind, 140),
|
|
source: safeText(event.source, 120),
|
|
sourceSeq: Number.isInteger(event.sourceSeq) ? event.sourceSeq : undefined,
|
|
runId: safeText(event.runId, 180),
|
|
commandId: safeText(event.commandId, 180),
|
|
attemptId: safeText(event.attemptId, 180),
|
|
runnerId: safeText(event.runnerId, 180),
|
|
jobName: safeText(event.jobName, 180),
|
|
namespace: safeText(event.namespace, 180),
|
|
sessionId: safeText(event.sessionId, 180),
|
|
sessionStatus: safeText(event.sessionStatus, 80),
|
|
sessionLifecycleStatus: safeText(event.sessionLifecycleStatus, 80),
|
|
sessionReused: typeof event.sessionReused === "boolean" ? event.sessionReused : undefined,
|
|
turn: typeof event.turn === "number" ? event.turn : undefined,
|
|
toolName,
|
|
itemId: safeText(event.itemId, 180),
|
|
command: safeText(event.command, 900),
|
|
exitCode: Number.isInteger(event.exitCode) ? event.exitCode : undefined,
|
|
durationMs: typeof event.durationMs === "number" ? Math.max(0, Math.trunc(event.durationMs)) : undefined,
|
|
outputBytes: typeof event.outputBytes === "number" ? Math.max(0, Math.trunc(event.outputBytes)) : undefined,
|
|
promptSummary: safeText(event.promptSummary, 240),
|
|
outputSummary: safeText(event.outputSummary, 400),
|
|
stdoutSummary: safeText(event.stdoutSummary, 600),
|
|
stderrSummary: safeText(event.stderrSummary, 600),
|
|
chunk: safeText(event.chunk, 400),
|
|
message: safeText(event.message, 1200),
|
|
errorCode: safeText(event.errorCode, 120),
|
|
diagnosisCode: safeText(event.diagnosisCode ?? event.diagnosis?.code, 160),
|
|
diagnosis: normalizeDiagnosis(event.diagnosis),
|
|
details: normalizeDetails(event.details),
|
|
waitingFor: safeText(event.waitingFor, 220),
|
|
timeoutMs: typeof event.timeoutMs === "number" ? Math.trunc(event.timeoutMs) : undefined,
|
|
hardTimeoutMs: typeof event.hardTimeoutMs === "number" ? Math.trunc(event.hardTimeoutMs) : undefined,
|
|
lastActivityAt: safeText(event.lastActivityAt, 80),
|
|
idleMs: typeof event.idleMs === "number" ? Math.max(0, Math.trunc(event.idleMs)) : undefined,
|
|
outputTruncated: event.outputTruncated === true,
|
|
terminal: event.terminal === true,
|
|
valuesPrinted: false
|
|
});
|
|
}
|
|
|
|
function normalizeDetails(value, depth = 0) {
|
|
if (value === undefined || value === null || depth > 3) return undefined;
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "number") return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
if (typeof value === "string") return safeText(value, 240) || undefined;
|
|
if (Array.isArray(value)) {
|
|
const items = value.slice(0, 40).map((item) => normalizeDetails(item, depth + 1)).filter((item) => item !== undefined);
|
|
return items.length > 0 ? items : undefined;
|
|
}
|
|
if (typeof value !== "object") return undefined;
|
|
const result = {};
|
|
for (const [key, item] of Object.entries(value).slice(0, 80)) {
|
|
const safeKey = safeToken(key);
|
|
if (!safeKey || safeKey === "prompt" || safeKey === "content" || safeKey === "text") continue;
|
|
const normalized = normalizeDetails(item, depth + 1);
|
|
if (normalized !== undefined) result[safeKey] = normalized;
|
|
}
|
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
}
|
|
|
|
function normalizeAssistantDelta(delta, { traceId, now, fallbackRunnerKind } = {}) {
|
|
const chunk = safeText(delta.chunk ?? delta.delta, 400);
|
|
if (!chunk) return null;
|
|
return dropUndefined({
|
|
traceId,
|
|
itemId: safeText(delta.itemId, 180),
|
|
chunk,
|
|
createdAt: safeTimestamp(delta.createdAt) ?? timestampFor(now),
|
|
runnerKind: safeText(delta.runnerKind ?? fallbackRunnerKind, 140),
|
|
sessionId: safeText(delta.sessionId, 180),
|
|
sessionStatus: safeText(delta.sessionStatus, 80),
|
|
turn: typeof delta.turn === "number" ? delta.turn : undefined,
|
|
threadId: safeText(delta.threadId, 180),
|
|
turnId: safeText(delta.turnId, 180),
|
|
waitingFor: safeText(delta.waitingFor, 220),
|
|
valuesPrinted: false
|
|
});
|
|
}
|
|
|
|
function safeTimestamp(value) {
|
|
const text = safeText(value, 80);
|
|
if (!text) return null;
|
|
const ms = Date.parse(text);
|
|
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
}
|
|
|
|
function assistantStreamsSnapshot(trace) {
|
|
if (!(trace?.assistantStreams instanceof Map)) return [];
|
|
return [...trace.assistantStreams.values()].map((stream) => ({ ...stream }));
|
|
}
|
|
|
|
function labelFor({ type, status, toolName }) {
|
|
if (type === "session") return `session:${status}`;
|
|
if (type === "prompt") return `prompt:${status}`;
|
|
if (type === "tool_call") return `tool:${toolName || "codex"}:${status}`;
|
|
if (type === "assistant_message") return `assistant:${status}`;
|
|
if (["timeout", "cancel", "error"].includes(type)) return type;
|
|
if (type === "request") return `request:${status}`;
|
|
if (type === "stdio") return `stdio:${status}`;
|
|
return `${type}:${status}`;
|
|
}
|
|
|
|
function notify(trace, event) {
|
|
const assistantStreams = assistantStreamsSnapshot(trace);
|
|
const snapshot = {
|
|
...emptySnapshot(trace.traceId),
|
|
...{
|
|
traceId: trace.traceId,
|
|
status: trace.status,
|
|
createdAt: trace.createdAt,
|
|
updatedAt: trace.updatedAt,
|
|
startedAt: trace.startedAt,
|
|
finishedAt: trace.finishedAt,
|
|
eventCount: trace.events.length,
|
|
events: trace.events.map((item) => ({ ...item })),
|
|
assistantStreams,
|
|
eventLabels: trace.events.map((item) => item.label).filter(Boolean),
|
|
lastEvent: event ?? trace.events.at(-1) ?? null,
|
|
elapsedMs: elapsedMs(trace.startedAt, trace.finishedAt ?? trace.updatedAt),
|
|
waitingFor: snapshotWaitingFor(trace, trace.events, assistantStreams, event ?? trace.events.at(-1) ?? null),
|
|
runnerKind: trace.meta.runnerKind,
|
|
workspace: trace.meta.workspace,
|
|
sandbox: trace.meta.sandbox,
|
|
sessionMode: trace.meta.sessionMode,
|
|
implementationType: trace.meta.implementationType,
|
|
sessionId: event?.sessionId ?? assistantStreams.at(-1)?.sessionId ?? null,
|
|
sessionStatus: event?.sessionStatus ?? assistantStreams.at(-1)?.sessionStatus ?? null,
|
|
sessionLifecycleStatus: event?.sessionLifecycleStatus ?? null,
|
|
outputTruncated: trace.events.some((item) => item.outputTruncated === true),
|
|
valuesPrinted: false
|
|
}
|
|
};
|
|
for (const listener of trace.listeners) {
|
|
try {
|
|
listener(event, snapshot);
|
|
} catch {
|
|
trace.listeners.delete(listener);
|
|
}
|
|
}
|
|
}
|
|
|
|
function emptySnapshot(traceId, extra = {}) {
|
|
const id = cleanTraceId(traceId);
|
|
return {
|
|
traceId: id,
|
|
status: "missing",
|
|
createdAt: null,
|
|
updatedAt: null,
|
|
startedAt: null,
|
|
finishedAt: null,
|
|
eventCount: 0,
|
|
events: [],
|
|
assistantStreams: [],
|
|
eventLabels: [],
|
|
lastEvent: null,
|
|
elapsedMs: null,
|
|
waitingFor: null,
|
|
runnerKind: extra.runnerKind ?? null,
|
|
workspace: extra.workspace ?? null,
|
|
sandbox: extra.sandbox ?? null,
|
|
sessionMode: extra.sessionMode ?? null,
|
|
implementationType: extra.implementationType ?? null,
|
|
sessionId: null,
|
|
sessionStatus: null,
|
|
sessionLifecycleStatus: null,
|
|
turn: null,
|
|
outputTruncated: false,
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function lastWaitingFor(events) {
|
|
for (const event of [...events].reverse()) {
|
|
if (event.waitingFor) return event.waitingFor;
|
|
if (event.type === "tool_call" && ["started", "output_chunk"].includes(event.status)) {
|
|
return `tool:${event.toolName ?? "codex"}`;
|
|
}
|
|
if (event.type === "prompt" && event.status === "sent") return "assistant-message";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function snapshotWaitingFor(trace, events, assistantStreams, lastEvent) {
|
|
if (lastEvent?.terminal === true) {
|
|
return lastEvent.waitingFor ?? null;
|
|
}
|
|
if (trace?.finishedAt) return null;
|
|
return lastWaitingFor(events) ?? assistantStreams.at(-1)?.waitingFor ?? null;
|
|
}
|
|
|
|
function elapsedMs(start, end) {
|
|
const startMs = Date.parse(start ?? "");
|
|
const endMs = Date.parse(end ?? "");
|
|
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return null;
|
|
return Math.max(0, endMs - startMs);
|
|
}
|
|
|
|
function normalizeDiagnosis(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
const lastRoutedEvent = value.lastRoutedEvent && typeof value.lastRoutedEvent === "object" && !Array.isArray(value.lastRoutedEvent)
|
|
? dropUndefined({
|
|
type: safeText(value.lastRoutedEvent.type, 80),
|
|
status: safeText(value.lastRoutedEvent.status, 80),
|
|
label: safeText(value.lastRoutedEvent.label, 180),
|
|
stage: safeText(value.lastRoutedEvent.stage, 80),
|
|
toolName: safeText(value.lastRoutedEvent.toolName, 120),
|
|
itemId: safeText(value.lastRoutedEvent.itemId, 180),
|
|
waitingFor: safeText(value.lastRoutedEvent.waitingFor, 220),
|
|
exitCode: Number.isInteger(value.lastRoutedEvent.exitCode) ? value.lastRoutedEvent.exitCode : undefined,
|
|
durationMs: typeof value.lastRoutedEvent.durationMs === "number" ? Math.max(0, Math.trunc(value.lastRoutedEvent.durationMs)) : undefined
|
|
})
|
|
: undefined;
|
|
return dropUndefined({
|
|
code: safeText(value.code, 160),
|
|
layer: safeText(value.layer, 120),
|
|
kind: safeText(value.kind, 120),
|
|
summary: safeText(value.summary, 500),
|
|
waitingFor: safeText(value.waitingFor, 220),
|
|
lastActivityAt: safeText(value.lastActivityAt, 80),
|
|
idleMs: typeof value.idleMs === "number" ? Math.max(0, Math.trunc(value.idleMs)) : undefined,
|
|
lastActivityLabel: safeText(value.lastActivityLabel, 180),
|
|
threadId: safeText(value.threadId, 180),
|
|
turnId: safeText(value.turnId, 180),
|
|
lastRoutedEvent
|
|
});
|
|
}
|
|
|
|
function safeText(value, limit = TEXT_LIMIT) {
|
|
if (value === undefined || value === null || value === "") return undefined;
|
|
const redacted = String(value).replace(SECRET_PATTERN, "[redacted]").replace(/\s+/gu, " ").trim();
|
|
if (!redacted) return undefined;
|
|
return redacted.length > limit ? `${redacted.slice(0, limit - 3)}...` : redacted;
|
|
}
|
|
|
|
function safeToken(value) {
|
|
return String(value ?? "")
|
|
.trim()
|
|
.replace(/[^A-Za-z0-9_.:-]/gu, "_")
|
|
.slice(0, 80) || "event";
|
|
}
|
|
|
|
function cleanTraceId(value) {
|
|
const id = String(value ?? "").trim();
|
|
return /^trc_[A-Za-z0-9_.:-]+$/u.test(id) ? id : null;
|
|
}
|
|
|
|
function timestampFor(now) {
|
|
const value = typeof now === "function" ? now() : now;
|
|
const date = value ? new Date(value) : new Date();
|
|
return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString();
|
|
}
|
|
|
|
function positiveInteger(value, fallback) {
|
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
function dropEmpty(value) {
|
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ""));
|
|
}
|
|
|
|
function dropUndefined(value) {
|
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
}
|