import type { Message } from "./api"; // The runtime also uses the Unix epoch for historical entries without a time. // The transcript renders these as "Earlier", without inventing a send time. export const UNKNOWN_AGENT_TIME = "1970-01-01T00:00:00.000Z"; const object = (value: unknown): Record | undefined => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; const timestamp = (value: unknown): string | undefined => { if (typeof value !== "string" || !value.trim()) return undefined; const time = Date.parse(value); return Number.isFinite(time) && time !== 0 ? new Date(time).toISOString() : undefined; }; export function agentMessageTime(message: Pick): string | undefined { const info = object(message.metadata); return timestamp(message.createdAt) ?? timestamp(info?.startedAt) ?? timestamp(info?.completedAt); } /** Normalize historical/partial rows at both transports, before state or cursors change. */ export function normalizeAgentMessage(value: unknown): Message { const message = object(value); if (!message || typeof message.id !== "string" || !message.id || typeof message.role !== "string" || !["system", "user", "assistant", "tool"].includes(message.role)) throw new Error("Invalid agent message."); const info = object(message.metadata); // Older runtime REST redaction emitted protected tool rows with only id/role/name. // Keep their identity for a later complete upsert; never discard a whole page. const content = typeof message.content === "string" ? message.content : message.role === "tool" && message.content == null ? (typeof message.name === "string" ? message.name : "Tool") : undefined; if (content === undefined) throw new Error("Invalid agent message."); const createdAt = agentMessageTime(message as Message) ?? UNKNOWN_AGENT_TIME; const needsToolMetadata = message.role === "tool" && info?.kind !== "tool_call"; if (message.createdAt === createdAt && message.content === content && !needsToolMetadata) return message as Message; return { ...message, content, createdAt, ...(needsToolMetadata ? { metadata: { ...info, kind: "tool_call" } } : {}), } as Message; } function cursor(message: Message): bigint | undefined { const entryId = object(message.metadata)?.entryId; if ((typeof entryId === "number" && Number.isSafeInteger(entryId) && entryId >= 0) || (typeof entryId === "string" && /^\d+$/.test(entryId))) return BigInt(entryId); } /** One total order for history, queue snapshots and optimistic rows, including ties. */ export function compareAgentMessages(left: Message, right: Message): number { const byTime = Date.parse(agentMessageTime(left) ?? UNKNOWN_AGENT_TIME) - Date.parse(agentMessageTime(right) ?? UNKNOWN_AGENT_TIME); if (byTime) return byTime; const a = cursor(left), b = cursor(right); if (a !== b) { if (a === undefined) return 1; if (b === undefined) return -1; return a < b ? -1 : 1; } return left.id < right.id ? -1 : left.id > right.id ? 1 : 0; }