import { isConnectedMachineOutcomeUnknownOutput, parseCustomMcpSetupRequest, parseMediaGenerationResult, parseToolDisplayMetadata, EMPTY_FINAL_REPLY_NOTICE, turnCompletedWithEmptyFinalReply, type HumanInputAnswer, type HumanInputQuestion, type HumanInputResponse, type HumanInputSecretAnswerResult, type HumanInputSecretSpec, type SessionEvent, type SessionStatus, type TimelineAnnotation, } from "@opengeni/sdk"; import fleetDecisionItem from "./fleet-decision-projection"; import { learningRefusalFromToolOutput } from "@opengeni/sdk/learning-refusal"; import { learningRefusalText } from "./learning-refusal-copy"; import { allowanceExhaustedMessage, parseAllowanceExhaustedRefusal, } from "@opengeni/sdk/allowance-refusal"; import { parseSessionUsageLimitReachedRefusal, SESSION_USAGE_LIMIT_SEGMENT, sessionUsageLimitReachedMessage, } from "@opengeni/sdk/session-usage-limits"; import { CREDIT_EXHAUSTION_MESSAGE, presentFailure, isCreditExhaustion, tryParseJson, } from "../lib/format"; import { mcpToolLeaf, toolMatchesLeaf } from "./tool-display-name"; import { invokedTool } from "./invoked-tool"; import { presentTurnFailureText } from "./compaction-copy"; import { toolOutputPreviewFact, type ToolOutputReader } from "../hooks/tool-output-reads"; import type { AgentMessageItem, ActivityItem, AuthNeededItem, ContextCompactionItem, GoalItem, HumanInputAnswerSummary, HumanInputItem, MachineInputBatchItem, MachineInputMember, MemoryItem, NoticeItem, SandboxItem, StartupPhase, StartupPhaseItem, SessionStatusItem, TimelineGroup, TimelineItem, TurnEndItem, TurnStepsItem, ToolCallItem, ToolOutputPreview, ToolCallTruncation, UserMessageItem, WorkerItem, } from "./types"; import { type OpenGeniText } from "../messages/runtime"; export { toolDisplayName, mcpToolLeaf, toolMatchesLeaf } from "./tool-display-name"; /** Message identity only: queue placement needs the surrounding turn history. */ export function isTimelineUserQuestion(event: SessionEvent): boolean { return ( event.type === "user.message" && workerCompletionPayload(asRecord(event.payload).childCompletion) === null ); } /* ---------------------------------------------------------------------------- Timeline projection `buildTimeline` folds a session's raw event log (replayed + live, ordered by sequence) into renderable items: chat messages with accumulated streaming deltas, reasoning summaries, tool calls matched to their outputs, sandbox operations with command output, spawned-worker status (the manager's `session_create` / `session_send_message` orchestration calls), goal markers, status changes, and turn failures. It is a pure function — same events in, same items out — so it can be memoized, unit-tested, and re-run incrementally as new events stream in. -------------------------------------------------------------------------- */ /** Tool leaves on the first-party Opengeni MCP server that operate on sessions. */ const WORKER_SPAWN_TOOL = "session_create"; const WORKER_MESSAGE_TOOL = "session_send_message"; const WORKER_FAILURE_CODE_MAX_LENGTH = 128; const WORKER_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024; type PendingWaitOutcome = { id: string; reason: string; occurredAt: string }; type TrackedAgentResponse = { item: AgentMessageItem; completed: boolean }; /** * Tools whose durable side-effect events already own the timeline (MemoryRow). * Emitting a generic tool-call too is double chrome — skip the call. * * Goal tools are intentionally NOT landmark-only: an agent `goal_set` / * `goal_update` / `goal_complete` / `goal_pause` stays an in-cluster tool row, * and the matching `goal.*` session event is suppressed below when `actor` is * `"agent"`. That keeps mid-turn goal tools from splitting the step rail with * a breakaway GoalRow pill. Non-agent goal events (API, create-session, * system auto-pause, continuations) still render as landmarks. * * Solo `goal_continuation` machine-input batches are also suppressed: the * paired `goal.continuation` GoalRow already marks the tick; rendering both * restates the goal text. Mixed batches (continuation + other kinds) still * render as machine-input rows. */ const LANDMARK_ONLY_TOOL_LEAVES = new Set(["memory_save", "memory_correct"]); const TIMELINE_ANNOTATION_ANSI_SEQUENCE = new RegExp("\\u001B\\[[0-?]*[ -/]*[@-~]", "g"); function safeTimelineAnnotationJson(value: unknown): string { try { return JSON.stringify(value) ?? ""; } catch { return String(value ?? ""); } } function timelineAnnotationToolOutputValue(value: unknown, depth = 0): string { if (depth > 8 || value === null || value === undefined) { return value == null ? "" : safeTimelineAnnotationJson(value); } if (typeof value === "string") return value; if (typeof value !== "object" || Array.isArray(value)) { return safeTimelineAnnotationJson(value); } const record = value as Record; if (record.type === "text" && typeof record.text === "string") return record.text; if (Array.isArray(record.content)) { const text = record.content.find( (part) => part !== null && typeof part === "object" && (part as { type?: unknown }).type === "text" && typeof (part as { text?: unknown }).text === "string", ) as { text: string } | undefined; if (text) return text.text; } if ("structuredContent" in record) { return timelineAnnotationToolOutputValue(record.structuredContent, depth + 1); } if ("result" in record) return timelineAnnotationToolOutputValue(record.result, depth + 1); return safeTimelineAnnotationJson(value); } /** Browser mirror of the server's canonical annotation source projection. */ function timelineAnnotationToolOutputText(output: unknown): string | null { const raw = timelineAnnotationToolOutputValue(output); const marker = raw.indexOf("\nOutput:\n"); const text = ( marker >= 0 ? raw.slice(marker + "\nOutput:\n".length) : raw.startsWith("Output:\n") ? raw.slice("Output:\n".length) : raw ).replace(TIMELINE_ANNOTATION_ANSI_SEQUENCE, ""); return text.length > 0 ? text : null; } export type BuildTimelineOptions = { partialStart?: boolean; /** * Turns whose steps are not all loaded, keyed by turn id. Each becomes a * `turn-steps` item placed at the turn's start (its `turn.started`, or its * first event), so the readable grouping gives the turn an expandable work * row even when none of its steps are in the browser. */ turnSteps?: ReadonlyMap>; /** * Exact reads of tool outputs a step page delivered as previews, by output * event. A previewed tool row carries its read's state and loader in * `outputPreview`; without a reader it is still marked as a preview. */ toolOutputs?: ToolOutputReader | undefined; }; export function buildTimeline( t: OpenGeniText, events: SessionEvent[], options: BuildTimelineOptions = {}, ): TimelineItem[] { const items: TimelineItem[] = []; // Calls remain in insertion order while message bubbles can move. Keep the // two existing lookup rules separate: creation merges only tool rows, while // an output correlates to the latest tool or worker with that exact id. const toolCallsById = new Map(); const callsById = new Map(); const allowanceNotices = new Set(); // One "not saved" notice per turn for the same refused change, however // often the agent retried it. const learningRefusalNotices = new Set(); const allowanceNotice = (event: SessionEvent, turnId: string | null) => { const refusal = parseAllowanceExhaustedRefusal(event.payload); if (!refusal) return null; const text = allowanceExhaustedMessage(refusal); const key = JSON.stringify([ turnId, event.turnAttemptId, startupRecoveryRevisionByTurn.get(turnId ?? "") ?? 0, refusal.scope, refusal.subjectId, refusal.resetsAt, ]); if (!allowanceNotices.has(key)) { allowanceNotices.add(key); items.push({ kind: "notice", id: `${event.id}-allowance`, tone: "failed", text, // The canonical sentence replaces upstream prose, so a host renderer // reading `message` never shows a wrapper's private wording. allowance: { ...refusal, message: text }, occurredAt: event.occurredAt, }); } return text; }; const prescan = prescanTurnAnchors(events); const ordered = orderTimelineEvents(events, prescan); // A bounded replay can begin halfway through a message (including inside an // interactive fence). Never parse that suffix as a complete Markdown source. // Keep the raw events available for pagination; a completed receipt restores // the authoritative message without requiring an unbounded history fetch. let completeHistoryPrefix = options.partialStart !== true; const knownMessagePrefixTurns = new Set(); const incompleteMessageKeys = new Set(); const pendingWaitOutcomeByTurn = new Map(); const latestAgentResponseByTurn = new Map(); const identifiedMessages = new Map(); // Messages drawn from streamed text that no completion has settled yet. A // Steer that cuts the answer short leaves its unfinished text here. const unsettledStreamedMessages = new Set(); // Steer bubbles drawn while the current turn still ran. The turn's answer // was written before them, so a handover keeps that answer above them. let steerBubblesAwaitingHandover: UserMessageItem[] = []; const humanInputRequests = humanInputRequestsById(events); const humanInputToolCallIds = new Set( [...humanInputRequests.values()] .map((request) => stringValue(request.toolCallId)) .filter(Boolean), ); // Delegated workers that have not reported back yet, so a recorded agent wait // can say how many agents it is waiting for. Only a session's own workers // report back, so a message to any other session is never awaited. Best // effort: a bounded replay only knows the spawns and results inside its window. const workerSessions = new Set(); const pendingWorkerSessions = new Set(); let openWaitNotice: NoticeItem | null = null; // Tool results update the original tool row in place, and running lifecycle // receipts have no row. Carry resume evidence on the attention landmarks so // grouping cannot mistake historical approval for a live wait. const presentationWaits = new Map>(); // Admission blocks hold the whole session, not one turn; any later status // (a recheck, a new message, a pause) ends them. let openAdmissionBlocks: SessionStatusItem[] = []; const presentationFailures = new Map(); let presentationTurnId: string | null = null; const rememberPresentationWait = ( item: NoticeItem | SessionStatusItem, turnId: string | null, ) => { const key = turnId ?? presentationTurnId; const waits = presentationWaits.get(key) ?? []; waits.push(item); presentationWaits.set(key, waits); items.push(item); }; const resolvePresentationWait = (event: SessionEvent, turnId: string | null) => { if (event.duplicateOfEventId || (event.turnAssociation && event.turnAssociation !== "current")) return; const key = turnId ?? presentationTurnId; for (const item of presentationWaits.get(key) ?? []) item.resolvedAt = event.occurredAt; presentationWaits.delete(key); }; // Live model-capacity waits. Later lifecycle evidence that the turn is no // longer blocked (resume, a non-waiting status, its settlement) resolves // them, so a recovered wait never lingers as a warning. let openCapacityWaits: NoticeItem[] = []; const resolveCapacityWaits = (event: SessionEvent, payload: Record) => { if (openCapacityWaits.length === 0 || !endsCapacityWait(event, payload)) return; const eventTurnId = event.turnId ?? null; openCapacityWaits = openCapacityWaits.filter((item) => { const waitTurnId = item.capacityWait?.turnId ?? null; // Status changes are session-wide; a turn settlement ends only its own wait // (a queued follow-up being cancelled does not unblock the waiting turn). const ends = event.type === "session.status.changed" || eventTurnId === null || waitTurnId === null || eventTurnId === waitTurnId; if (ends) item.resolvedAt = event.occurredAt; return !ends; }); }; const queuedAtByTurn = new Map(); // When the session's previous turn ended. A prompt queued behind running // work waits for that turn by the person's choice, so its queue span starts // only once the session was free to run it. let previousTurnEndedAt: string | null = null; const startupRecoveryRevisionByTurn = new Map(); const startupPhases = new Map(); let startupEvent: SessionEvent; let startupTurnId: string | null = null; let startupAttemptId: string | null = null; let startupRecoveryRevision = 0; const settleStartupPhase = ( phase: StartupPhase, status: StartupPhaseItem["status"], duration: number | null, outcome: StartupPhaseItem["outcome"], mode = 0, explicitStartedAt?: string, explicitId?: string, blockedReason?: StartupPhaseItem["blockedReason"], ): void => { const key = `${startupTurnId}:${phase}`; const priorState = startupPhases.get(key); const prior = priorState?.[0]; const comparedByAttempt = priorState?.[1] && startupAttemptId; const sameAttempt = comparedByAttempt ? priorState[1] === startupAttemptId : (priorState?.[2] ?? 0) === startupRecoveryRevision; const open = prior?.status === "running" && sameAttempt ? prior : undefined; const replacesPriorAttempt = prior !== undefined && (comparedByAttempt ? !sameAttempt : startupRecoveryRevision > (priorState?.[2] ?? 0)); // A later activity attempt may replay preparation that the logical turn had // already completed before the worker moved. Keep that first successful // landmark; only an abandoned or failed attempt needs replacement. if (replacesPriorAttempt && prior.status === "complete") return; if (mode === 2 && prior && !replacesPriorAttempt) return; if (mode === 1 && !open) return; if (!replacesPriorAttempt && status !== "running" && open) { open.status = status; open.completedAt = startupEvent.occurredAt; open.durationMs = duration === null ? elapsedDurationMs(open.startedAt, startupEvent.occurredAt) : Math.max(0, duration); open.outcome = outcome ?? open.outcome; open.blockedReason = blockedReason; return; } const running = status === "running"; const durationMs = running || duration === null ? null : Math.max(0, duration); const completedEpoch = Date.parse(startupEvent.occurredAt); const startedAt = explicitStartedAt ?? (!running && durationMs !== null && Number.isFinite(completedEpoch) ? new Date(completedEpoch - durationMs).toISOString() : startupEvent.occurredAt); if (replacesPriorAttempt) { Object.assign(prior, { id: explicitId ?? startupEvent.id, status, startedAt, completedAt: running ? null : startupEvent.occurredAt, durationMs, outcome, blockedReason, occurredAt: startedAt, } satisfies Partial); startupPhases.set(key, [prior, startupAttemptId, startupRecoveryRevision]); return; } const item: StartupPhaseItem = { kind: "startup-phase", id: explicitId ?? startupEvent.id, turnId: startupTurnId, phase, status, startedAt, completedAt: running ? null : startupEvent.occurredAt, durationMs, outcome, blockedReason, occurredAt: startedAt, }; items.push(item); startupPhases.set(key, [item, startupAttemptId, startupRecoveryRevision]); }; const last = (): TimelineItem | undefined => items[items.length - 1]; const rememberAgentResponse = ( turnId: string | null, item: AgentMessageItem, completed: boolean, ): void => { latestAgentResponseByTurn.set(turnId, { item, completed }); }; const takePendingWaitOutcome = (turnId: string | null): PendingWaitOutcome | undefined => { const outcome = pendingWaitOutcomeByTurn.get(turnId); pendingWaitOutcomeByTurn.delete(turnId); return outcome; }; const takeAgentResponse = (turnId: string | null): TrackedAgentResponse | undefined => { const response = latestAgentResponseByTurn.get(turnId); latestAgentResponseByTurn.delete(turnId); return response; }; const clearUnscopedTerminalTracking = (): void => { latestAgentResponseByTurn.delete(null); pendingWaitOutcomeByTurn.delete(null); }; /** A new item of a different kind ends whatever was streaming at the tail. */ const closeStreamingTail = (): void => { const open = last(); if ((open?.kind === "agent-message" || open?.kind === "reasoning") && open.streaming) { open.streaming = false; } }; const finalizeOpen = ( turnId?: string | null, disposition: "complete" | "failed" | "cancelled" = "complete", completedAt?: string, ): void => { for (const item of items) { if ( turnId !== undefined && "turnId" in item && item.turnId && turnId && item.turnId !== turnId ) { continue; } if ((item.kind === "agent-message" || item.kind === "reasoning") && item.streaming) { item.streaming = false; } if ((item.kind === "tool-call" || item.kind === "worker") && item.status === "running") { item.status = disposition; } if (item.kind === "sandbox" && item.status === "running") { item.status = disposition; } if (item.kind === "startup-phase" && item.status === "running") { item.status = disposition; if (completedAt) { item.completedAt = completedAt; item.durationMs = elapsedDurationMs(item.startedAt, completedAt); } } } }; const turnStepsByAnchor = turnStepsAnchors(ordered, options.turnSteps); let pendingTurnSteps: TurnStepsItem | null = null; for (const event of ordered) { // A turn's steps marker follows its anchor event, before anything later. if (pendingTurnSteps) { items.push(pendingTurnSteps); pendingTurnSteps = null; } pendingTurnSteps = turnStepsByAnchor.get(event.id) ?? null; const payload = asRecord(event.payload); const turnId = event.turnId ?? null; resolveCapacityWaits(event, payload); if ( turnId && (event.type === "turn.started" || (presentationTurnId === null && isAgentActivityEvent(event.type))) ) { presentationTurnId = turnId; } if ( openWaitNotice && (event.type === "user.message" || event.type === "system.update.delivered" || event.type === "turn.started" || event.type === "session.control.paused" || (event.type === "session.status.changed" && (payload.status === "cancelled" || payload.status === "failed"))) ) { // New input, a pause, or the end of the session ends a recorded wait; the // notice keeps its wait span. openWaitNotice.waitEndedAt = event.occurredAt; openWaitNotice = null; } if (event.type === "session.created") { completeHistoryPrefix = true; } else if ( event.type === "turn.started" || (event.type === "user.message" && payload.delivery !== "steer" && payload.routing !== "accepted_for_steering") ) { // Accepted Steer is visible before its replacement turn starts. It says // nothing about the prefix of a still-streaming message from the old turn. knownMessagePrefixTurns.add(turnId); } startupEvent = event; startupTurnId = turnId; startupAttemptId = (typeof event.turnAttemptId === "string" ? event.turnAttemptId : null) ?? stringValue(payload.attemptId); startupRecoveryRevision = turnId ? (startupRecoveryRevisionByTurn.get(turnId) ?? 0) : 0; const setupPhase = event.type.startsWith("rig.setup.") ? "rig" : event.type.startsWith("turn.startup.phase.") ? startupPhaseFromPayload(payload.phase) : null; const setupStatus = event.type.endsWith(".started") ? "running" : event.type.endsWith(".failed") ? "failed" : event.type.endsWith(".completed") || event.type === "rig.setup.skipped" ? "complete" : null; if (setupPhase && setupStatus) { closeStreamingTail(); let blockedReason: StartupPhaseItem["blockedReason"]; if (setupPhase === "model_preparation" && setupStatus === "failed") { blockedReason = startupLifecycleWaitReason(payload); // Older preparation events omit the typed lifecycle fields. The // sandbox span can supply them only inside the same turn attempt. const sandbox = startupPhases.get(`${startupTurnId}:sandbox`); const sameAttempt = sandbox?.[1] || startupAttemptId ? Boolean(startupAttemptId && sandbox?.[1] === startupAttemptId) : sandbox?.[2] === startupRecoveryRevision; if ( !blockedReason && payload.expectedTransition === undefined && payload.failureCode === undefined && payload.failureStage === undefined && payload.failureCategory === undefined && payload.error === undefined && startupTurnId && sameAttempt && sandbox?.[0].status === "cancelled" ) { blockedReason = sandbox[0].blockedReason; } } settleStartupPhase( setupPhase, blockedReason ? "cancelled" : setupStatus, numberOrNull(payload.durationMs), event.type === "rig.setup.skipped" ? "skipped" : null, 0, undefined, undefined, blockedReason, ); continue; } if ( event.turnId && (event.type === "turn.completed" || event.type === "turn.failed" || event.type === "turn.cancelled" || event.type === "turn.superseded") ) { previousTurnEndedAt = event.occurredAt; } switch (event.type) { case "user.message": { // A steering message must not mark in-flight tools complete; it only // ends whatever text was streaming. Turn lifecycle events finalize. clearUnscopedTerminalTracking(); closeStreamingTail(); const childCompletion = workerCompletionPayload(payload.childCompletion); if (childCompletion) { workerSessions.add(childCompletion.childSessionId); pendingWorkerSessions.delete(childCompletion.childSessionId); items.push({ kind: "worker-completion", id: event.id, turnId, occurredAt: event.occurredAt, childSessionId: childCompletion.childSessionId, childStatus: childCompletion.childStatus, goalStatus: childCompletion.goalStatus, goalText: childCompletion.goalText, evidence: childCompletion.evidence, pausedReason: childCompletion.pausedReason, text: stringValue(payload.text), }); break; } const voiceMessage = realtimeVoiceMessage(payload); const sender = messageSender(payload.initiator); const userMessage: UserMessageItem = { kind: "user-message", id: event.id, ...(event.clientEventId ? { reconciliationKey: `user-message:${event.clientEventId}` } : {}), text: voiceMessage?.text ?? stringValue(payload.text), annotations: timelineAnnotations(payload.annotations), annotationSource: { kind: "user_message", eventId: event.id, eventType: "user.message", sequence: event.sequence, turnId, text: voiceMessage?.text ?? stringValue(payload.text), }, ...(voiceMessage ? { presentation: voiceMessage.presentation } : {}), resources: resourceRefs(payload.resources), tools: toolRefs(payload.tools), ...(sender ? { sender } : {}), occurredAt: event.occurredAt, }; items.push(userMessage); if (payload.delivery === "steer" || payload.routing === "accepted_for_steering") { steerBubblesAwaitingHandover.push(userMessage); } break; } case "user.humanInputResponse": { // A structured answer resumes the frozen tool call rather than adding a // synthetic user.message to model history. Keep the original question // and the resolved human choice together as first-class chat history; // neither belongs behind the generic activity fold. const item = humanInputItemFromResponse(event, payload, humanInputRequests); if (!item) break; closeStreamingTail(); items.push(item); break; } case "system.update.settled": { // An operator retirement is a passive historical receipt, not a new // prompt or model-input delivery. Reuse the existing agent-result row // while ordinary turn-input settlement metadata remains quiet. if (payload.notificationOnly !== true || payload.reason !== "operator_retirement") break; const members = machineInputMembers(payload.members).filter( (member) => member.kind === "child_terminal_result" && member.classification === "failure", ); if (members.length === 0) break; for (const member of members) { workerSessions.add(member.sourceId); pendingWorkerSessions.delete(member.sourceId); } items.push({ kind: "machine-input-batch", id: event.id, turnId: null, members, occurredAt: event.occurredAt, }); break; } case "system.update.delivered": { const inputs = machineInputMembers(payload.members); for (const input of inputs) { if (input.kind === "child_terminal_result") { workerSessions.add(input.sourceId); pendingWorkerSessions.delete(input.sourceId); } } if (inputs.length === 0) break; // Background command receipts are agent-facing inputs, not chat messages. // The durable event remains available to the agent and audit history. const visibleInputs = inputs.filter( (member) => member.kind !== "background_command_result", ); // Goal continuations already have GoalRows. A batch with no other // visible update must not interrupt an in-flight streaming message. if ( visibleInputs.length === 0 || visibleInputs.every((member) => member.kind === "goal_continuation") ) { break; } closeStreamingTail(); items.push({ kind: "machine-input-batch", id: event.id, turnId, members: visibleInputs, occurredAt: event.occurredAt, }); break; } case "agent.message.delta": { const text = stringValue(payload.text); if (!text) { break; } const messageId = stringValue(payload.messageId); const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null; // Responses providers declare the phase when they announce the item, so // a streaming progress note is known as commentary before it completes. const phase = assistantMessagePhase(payload.phase); if ( (!completeHistoryPrefix && !knownMessagePrefixTurns.has(turnId)) || (messageKey && incompleteMessageKeys.has(messageKey)) ) { if (messageKey) incompleteMessageKeys.add(messageKey); break; } const identified = messageKey ? identifiedMessages.get(messageKey) : undefined; if (identified) { if (identified.annotationSource?.eventType !== "agent.message.completed") { identified.text += text; if (phase && !identified.phase) identified.phase = phase; rememberAgentResponse(turnId, identified, false); } break; } const open = last(); if (!messageKey && open?.kind === "tool-call" && open.status === "running") { const previous = latestAgentResponseByTurn.get(turnId); const previousIndex = previous ? items.indexOf(previous.item) : -1; if ( previous && !previous.completed && previousIndex >= 0 && items .slice(previousIndex + 1) .every( (item) => item.kind === "tool-call" && item.turnId === turnId && item.status === "running", ) ) { // Legacy deltas have no provider message identity. Tool creation // alone is not a text boundary; completed output still is. previous.item.text += text; break; } } if ( !messageKey && open?.kind === "agent-message" && open.streaming && open.turnId === turnId ) { open.text += text; rememberAgentResponse(turnId, open, false); break; } closeStreamingTail(); const item: AgentMessageItem = { kind: "agent-message", id: event.id, turnId, text, ...(phase ? { phase } : {}), streaming: true, occurredAt: event.occurredAt, }; items.push(item); unsettledStreamedMessages.add(item); if (messageKey) identifiedMessages.set(messageKey, item); rememberAgentResponse(turnId, item, false); break; } case "agent.message.completed": { knownMessagePrefixTurns.add(turnId); const text = stringValue(payload.text); const messageId = stringValue(payload.messageId); const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null; const phase = assistantMessagePhase(payload.phase); // Reconcile the most recent same-turn agent message — even when // activity (tool calls, reasoning) landed after its deltas — so the // completed text never duplicates the streamed one. let openIndex = messageKey ? items.indexOf(identifiedMessages.get(messageKey)!) : -1; for (let index = messageKey ? -1 : items.length - 1; index >= 0; index -= 1) { const candidate = items[index]; if (candidate?.kind === "agent-message" && candidate.turnId === turnId) { openIndex = index; break; } } const candidate = openIndex >= 0 ? items[openIndex] : undefined; const open: AgentMessageItem | undefined = candidate?.kind === "agent-message" ? candidate : undefined; if ( open && (messageKey || open.streaming || !open.text || text === open.text || text.startsWith(open.text)) ) { // Identity makes the final text authoritative even if it corrects the // draft; legacy receipts still require an extension match. if (!open.text || (text && (messageKey || text.startsWith(open.text)))) { open.text = text || open.text; } // A phase-less settlement mirror (the worker's final-output receipt) // must not erase the provider-declared phase captured by the SDK item. if (phase) open.phase = phase; // Completion time is what the footer shows ("finished at"); the // first-delta stamp stays as the stream's start. if (open.startedAt === undefined && open.occurredAt !== event.occurredAt) { open.startedAt = open.occurredAt; } open.streaming = false; open.occurredAt = event.occurredAt; open.annotationSource = text && open.text === text ? { kind: "assistant_message", eventId: event.id, eventType: "agent.message.completed", sequence: event.sequence, turnId, text, } : undefined; // The SDK can emit a hosted-tool item only after its provider-native // operation has completed, even though answer deltas were already // streamed. The completed message event is the durable ordering // authority, so move the reconciled row after any intervening tool // activity instead of leaving completed web searches below the answer. if (openIndex < items.length - 1) { items.splice(openIndex, 1); items.push(open); } unsettledStreamedMessages.delete(open); rememberAgentResponse(turnId, open, true); break; } if (text) { const item: AgentMessageItem = { kind: "agent-message", id: event.id, turnId, text, ...(phase ? { phase } : {}), streaming: false, occurredAt: event.occurredAt, annotationSource: { kind: "assistant_message", eventId: event.id, eventType: "agent.message.completed", sequence: event.sequence, turnId, text, }, }; items.push(item); if (messageKey) identifiedMessages.set(messageKey, item); rememberAgentResponse(turnId, item, true); } break; } case "agent.reasoning.delta": { const text = reasoningText(event.payload); if (!text) { break; } const open = last(); if (open?.kind === "reasoning" && open.streaming && open.turnId === turnId) { open.text += reasoningDeltaJoiner(open.text, text) + text; break; } closeStreamingTail(); items.push({ kind: "reasoning", id: event.id, turnId, text, streaming: true, occurredAt: event.occurredAt, }); break; } case "agent.toolCall.created": { const name = typeof payload.name === "string" ? payload.name : "tool"; const display = parseToolDisplayMetadata(payload.display); const callId = typeof payload.id === "string" ? payload.id : null; const args = payload.arguments ?? null; closeStreamingTail(); if (callId && humanInputToolCallIds.has(callId)) { // The matching session.humanInput.requested / response lifecycle owns // the visible chat card. Rendering the raw tool too would hide the // decision behind a duplicate generic "Ask" step. break; } // A worker started or messaged through tool_invoke is the same worker. const invoked = invokedTool(name, args); const workerName = invoked?.name ?? name; const workerArgs = invoked ? invoked.arguments : args; if ( toolMatchesLeaf(workerName, WORKER_SPAWN_TOOL) || toolMatchesLeaf(workerName, WORKER_MESSAGE_TOOL) ) { const spawn = toolMatchesLeaf(workerName, WORKER_SPAWN_TOOL); const title = spawn ? workerTitle(workerArgs) : null; const item: WorkerItem = { kind: "worker", id: event.id, turnId, callId, action: spawn ? "spawn" : "message", prompt: workerPrompt(workerArgs), ...(title ? { title } : {}), workerSessionId: extractSessionRef(workerArgs), failure: null, status: "running", occurredAt: event.occurredAt, }; items.push(item); if (callId) callsById.set(callId, item); break; } if (LANDMARK_ONLY_TOOL_LEAVES.has(mcpToolLeaf(name))) { // Goal/memory landmarks arrive as goal.* / memory.* events. break; } // Live Responses `web_search_call` events and the later SDK // `RunToolCallItem` share the same item id. Merge so mid-stream cards // do not duplicate when the step finally materializes. if (callId) { const existing = toolCallsById.get(callId); if (existing) { if (display) existing.display = display; if (args != null) { existing.arguments = args; } if (payload.raw !== undefined) { existing.raw = mergeToolCallRaw(existing.raw, payload.raw); } existing.status = providerNativeToolStatus(existing.raw); break; } } const item: ToolCallItem = { kind: "tool-call", id: event.id, turnId, callId, name, ...(display ? { display } : {}), arguments: args, output: undefined, truncation: null, // The provider-native item drives the per-tool renderers (apply_patch // operation, computer_call action, web_search providerData, …). raw: payload.raw, status: providerNativeToolStatus(payload.raw), occurredAt: event.occurredAt, }; items.push(item); if (callId) { toolCallsById.set(callId, item); callsById.set(callId, item); } break; } case "agent.toolCall.output": { const callId = typeof payload.id === "string" ? payload.id : null; const target = findOpenCall(items, callId, callsById); if (!target) { break; } resolvePresentationWait(event, turnId ?? target.turnId); if (target.kind === "worker") { // A worker spawn/message that returns an error flag (or an MCP // isError result) settles to "failed" too, so WorkerRow surfaces it. target.status = isErrorOutput(payload) ? "failed" : "complete"; target.workerSessionId = target.workerSessionId ?? extractSessionRef(payload.output); target.failure = target.status === "failed" ? workerFailure(payload.output) : null; // A spawned or re-messaged worker owes this session a result. if (target.status === "complete" && target.workerSessionId) { if (target.action === "spawn") workerSessions.add(target.workerSessionId); if (workerSessions.has(target.workerSessionId)) { pendingWorkerSessions.add(target.workerSessionId); } } break; } // An output carrying an explicit error flag (or an MCP isError result) // settles the tool to "failed" so the renderer can surface it loudly. const truncation = toolCallTruncation(payload); const previewFact = toolOutputPreviewFact(payload); const previewRead = previewFact ? options.toolOutputs?.(event.id, event.sequence, event.turnId ?? null) : undefined; // An exact read that landed replaced this event; a forged fact on an // exact output must not ask again. const outputPreview: ToolOutputPreview | null = previewFact && previewRead?.state !== "loaded" ? { shape: previewFact.shape, originalBytes: previewFact.originalBytes, previewBytes: previewFact.previewBytes, state: previewRead?.state ?? "preview", ...(previewRead ? { load: previewRead.load } : {}), } : null; target.status = isErrorOutput(payload) ? "failed" : "complete"; target.output = Object.prototype.hasOwnProperty.call(payload, "output") ? payload.output : truncation && typeof payload.preview === "string" ? payload.preview : payload.output; target.truncation = truncation; target.outputPreview = outputPreview; const sourceText = timelineAnnotationToolOutputText( Object.prototype.hasOwnProperty.call(payload, "output") ? payload.output : payload.preview, ); // Annotations anchor in the exact text, which a preview is not. if (sourceText !== null && !outputPreview) { target.annotationSource = { kind: "tool_output", eventId: event.id, eventType: "agent.toolCall.output", sequence: event.sequence, turnId, text: sourceText, label: target.name, }; } // A refused Skill, instruction, or Knowledge change is told to the // person outside the folded work row, whatever the agent then says. const learningRefusal = learningRefusalFromToolOutput(payload.output); if (learningRefusal) { const key = JSON.stringify([ turnId ?? target.turnId, learningRefusal.destination, learningRefusal.reason, learningRefusal.skillId ?? null, ]); if (!learningRefusalNotices.has(key)) { learningRefusalNotices.add(key); closeStreamingTail(); items.push({ kind: "notice", id: `${event.id}-learning-refusal`, tone: "failed", text: learningRefusalText(t, learningRefusal), learningRefusal, occurredAt: event.occurredAt, }); } } break; } case "turn.queued": { const queuedTurnId = turnId ?? (typeof payload.turnId === "string" ? payload.turnId : null); if (queuedTurnId) { queuedAtByTurn.set(queuedTurnId, event.occurredAt); } break; } case "turn.started": { if (turnId) clearUnscopedTerminalTracking(); if (!turnId) break; const queuedAt = queuedAtByTurn.get(turnId); if (!queuedAt) break; // Recovery starts the same logical turn again. Queue latency belongs to // the first admission only, so consume the queued timestamp after its // first projection instead of manufacturing another queue span. queuedAtByTurn.delete(turnId); const queueStart = previousTurnEndedAt && Date.parse(previousTurnEndedAt) > Date.parse(queuedAt) && Date.parse(previousTurnEndedAt) <= Date.parse(event.occurredAt) ? previousTurnEndedAt : queuedAt; closeStreamingTail(); settleStartupPhase( "queue", "complete", elapsedDurationMs(queueStart, event.occurredAt), null, 0, queueStart, `${turnId}-queue`, ); break; } case "sandbox.operation.started": case "sandbox.operation.completed": case "sandbox.operation.failed": { const name = typeof payload.name === "string" ? payload.name : "sandbox"; const status = event.type.endsWith(".failed") ? payload.expectedTransition === true ? "cancelled" : "failed" : event.type.endsWith(".completed") ? "complete" : "running"; const origin = payload.origin === "created" || payload.origin === "restored" || payload.origin === "resumed" ? payload.origin : null; const startupPhase = startupPhaseForSandboxOperation(name); if (startupPhase) { closeStreamingTail(); settleStartupPhase( startupPhase, status, numberOrNull(payload.durationMs), origin, 0, undefined, undefined, status === "cancelled" ? (startupLifecycleWaitReason(payload) ?? (payload.failureCode === "rotation_in_progress" ? "rotation_in_progress" : undefined)) : undefined, ); break; } // Routine per-turn platform plumbing that runs before EVERY turn to // guarantee box contents survive a re-warm — NOT the agent redoing work: // - repository-clone: idempotent clone check + off-manifest token re-seed; // - file-resource-download: idempotent `if [ ! -f ] then curl` (skips // when the attached file is already on the box — see // sandboxFileDownloadCommand), so an uploaded image is not re-fetched; // the operation still emits every turn even when it does nothing. // Rendering either every turn reads as churn. Only FAILURES surface, and // they surface loudly — the failed event below creates its own item even // without a started row. // - optional-repository-access: the worker's per-turn report of an // automatically attached repository that sat the turn out, reported // like a skipped optional clone (event payload + log, no row). if ( (name === "repository-clone" || name === "file-resource-download" || name === "optional-repository-access") && status !== "failed" ) { break; } const existing = findOpenSandbox(items, name); if (existing && status !== "running") { existing.status = status; if (origin) existing.origin = origin; const message = failureMessage(t, payload); if (message) { existing.output = existing.output ? `${existing.output}\n${message}` : message; } break; } if (!existing) { closeStreamingTail(); items.push({ kind: "sandbox", id: event.id, turnId, name, command: typeof payload.command === "string" ? payload.command : null, output: failureMessage(t, payload) ?? "", origin, status, occurredAt: event.occurredAt, }); } break; } case "agent.model.request": { const requestPhase = typeof payload.phase === "string" ? payload.phase : null; const status = requestPhase === "started" ? "running" : requestPhase === "first_byte" || requestPhase === "first_event" ? "complete" : requestPhase === "failed" || requestPhase === "timed_out" ? "failed" : null; if (status) { if (status === "running") closeStreamingTail(); settleStartupPhase( "provider_first_byte", status, status === "running" ? null : numberOrNull(payload.durationMs), null, status === "running" ? 2 : 1, ); } break; } case "sandbox.command.output.delta": { // `chunk` is the canonical wire field; text/output are legacy shapes. const text = typeof payload.chunk === "string" ? payload.chunk : typeof payload.text === "string" ? payload.text : typeof payload.output === "string" ? payload.output : ""; if (!text) { break; } // Attach to the named operation when the payload carries one; // otherwise the latest running operation is the best available owner. const open = (typeof payload.name === "string" ? findOpenSandbox(items, payload.name) : undefined) ?? [...items] .reverse() .find( (item): item is SandboxItem => item.kind === "sandbox" && item.status === "running", ); if (open) { open.output += text; } break; } case "session.status.changed": { const status = payload.status; if (!isSessionStatus(status)) { break; } const blocked = status === "requires_action" && payload.code === "admission_blocked"; if (!blocked && openAdmissionBlocks.length > 0) { for (const open of openAdmissionBlocks) open.resolvedAt = event.occurredAt; openAdmissionBlocks = []; } if (status === "running") resolvePresentationWait(event, turnId); // Only attention-worthy statuses earn a timeline divider. queued / // running / idle are machinery telemetry: the header pill carries the // live status, the shimmer says "running", and the turn chip's duration // facet says how long — a stale "idle · 27s" row is pure noise, // especially in historical traces. if (!ATTENTION_STATUSES.has(status)) { break; } const previous = [...items] .reverse() .find((item): item is SessionStatusItem => item.kind === "session-status"); if ( previous?.status === status && !previous.resolvedAt && Boolean(previous.blocked) === blocked ) { break; } const item: SessionStatusItem = { kind: "session-status", id: event.id, status, occurredAt: event.occurredAt, ...(blocked ? { blocked: true as const } : {}), }; if (blocked) { openAdmissionBlocks.push(item); items.push(item); } else if (status === "requires_action") rememberPresentationWait(item, turnId); else { items.push(item); if (status === "failed") { const key = turnId ?? presentationTurnId; const failures = presentationFailures.get(key) ?? []; failures.push(item); presentationFailures.set(key, failures); } } break; } case "sandbox.recovery.selected": { if (payload.mode === "fresh_workspace" && typeof payload.text === "string") items.push({ kind: "notice", id: event.id, tone: "input", text: payload.text, occurredAt: event.occurredAt, }); break; } case "subscription.account.switched": { // One neutral receipt, not its provider-specific compatibility alias. // Never expose connection ids, personal labels, or provider error text. const text = payload.reason === "return_to_preferred" ? t("timeline.projection.returnedToYourPreferredModel") : payload.reason === "failover_cross_provider" ? t("timeline.projection.switchedToAFallbackModel") : t("timeline.projection.switchedToAnotherSubscriptionAccount"); items.push({ kind: "notice", id: event.id, tone: "input", text, occurredAt: event.occurredAt, }); break; } case "codex.capacity.waiting": case "turn.capacity_waiting": { const capacityWait = capacityWaitPresentation(t, event.type, payload, turnId); const item: NoticeItem = { kind: "notice", id: event.id, tone: "waiting", text: capacityWait.text, capacityWait: { turnId: capacityWait.turnId, label: capacityWait.label, detail: capacityWait.detail, }, occurredAt: event.occurredAt, }; // A re-armed wait continues the same blocked span; one live wait per turn. openCapacityWaits = openCapacityWaits.filter((open) => { if ((open.capacityWait?.turnId ?? null) !== turnId) return true; open.resolvedAt = event.occurredAt; return false; }); items.push(item); openCapacityWaits.push(item); break; } case "session.requiresAction": { finalizeOpen(turnId, "complete", event.occurredAt); rememberPresentationWait( { kind: "notice", id: event.id, tone: "waiting", text: t("timeline.projection.approvalNeededTheTurnIs"), approval: true, occurredAt: event.occurredAt, }, turnId, ); break; } case "session.context.compaction.requested": case "session.context.compaction.started": { closeStreamingTail(); settleOrPushContextCompaction(items, { id: event.id, turnId, phase: "started", trigger: compactionTrigger(payload), estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore), estimatedTokensAfter: null, skipReason: null, providerRejection: null, implementation: typeof payload.implementation === "string" ? payload.implementation : null, occurredAt: event.occurredAt, }); break; } case "session.context.compacted": { closeStreamingTail(); settleOrPushContextCompaction(items, { id: event.id, turnId, phase: "compacted", trigger: compactionTrigger(payload), estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore), estimatedTokensAfter: numberOrNull(payload.estimatedTokensAfter), skipReason: null, providerRejection: null, implementation: typeof payload.implementation === "string" ? payload.implementation : null, occurredAt: event.occurredAt, }); break; } case "session.context.compaction.skipped": { closeStreamingTail(); settleOrPushContextCompaction(items, { id: event.id, turnId, phase: "skipped", trigger: compactionTrigger(payload), estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore), estimatedTokensAfter: null, skipReason: typeof payload.reason === "string" ? payload.reason : null, providerRejection: compactionProviderRejection(payload), implementation: typeof payload.implementation === "string" ? payload.implementation : null, occurredAt: event.occurredAt, }); break; } case "turn.recovery.requested": { // Recovery requests are durable control-plane evidence, not a user // message or proof that recovery succeeded. Live state already exposes // a genuinely recovering session; keep the raw event in Debug/audit. if (turnId) { startupRecoveryRevisionByTurn.set( turnId, (startupRecoveryRevisionByTurn.get(turnId) ?? 0) + 1, ); if ( !event.duplicateOfEventId && (!event.turnAssociation || event.turnAssociation === "current") ) { // Human Retry reopens the existing logical turn, unlike a new // prompt. Keep failure evidence, but do not let its old settlement // close the recovered turn's work surface. for (const item of items) { if (item.kind === "turn-end" && item.turnId === turnId && item.outcome === "failed") { item.resumedAt ??= event.occurredAt; } } for (const item of presentationFailures.get(turnId) ?? []) { item.resolvedAt = event.occurredAt; } presentationFailures.delete(turnId); } } break; } case "turn.event.rejected_late": { // Attempt-fence rejections prove stale callbacks did not alter current // truth. They are useful diagnostics but never actionable chat content, // regardless of the rejected event type. Debug/audit retains the event. break; } case "tool.auth_needed": case "credential.auth_needed": { const capability = capabilityAuthorizationRequest(payload.capability); const setupRequest = customMcpSetupRequest(payload.setupRequest); if ( event.type === "tool.auth_needed" && !stringValue(payload.toolName) && stringValue(payload.serverId) !== "codex_apps" ) { // Historical optional-MCP initialize/tools-list credential misses are // setup availability, not evidence of a concrete conversational tool // call. Keep the raw event in Debug/audit but do not manufacture an // actionable reconnect card in the transcript. Actual tools/call auth // failures carry a concrete toolName and remain visible. break; } // Keep the whole structured payload — the renderer turns it into a clean // inline reconnect card, and the app starts the recovery flow off the // connectionId/resource. Losing it to a plain-text notice was the ugly // "linear.app needs to be reconnected." line users complained about. closeStreamingTail(); items.push({ kind: "auth-needed", id: event.id, turnId, serverId: typeof payload.serverId === "string" ? payload.serverId : null, canonicalServerId: typeof payload.canonicalServerId === "string" ? payload.canonicalServerId : null, connectionSubjectScope: payload.connectionSubjectScope === "subject" || payload.connectionSubjectScope === "workspace" ? payload.connectionSubjectScope : null, source: capability || setupRequest ? "capability" : event.type === "tool.auth_needed" ? "tool" : "credential", providerDomain: stringValue(payload.providerDomain), connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null, authoritySource: payload.authoritySource === "host" ? "host" : null, reason: authNeededReason( payload.authoritySource === "host" && typeof payload.authorizationUrl === "string" ? (payload.hostReason ?? payload.reason) : payload.reason, ), scopes: stringList(payload.scopes), resource: typeof payload.resource === "string" ? payload.resource : null, toolName: typeof payload.toolName === "string" ? payload.toolName : null, authorizationUrl: typeof payload.authorizationUrl === "string" ? payload.authorizationUrl : null, capability, setupRequest, requestedSandboxId: stringValue(payload.requestedSandboxId) || null, ...(payload.purpose === "chrome" ? { purpose: "chrome" as const } : {}), occurredAt: event.occurredAt, }); break; } case "usage.exhausted": { allowanceNotice(event, turnId); break; } case "turn.completed": { steerBubblesAwaitingHandover = []; // A standalone manual compaction uses the turn ledger for fencing and // recovery, but it is maintenance rather than a conversational turn. // The dedicated context-compaction landmark is the complete UI truth; // adding a generic turn chip would falsely make it look like an // extra agent response. if (payload.maintenance === "context_compaction") { finalizeOpen(turnId, "complete", event.occurredAt); takeAgentResponse(turnId); takePendingWaitOutcome(turnId); break; } // Credit exhaustion arrives as a NOMINALLY completed turn (`detail: // "insufficient Opengeni credits"`, `segmentLimit: "budget_exhausted"`) // — the engine ended the segment early, it did not finish the work. // Rendering it as a clean "complete" turn is a lie that leaves the // session looking healthy while every future turn silently dies, so it // projects exactly like a failed turn plus an explicit notice. const allowance = parseAllowanceExhaustedRefusal(payload); // The task's own usage limit refused the next model request. That is a // person's setting doing its job, not a failure or a billing problem: // the turn settles complete and one calm row says what stopped it. if (payload.segmentLimit === SESSION_USAGE_LIMIT_SEGMENT) { finalizeOpen(turnId, "complete", event.occurredAt); takeAgentResponse(turnId); takePendingWaitOutcome(turnId); items.push(turnEndItem(event, "complete", null)); items.push(sessionUsageLimitNotice(t, event, payload)); break; } if (allowance) { finalizeOpen(turnId, "complete", event.occurredAt); takeAgentResponse(turnId); takePendingWaitOutcome(turnId); items.push(turnEndItem(event, "failed", ALLOWANCE_TURN_END_TEXT(t))); allowanceNotice(event, turnId); break; } if (isCreditExhaustionPayload(payload)) { finalizeOpen(turnId, "complete", event.occurredAt); takeAgentResponse(turnId); takePendingWaitOutcome(turnId); items.push(turnEndItem(event, "failed", CREDIT_EXHAUSTION_MESSAGE(t))); items.push({ kind: "notice", id: event.id, tone: "failed", text: CREDIT_EXHAUSTION_MESSAGE(t), occurredAt: event.occurredAt, }); break; } const latestAgentResponse = takeAgentResponse(turnId); const finalOutput = stringValue(payload.output); const visibleFinalOutput = stripOpaqueCitationTokens(finalOutput).trim(); const visibleTrackedResponse = stripOpaqueCitationTokens( latestAgentResponse?.item.text ?? "", ).trim(); const finalOutputMirrorsCommentary = latestAgentResponse?.item.phase === "commentary" && visibleTrackedResponse === visibleFinalOutput; const hasAuthoritativeFinalOutput = Boolean(visibleFinalOutput) && !finalOutputMirrorsCommentary; const pendingWaitOutcome = takePendingWaitOutcome(turnId); if (hasAuthoritativeFinalOutput) { // The final message normally lands as its own completion before // `turn.completed` (older workers commit a copy with it), but legacy or // partially compacted ledgers may retain only the terminal output // receipt. Keep that authoritative response visible instead of // leaving it trapped in raw audit data. if ( latestAgentResponse && !latestAgentResponse.completed && finalOutput.startsWith(latestAgentResponse.item.text) ) { latestAgentResponse.item.text = finalOutput; latestAgentResponse.item.streaming = false; if ( latestAgentResponse.item.startedAt === undefined && latestAgentResponse.item.occurredAt !== event.occurredAt ) { latestAgentResponse.item.startedAt = latestAgentResponse.item.occurredAt; } latestAgentResponse.item.occurredAt = event.occurredAt; const responseIndex = items.indexOf(latestAgentResponse.item); if (responseIndex >= 0 && responseIndex < items.length - 1) { items.splice(responseIndex, 1); items.push(latestAgentResponse.item); } } else if (visibleTrackedResponse !== visibleFinalOutput) { items.push({ kind: "agent-message", id: `${event.id}-output-message`, turnId, text: finalOutput, streaming: false, occurredAt: event.occurredAt, }); } } finalizeOpen(turnId, "complete", event.occurredAt); const completedTurn = turnEndItem(event, "complete", null); items.push(completedTurn); if (turnCompletedWithEmptyFinalReply(payload)) { items.push({ kind: "notice", id: `${event.id}-empty-final-reply`, tone: "input", text: EMPTY_FINAL_REPLY_NOTICE, recordedOutcome: true, occurredAt: event.occurredAt, }); } const hasCompletedFinalResponse = latestAgentResponse?.completed === true && latestAgentResponse.item.phase !== "commentary" && Boolean(visibleTrackedResponse); if (!hasAuthoritativeFinalOutput && pendingWaitOutcome) { // Preserve existing prose, including delta-only streams, when a // trailing wait yields without a terminal output receipt. completedTurn.preserveWaitResponse = true; } if (!hasAuthoritativeFinalOutput && !hasCompletedFinalResponse && pendingWaitOutcome) { const notice: NoticeItem = { kind: "notice", id: `${pendingWaitOutcome.id}-visible-outcome`, tone: "waiting", text: waitingOutcomeText(t, pendingWaitOutcome.reason), recordedOutcome: true, ...(pendingWorkerSessions.size > 0 ? { waitingAgents: pendingWorkerSessions.size } : {}), occurredAt: pendingWaitOutcome.occurredAt, }; items.push(notice); openWaitNotice = notice; } break; } case "turn.failed": { steerBubblesAwaitingHandover = []; takeAgentResponse(turnId); takePendingWaitOutcome(turnId); const hadActivity = hasTurnActivity(items, turnId); // Credit death can hide behind fields `failureMessage` doesn't read // (detail/segmentLimit), so classify the whole payload before falling // back to the generic error/message extraction. const allowance = parseAllowanceExhaustedRefusal(payload); const failureText = allowance ? ALLOWANCE_TURN_END_TEXT(t) : isCreditExhaustionPayload(payload) ? CREDIT_EXHAUSTION_MESSAGE(t) : failureMessage(t, payload); // The TURN failed — the in-flight items did not. Chip doctrine: red is // spent once, on the turn-level outcome. Items caught mid-flight read // as calm "interrupted" (same as turn.cancelled); an item that itself // failed keeps its own failed status from its output event. finalizeOpen(turnId, "cancelled", event.occurredAt); items.push(turnEndItem(event, "failed", failureText)); // A compaction failure names its outcome and keeps the recorded // provider text behind Details. The turn's own failed compaction // landmark is the one place that shows it: it receives the recorded // text, and no separate notice repeats it. // Retry reopens the same logical turn, so only a landmark from this // attempt (after the turn's previous settlement) can own this failure. const failure = allowance ? null : presentTurnFailureText(t, failureText); let landmarkIndex = -1; if (failure?.detail !== undefined && turnId !== null) { // Skip the turn-end just pushed for this failure. for (let index = items.length - 2; index >= 0; index -= 1) { const item = items[index]; if (item?.kind === "turn-end" && item.turnId === turnId) break; if ( item?.kind === "context-compaction" && item.turnId === turnId && item.phase === "skipped" && item.skipReason === "summarization_failed" ) { landmarkIndex = index; break; } } } const landmark = landmarkIndex >= 0 ? items[landmarkIndex] : undefined; if (landmark?.kind === "context-compaction" && failure?.detail !== undefined) { items[landmarkIndex] = { ...landmark, failureDetail: failure.detail }; } if (allowance) { allowanceNotice(event, turnId); } else if (!hadActivity && !landmark) { items.push({ kind: "notice", id: event.id, tone: "failed", text: failure?.summary ?? t("timeline.projection.theTurnFailed"), ...(failure?.detail ? { details: { label: t("timeline.projection.details"), value: failure.detail } } : {}), occurredAt: event.occurredAt, }); } break; } case "turn.superseded": case "turn.cancelled": { // Only an explicit batch receipt proves a supersession never ran: // a partial history may omit a real handover's earlier execution. if (event.type === "turn.superseded" && payload.beforeAttempt === true) { break; } // A withdrawn queued turn must not manufacture an empty response. if (event.type === "turn.cancelled" && turnId && !prescan.startedTurnIds.has(turnId)) { break; } // The turn handed over to a Steer at a safe boundary: its next model // step, right after a context compaction, or while it only waited (an // approval, a question or capacity; those supersessions name their // target). No running work was stopped: the agent reads the message // next and carries on, so the turn closes quietly instead of as // interrupted. Only the API hard interrupt stops a live attempt. const steerReplacedWait = (payload.reason === "steer" || payload.reason === "agent_steer") && (typeof payload.targetTurnId === "string" || typeof payload.targetUpdateId === "string"); if ( event.type === "turn.superseded" && (payload.atModelStep === true || payload.deferredUntilCompaction === true || steerReplacedWait) ) { takeAgentResponse(turnId); takePendingWaitOutcome(turnId); // A Steer cut the model's answer short. Its unfinished streamed text // stays visible, after the messages that did finish (their // completions arrive only when the cut request ends). When those // deltas are absent (an archived history) the recorded cut-off text // stands in, as a plain progress note rather than an error. Either // way the answer stays above the Steer's own bubble, which was drawn // while this request still streamed: a late completion must not // carry finished text below it either. const cutOffText = stringValue(payload.cutOffText); const steerBoundary = Math.min( ...steerBubblesAwaitingHandover .map((bubble) => items.indexOf(bubble)) .filter((index) => index >= 0), items.length, ); const isTurnMessage = (item: TimelineItem): item is AgentMessageItem => item.kind === "agent-message" && item.turnId === turnId; // Finished messages a late completion moved below the Steer go // back above it, in their order; unfinished ones follow them. const settledBelow = items .slice(steerBoundary) .filter( (item): item is AgentMessageItem => isTurnMessage(item) && !unsettledStreamedMessages.has(item), ); const unsettled = cutOffText ? items.filter( (item): item is AgentMessageItem => isTurnMessage(item) && unsettledStreamedMessages.has(item), ) : []; const moved: AgentMessageItem[] = [...settledBelow, ...unsettled]; if (cutOffText && unsettled.length === 0) { closeStreamingTail(); moved.push({ kind: "agent-message", id: `${event.id}:cut-off`, turnId, text: cutOffText, phase: "commentary", streaming: false, occurredAt: event.occurredAt, }); } if (moved.length > 0) { let insertAt = items.length; for (const item of moved) { const index = items.indexOf(item); if (index < 0) continue; items.splice(index, 1); unsettledStreamedMessages.delete(item); } if (steerBubblesAwaitingHandover.length > 0) { const bubbleIndexes = steerBubblesAwaitingHandover .map((bubble) => items.indexOf(bubble)) .filter((index) => index >= 0); if (bubbleIndexes.length > 0) insertAt = Math.min(...bubbleIndexes); } else { // No Steer bubble in view (an API-only history): keep the text // right after this turn's other work. for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]!; if ("turnId" in item && item.turnId === turnId) { insertAt = index + 1; break; } } } items.splice(insertAt, 0, ...moved); } steerBubblesAwaitingHandover = []; // A call still waiting for approval or an answer never ran. finalizeOpen(turnId, steerReplacedWait ? "cancelled" : "complete", event.occurredAt); items.push({ ...turnEndItem(event, "complete", null), handedOver: true }); break; } steerBubblesAwaitingHandover = []; takeAgentResponse(turnId); takePendingWaitOutcome(turnId); const hadActivity = hasTurnActivity(items, turnId); finalizeOpen(turnId, "cancelled", event.occurredAt); items.push(turnEndItem(event, "cancelled", null)); if (!hadActivity) { items.push({ kind: "notice", id: event.id, tone: "cancelled", text: t("timeline.projection.interrupted"), occurredAt: event.occurredAt, }); } break; } case "instruction.confirmation.recovered": { const receipt = asRecord(payload.receipt); if (receipt.outcome !== "published") break; closeStreamingTail(); items.push({ kind: "notice", id: event.id, tone: "input", text: t("timeline.projection.yourPreviouslyApprovedWorkspaceInstruction"), occurredAt: event.occurredAt, }); break; } case "knowledge.confirmation.recovered": case "knowledge.source.prepared": case "knowledge.source.failed": { const receipt = asRecord(payload.receipt); const outcome = event.type === "knowledge.source.failed" ? "failed" : receipt.outcome; if ( (event.type !== "knowledge.confirmation.recovered" && typeof payload.fileId !== "string") || !["published", "pending", "rejected", "archived", "failed"].includes(String(outcome)) ) break; closeStreamingTail(); items.push({ kind: "knowledge", id: event.id, turnId, fileId: typeof payload.fileId === "string" ? payload.fileId : undefined, filename: typeof payload.filename === "string" ? payload.filename : undefined, entryId: typeof receipt.entryId === "string" ? receipt.entryId : undefined, status: outcome === "failed" ? "failed" : "complete", outcome: outcome as "published" | "pending" | "rejected" | "archived" | "failed", occurredAt: event.occurredAt, }); break; } case "memory.saved": case "memory.corrected": { // A first-party memory write is a discrete step, not streamed text, so it // ends whatever was streaming (mirrors tool/sandbox pushes). A payload // missing the memory id is malformed and dropped rather than shown blank. const memory = memoryItem(event.id, event.type, turnId, payload, event.occurredAt); if (memory) { closeStreamingTail(); items.push(memory); } break; } case "codex.fleet.decision": { const decision = fleetDecisionItem(event, payload); if (decision) { closeStreamingTail(); items.push(decision); } break; } case "goal.set": case "goal.updated": case "goal.completed": case "goal.paused": case "goal.resumed": case "goal.cleared": case "goal.held": case "goal.continuation": { if (event.type === "goal.held" && payload.actor === "agent") { rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId); } // Agent tool mutations already appear as tool-call rows in the activity // cluster. Re-emitting them as GoalRow landmarks splits "N steps" mid-turn. if (shouldSuppressAgentGoalLandmark(event.type, payload)) { break; } items.push({ kind: "goal", id: event.id, action: event.type.slice("goal.".length) as GoalItem["action"], text: event.type === "goal.held" && typeof payload.reason === "string" && payload.reason ? payload.reason : goalText(payload), occurredAt: event.occurredAt, }); break; } case "session.wait.started": { if (payload.actor === "agent") { rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId); } break; } default: break; } } if (pendingTurnSteps) items.push(pendingTurnSteps); markRepeatedMachineRequests(items); const sourceEventsById = new Map(events.map((event) => [event.id, event])); for (const item of items) { const source = sourceEventsById.get(item.id); const canonical = "annotationSource" in item ? item.annotationSource : undefined; item.sourceEvents = [ ...(source ? [{ eventId: source.id, sequence: source.sequence }] : []), ...(canonical && canonical.eventId !== source?.id ? [{ eventId: canonical.eventId, sequence: canonical.sequence }] : []), ]; if (item.kind === "agent-message") { item.text = stripOpaqueCitationTokens(item.text); } } return items; } function realtimeVoiceMessage(payload: Record): { text: string; presentation: { kind: "realtime_voice" | "realtime_voice_handoff"; context: string; }; } | null { const presentation = asRecord(payload.presentation); const visibleText = stringValue(payload.text); if (presentation.kind === "realtime_voice" || presentation.kind === "realtime_voice_handoff") { const context = stringValue(presentation.context); return visibleText && context ? { text: visibleText, presentation: { kind: presentation.kind, context } } : null; } return null; } function humanInputRequestsById(events: SessionEvent[]): Map> { const requests = new Map>(); for (const event of events) { if (event.type !== "session.humanInput.requested") continue; const request = asRecord(asRecord(event.payload).request); const requestId = stringValue(request.id); if (requestId) requests.set(requestId, request); } return requests; } function humanInputItemFromResponse( event: SessionEvent, payload: Record, requests: Map>, ): HumanInputItem | null { const response = asRecord(payload.response); const outcome = response.outcome; if ( outcome !== "answered" && outcome !== "skipped" && outcome !== "expired" && outcome !== "cancelled" ) { return null; } const requestId = stringValue(payload.requestId); const request = requests.get(requestId); const questions = humanInputQuestions(request?.questions); const normalizedResponse: HumanInputResponse = outcome === "answered" ? { outcome, answers: Array.isArray(response.answers) ? response.answers .map(asRecord) .map(humanInputAnswer) .filter((answer): answer is HumanInputAnswer => answer !== null) : [], } : { outcome }; const questionsById = new Map(questions.map((question) => [question.id, question])); const answers = normalizedResponse.outcome === "answered" ? normalizedResponse.answers.map((answer) => humanInputAnswerSummary(answer, questionsById.get(answer.questionId)), ) : []; return { kind: "human-input", id: event.id, turnId: event.turnId ?? null, requestId, questions, response: normalizedResponse, answers, occurredAt: event.occurredAt, }; } function humanInputQuestions(value: unknown): HumanInputQuestion[] { if (!Array.isArray(value)) return []; return value.map(asRecord).flatMap((question) => { const id = stringValue(question.id); const prompt = stringValue(question.prompt); const kind = question.kind; if ( !id || !prompt || (kind !== "text" && kind !== "single_select" && kind !== "multi_select" && kind !== "secret") ) { return []; } // Secret questions keep their spec (names and hints only; never a value). const secret = kind === "secret" ? humanInputSecretSpec(question.secret) : null; if (kind === "secret" && !secret) return []; const options = Array.isArray(question.options) ? question.options.map(asRecord).flatMap((option) => { const optionId = stringValue(option.id); const label = stringValue(option.label); return optionId && label ? [{ id: optionId, label, description: stringValue(option.description) || null }] : []; }) : []; return [ { id, kind, prompt, label: stringValue(question.label) || null, helpText: stringValue(question.helpText) || null, options, required: question.required === true, allowOther: question.allowOther === true, ...(secret ? { secret } : {}), }, ]; }); } function humanInputSecretSpec(value: unknown): HumanInputSecretSpec | null { const spec = asRecord(value); const item = stringValue(spec.item); if (!item) return null; return spec as unknown as HumanInputSecretSpec; } function humanInputSecretResult(value: unknown): HumanInputSecretAnswerResult | null { if (value === undefined || value === null) return null; const result = asRecord(value); const reference = stringValue(result.reference); const vaultName = stringValue(result.vaultName); const itemName = stringValue(result.itemName); const saveTo = result.saveTo; if ( !reference || !vaultName || !itemName || (saveTo !== "user" && saveTo !== "task" && saveTo !== "workspace") ) { return null; } return { reference, username: stringValue(result.username) || null, vaultName, itemName, saveTo, }; } function humanInputAnswer(value: Record): HumanInputAnswer | null { const questionId = stringValue(value.questionId); if (!questionId) return null; const secret = humanInputSecretResult(value.secret); return { questionId, values: Array.isArray(value.values) ? value.values.filter((answer): answer is string => typeof answer === "string") : [], ...(typeof value.other === "string" && value.other ? { other: value.other } : {}), ...(secret ? { secret } : {}), }; } function humanInputAnswerSummary( answer: HumanInputAnswer, question: HumanInputQuestion | undefined, ): HumanInputAnswerSummary { const optionLabels = new Map(question?.options.map((option) => [option.id, option.label]) ?? []); const values = answer.values.map((value) => optionLabels.get(value) || value); if (answer.other) values.push(answer.other); const fallbackLabel = answer.questionId .replace(/[_-]+/g, " ") .replace(/\b\w/g, (character) => character.toUpperCase()); return { questionId: answer.questionId, label: question?.label || question?.prompt || fallbackLabel, prompt: question?.prompt || fallbackLabel, values, ...(answer.secret ? { secret: answer.secret } : {}), }; } function providerNativeToolStatus(rawValue: unknown): ToolCallItem["status"] { const raw = asRecord(rawValue); if (raw.type !== "hosted_tool_call") { return "running"; } switch (raw.status) { case "completed": return "complete"; case "failed": case "incomplete": return "failed"; case "cancelled": return "cancelled"; default: return "running"; } } /** * Prefer newer status/fields, but keep earlier providerData.action when a * progress-only Responses event arrives without the search query payload. */ function mergeToolCallRaw(existingValue: unknown, nextValue: unknown): unknown { const existing = asRecord(existingValue); const next = asRecord(nextValue); if (Object.keys(next).length === 0) { return existingValue; } const existingProvider = asRecord(existing.providerData); const nextProvider = asRecord(next.providerData); const providerData = { ...existingProvider, ...nextProvider, }; if ( existingProvider.action != null && (nextProvider.action == null || (typeof nextProvider.action === "object" && nextProvider.action !== null && Object.keys(nextProvider.action as object).length === 0)) ) { providerData.action = existingProvider.action; } return { ...existing, ...next, ...(Object.keys(providerData).length > 0 ? { providerData } : {}), }; } const CONNECTED_MACHINE_CARD_ID = "api:connected-machine"; /** * An agent that is refused a machine often asks again in later turns. Only the * newest Connected Machine card for the same request stays a full card; earlier * ones become a short note, so a chat never piles up copies of the same card. * Cards that name different machines, or ask for Chrome rather than a machine, * are separate requests. */ function markRepeatedMachineRequests(items: TimelineItem[]): void { const seen = new Set(); for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]!; if (item.kind !== "auth-needed" || item.capability?.id !== CONNECTED_MACHINE_CARD_ID) continue; const key = `${item.purpose ?? ""}:${item.requestedSandboxId ?? ""}`; if (seen.has(key)) item.askedAgainLater = true; else seen.add(key); } } /** * Codex subscription web search can return private citation handles without * the URL annotation table that would make them resolvable. Keep the canonical * model-history item untouched, but never expose those unusable handles in the * human timeline. Ordinary markdown links and structured URL citations remain. */ export function stripOpaqueCitationTokens(text: string): string { return text.replace(/\s*cite(?:[^]+)+/gu, ""); } /** * The turn summary's short outcome for a usage ceiling. The adjacent notice row * carries who can raise it and when it resets (and is host-customizable), so * the summary line stays a neutral fact instead of repeating remedy prose. */ const ALLOWANCE_TURN_END_TEXT = (t: OpenGeniText) => t("timeline.projection.usageLimitReached"); /** The turn-end payload shape, as `isCreditExhaustion` wants it. */ function isCreditExhaustionPayload(payload: Record): boolean { if (payload.code === "allowance_exhausted") return false; return isCreditExhaustion({ error: typeof payload.error === "string" ? payload.error : null, detail: typeof payload.detail === "string" ? payload.detail : null, segmentLimit: typeof payload.segmentLimit === "string" ? payload.segmentLimit : null, }); } /** * Whether the session's most recent turn ended in credit exhaustion — the * terminal credit state apps key their "add credits" affordances on. Derived * from the LAST turn-end event (completed/failed/cancelled): a later turn that * settles any other way (someone topped up and kept working) clears it. */ export function creditExhaustedFromEvents(events: SessionEvent[]): boolean { const ordered = [...events].sort((a, b) => a.sequence - b.sequence); for (let index = ordered.length - 1; index >= 0; index -= 1) { const event = ordered[index]; if ( event?.type !== "turn.completed" && event?.type !== "turn.failed" && event?.type !== "turn.cancelled" ) { continue; } return isCreditExhaustionPayload(asRecord(event.payload)); } return false; } /** The latest session status carried in the event log, if any. */ export function sessionStatusFromEvents(events: SessionEvent[]): SessionStatus | null { for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index]; if (event?.type !== "session.status.changed") { continue; } const status = asRecord(event.payload).status; if (isSessionStatus(status)) { return status; } } return null; } /* ---------------------------------------------------------------------------- Visual grouping: consecutive activity items (reasoning / tools / workers / sandbox) cluster into collapsible blocks. Once a turn settles, the full non-user span folds behind a turn group, with activity blocks nested inside. -------------------------------------------------------------------------- */ /** Work items: every activity kind except assistant commentary. */ type WorkActivityItem = Exclude; /** * Whether an item clusters into an activity block. A `switch` (not a stringly- * typed set) so adding an {@link ActivityItem} kind is a compile-time prompt to * decide its grouping, and it narrows `item` to a work item with no cast. * Assistant prose stays outside activity clusters. */ /** Each marked turn's anchor: its `turn.started`, else its first event. */ function turnStepsAnchors( ordered: readonly SessionEvent[], turnSteps: BuildTimelineOptions["turnSteps"], ): Map { const anchors = new Map(); if (!turnSteps || turnSteps.size === 0) return anchors; const chosen = new Map(); for (const event of ordered) { const turnId = event.turnId; if (!turnId || !turnSteps.has(turnId)) continue; const current = chosen.get(turnId); if (!current || (event.type === "turn.started" && current.type !== "turn.started")) { chosen.set(turnId, event); } } for (const [turnId, anchor] of chosen) { const marker = turnSteps.get(turnId)!; anchors.set(anchor.id, { kind: "turn-steps", id: `turn-steps-${turnId}`, turnId, occurredAt: anchor.occurredAt, state: marker.state, load: marker.load, retain: marker.retain, }); } return anchors; } function isActivityItem(item: TimelineItem): item is WorkActivityItem { switch (item.kind) { case "reasoning": case "tool-call": case "worker": case "sandbox": case "startup-phase": case "memory": case "knowledge": case "fleet-decision": return true; default: return false; } } export type GroupTimelineOptions = { /** Readable assistant prose with an independent work summary for each turn. */ readableTurns?: boolean; /** @deprecated Use readableTurns. Cross-turn exchange folding has been removed. */ foldExchanges?: boolean; }; export function groupTimeline( t: OpenGeniText, items: TimelineItem[], options: GroupTimelineOptions = {}, ): TimelineGroup[] { if (options.readableTurns || options.foldExchanges) return groupReadableTurns(t, items); const groups: TimelineGroup[] = []; for (const item of items) { // A recovered capacity wait is over; it leaves no warning behind. if (item.kind === "notice" && item.capacityWait && item.resolvedAt) continue; if (isActivityItem(item)) { const open = groups[groups.length - 1]; if (open?.kind === "activity" && open.outcome === undefined) { open.items.push(item); } else { groups.push({ kind: "activity", id: `activity-${item.id}`, items: [item], }); } continue; } if (item.kind === "turn-end") { stampTurnOutcome(groups, item); foldSettledTurn(groups, item); continue; } groups.push({ kind: "item", item }); } return groups; } /** Live prose stays primary; settled progress joins its own turn's work history. */ function groupReadableTurns(t: OpenGeniText, items: TimelineItem[]): TimelineGroup[] { type Work = Extract; let groups: TimelineGroup[] = []; const turns = new Map(); // Turn identity and settlement exist even when a final-only turn has no // disclosure. A late receipt must not become a new turn or revive its clock. const seenTurns = new Set(); const settlements = new Map< string, { endedAt: string; outcome?: Work["outcome"]; failureText?: string } >(); const compactionTurns = new Map(); // Turns that handed over to a Steer instead of finishing. const handedOverTurns = new Set(); const messages = new Map(); // Capacity waits per turn: their spans are not work, and a live one is // carried by the turn's work row instead of a separate warning. const capacityPauses = new Map>(); const liveCapacityWaits = new Map(); const inputs = new Map>(); const itemOrder = new Map(items.map((item, index) => [item.id, index])); // Turns that answer a person's message: the first turn after it, and a live // turn it steered. Their replies are never folded as mere progress. const answersPerson = new Set(); let personAwaitingTurn = false; // A live turn a person's message may have steered. If it keeps working after // the message, the steer was absorbed and no later turn inherits it; if it is // superseded instead, the next turn answers the person. let steeredTurn: string | undefined; let legacyTurn = "start"; let currentTurn = legacyTurn; for (const item of items) { if (item.kind === "user-message") { legacyTurn = item.id; personAwaitingTurn = true; steeredTurn = undefined; if (seenTurns.has(currentTurn) && !settlements.has(currentTurn)) { answersPerson.add(currentTurn); steeredTurn = currentTurn; } } // A capacity wait belongs to the turn it blocks, even before that turn's // first step. const itemTurnId = item.kind === "notice" && item.capacityWait ? item.capacityWait.turnId : "turnId" in item ? item.turnId : null; const key = itemTurnId || legacyTurn; if ( steeredTurn !== undefined && key === steeredTurn && (isActivityItem(item) || (item.kind === "agent-message" && item.text.trim())) ) { personAwaitingTurn = false; steeredTurn = undefined; } // Legacy work still establishes a boundary using its prompt key. // A human message alone does not: it may be steering the existing turn. if (itemTurnId || isActivityItem(item) || (item.kind === "agent-message" && item.text.trim())) { if (key !== currentTurn && !seenTurns.has(key)) { const previous = turns.get(currentTurn); if (!settlements.has(currentTurn)) { settlements.set(currentTurn, { endedAt: item.occurredAt }); } if (previous?.work && !previous.work.endedAt) previous.work.endedAt = item.occurredAt; currentTurn = key; if (personAwaitingTurn) answersPerson.add(key); personAwaitingTurn = false; steeredTurn = undefined; } seenTurns.add(key); } const workForTurn = () => { let group = turns.get(key); const firstMessage = messages.get(key)?.[0]; const messageStart = firstMessage?.startedAt ?? firstMessage?.occurredAt; const activityStart = item.kind === "startup-phase" ? (item.loadingStartedAt ?? item.occurredAt) : item.occurredAt; const startedAt = messageStart && Date.parse(messageStart) < Date.parse(activityStart) ? messageStart : activityStart; if (!group) { const settlement = settlements.get(key); group = { kind: "activity", id: `work-${key}`, items: [], work: { startedAt, details: [] }, }; if (settlement) { group.work!.endedAt = settlement.endedAt; if (settlement.outcome) group.outcome = settlement.outcome; if (settlement.failureText) group.failureText = settlement.failureText; } turns.set(key, group); const firstMessageIndex = settlement && firstMessage ? groups.findIndex((entry) => entry.kind === "item" && entry.item === firstMessage) : -1; if (firstMessageIndex >= 0) groups.splice(firstMessageIndex, 0, group); else groups.push(group); } else if (Date.parse(startedAt) < Date.parse(group.work!.startedAt)) { // Completion-only startup receipts can reveal an earlier phase start // after another activity has already established the work row. group.work!.startedAt = startedAt; } return group; }; if (item.kind === "turn-steps") { // Not a step: it gives the turn its work row (a turn whose steps are all // unloaded has nothing else) and is rendered by that row. workForTurn().work!.steps = item; } else if (isActivityItem(item)) { const group = workForTurn(); group.items.push(item); const previous = group.work!.details.at(-1); const preparing = (entry: ActivityItem) => entry.kind === "startup-phase" || (entry.kind === "reasoning" && !entry.text.trim()); if ( preparing(item) && previous?.kind === "activity" && previous.items.every(preparing) && previous.items.at(-1) === items[(itemOrder.get(item.id) ?? 0) - 1] ) previous.items.push(item); else group.work!.details.push({ kind: "activity", id: `work-item-${item.id}`, items: [item] }); delete group.work!.waiting; } else if (item.kind === "notice" && item.capacityWait) { const pauses = capacityPauses.get(key) ?? []; pauses.push({ startedAt: item.occurredAt, ...(item.resolvedAt ? { endedAt: item.resolvedAt } : {}), }); capacityPauses.set(key, pauses); if (!item.resolvedAt) { if (settlements.has(key)) groups.push({ kind: "item", item }); else { workForTurn(); liveCapacityWaits.set(key, item); } } } else if (item.kind === "turn-end") { if (item.handedOver) handedOverTurns.add(key); if (!item.resumedAt) { settlements.set(key, { endedAt: item.occurredAt, outcome: item.outcome, ...(item.failureText ? { failureText: item.failureText } : {}), }); } const group = turns.get(key); if (group?.work) { if (item.resumedAt) { group.work.details.push({ kind: "item", item: { kind: "notice", id: item.id, tone: "failed", text: item.failureText ?? t("timeline.projection.theTurnFailedBeforeRetrying"), occurredAt: item.occurredAt, }, }); continue; } group.outcome = item.outcome; if (item.failureText) group.failureText = item.failureText; group.work.endedAt = item.occurredAt; } } else if (item.kind === "context-compaction" && item.phase === "compacted") { // A maintenance checkpoint alone is not conversational work. Resolve its // attachment after real activity has established this turn's work row. compactionTurns.set(item, key); groups.push({ kind: "item", item }); } else if ( item.kind === "machine-input-batch" && item.members.every((member) => ROUTINE_MACHINE_INPUT_KINDS.has(member.kind)) ) { const previous = inputs.get(key); if (previous) { previous.members.push( ...item.members.filter((member) => !previous.members.some((old) => old.id === member.id)), ); previous.sourceEvents = [...(previous.sourceEvents ?? []), ...(item.sourceEvents ?? [])]; } else { const batch = { ...item, compact: true, members: [...item.members] }; inputs.set(key, batch); groups.push({ kind: "item", item: batch }); } } else if ( item.kind === "worker-completion" || (item.kind === "goal" && item.action === "continuation") ) { const group = turns.get(currentTurn); if (group?.work) group.work.details.push({ kind: "item", item }); else groups.push({ kind: "item", item }); } else { const current = turns.get(currentTurn)?.work; // A live approval wait is carried by the turn header ("Waiting for you") // and decided in the host's approval surface; a second in-timeline banner // only repeats it. Once resolved it returns as the quiet recorded marker. const liveApprovalWait = item.kind === "notice" && item.tone === "waiting" && !item.recordedOutcome && !item.resolvedAt && item.approval === true && current !== undefined && !current.endedAt; // Likewise the live "waiting on you" divider: the open turn's header // already reads "Waiting for you · ". It returns, resolved, as // the "work resumed" divider once the session runs again. const liveStatusWait = item.kind === "session-status" && item.status === "requires_action" && !item.blocked && !item.resolvedAt && current !== undefined && !current.endedAt; if (!liveApprovalWait && !liveStatusWait) groups.push({ kind: "item", item }); if (current && !current.endedAt) { if ( item.kind === "notice" && item.tone === "waiting" && !item.recordedOutcome && !item.resolvedAt ) { current.waiting = { label: item.approval === true ? t("timeline.projection.waitingForYou") : t("timeline.projection.waiting"), since: item.occurredAt, }; } else if ( item.kind === "session-status" && item.status === "requires_action" && !item.blocked && !item.resolvedAt ) { current.waiting = { label: t("timeline.projection.waitingForYou"), since: item.occurredAt, }; } else if ( item.kind === "session-status" && ["paused", "cancelled", "failed"].includes(item.status) && !item.resolvedAt ) { current.endedAt = item.occurredAt; delete current.waiting; } } if (item.kind === "agent-message" && item.text.trim()) { const prose = messages.get(key) ?? []; prose.push(item); messages.set(key, prose); // A text-only turn still has one overall activity surface. An explicit // final-only response does not manufacture an empty work disclosure. if ( (item.phase !== "final_answer" || prose.length > 1) && (turns.has(key) || !settlements.has(key)) ) workForTurn(); } } } for (const [key, group] of turns) { const work = group.work!; const live = liveCapacityWaits.get(key); // Re-assert after the loop: a late receipt for the blocked turn must not // turn "Limit reached" back into "Working" while the wait is still open. if (live?.capacityWait && !work.endedAt) { work.waiting = { label: live.capacityWait.label, since: live.occurredAt, detail: live.capacityWait.detail, // Its span is not work and may last until a quota resets: no clock. untimed: true, }; } const pausedMs = (capacityPauses.get(key) ?? []).reduce((total, pause) => { if (!pause.endedAt) return total; const start = Math.max(Date.parse(work.startedAt), Date.parse(pause.startedAt)); const end = Math.min( work.endedAt ? Date.parse(work.endedAt) : Number.POSITIVE_INFINITY, Date.parse(pause.endedAt), ); return Number.isFinite(start) && Number.isFinite(end) && end > start ? total + end - start : total; }, 0); if (pausedMs > 0) work.pausedMs = pausedMs; } groups = groups.filter((entry) => { if (entry.kind !== "item" || entry.item.kind !== "context-compaction") return true; const key = compactionTurns.get(entry.item); const work = key === undefined ? undefined : turns.get(key)?.work; if (!work) return true; work.details.push(entry); return false; }); // A turn that handed over to the next Steer before doing anything but start // up did no work of its own: the steered message carries on in the next // turn. Quickly steering several queued messages in a row would otherwise // leave a stack of empty "Worked for" rows between the messages. for (const key of handedOverTurns) { const group = turns.get(key); if ( !group || (messages.get(key)?.length ?? 0) > 0 || !group.items.every((item) => item.kind === "startup-phase") || !group.work!.details.every( (detail) => detail.kind === "activity" && detail.items.every((item) => item.kind === "startup-phase"), ) ) continue; turns.delete(key); groups = groups.filter((entry) => entry !== group); } // A declared answer starts the response clock immediately. Without a phase, // only settlement identifies the last response; text length never does. const beforeRows = new Map(); const afterRows = new Map(); const movedRows = new Set(); const foldedProse = new Set(); const positions = new Map(); const nextBoundary = new Map(); let boundary = groups.length; for (let index = groups.length - 1; index >= 0; index -= 1) { const entry = groups[index]!; positions.set(entry.kind === "item" ? entry.item : entry, index); if (entry.kind === "activity" && entry.work) { nextBoundary.set(entry, boundary); boundary = index; } else if ( entry.kind === "item" && (entry.item.kind === "user-message" || entry.item.kind === "machine-input-batch") ) { boundary = index; } } for (const [key, group] of turns) { const prose = messages.get(key) ?? []; const settledAt = group.work!.endedAt; const responseCandidates = settledAt ? prose.filter( (message) => Date.parse(message.startedAt ?? message.occurredAt) <= Date.parse(settledAt), ) : prose; const response = responseCandidates.filter((message) => message.phase === "final_answer").at(-1) ?? (settledAt ? (responseCandidates.at(-1) ?? prose.filter((message) => message.phase === "final_answer").at(-1)) : undefined); const responses = new Set(response ? [response] : []); // A turn that answers a person but ends with only commentary (typically by // yielding to a wait) has no single authoritative reply. Its first message // is the immediate answer and its last is the latest word; only the // progress narration between them folds. const firstCandidate = responseCandidates[0]; if ( settledAt && firstCandidate && answersPerson.has(key) && responseCandidates.every((message) => message.phase === "commentary") ) { responses.add(firstCandidate); } if (settledAt) { for (const message of responseCandidates) { // Markdown image syntax also carries retained video/audio previews. // Keep potential primary media visible rather than guessing whether a // partial/reference-style embed can safely disappear into history. if (responses.has(message) || message.text.includes("![")) continue; foldedProse.add(message); group.work!.details.push({ kind: "item", item: message }); } } else { // Live progress stays primary above the work row, and is also listed in // the work history so an expanded disclosure reads like the settled one. // The timeline folds the outside copies only while that disclosure is open. for (const message of prose) { if (message.phase === "final_answer" || message.text.includes("![")) continue; group.work!.details.push({ kind: "item", item: message }); (group.work!.liveNoteIds ??= []).push(message.id); } } // Preserve event chronology, not completion timestamps or activity kinds. group.work!.details.sort((a, b) => { const first = (entry: TimelineGroup) => entry.kind === "item" ? entry.item.id : entry.kind === "activity" ? entry.items[0]?.id : undefined; return (itemOrder.get(first(a) ?? "") ?? 0) - (itemOrder.get(first(b) ?? "") ?? 0); }); if (response) { const responseAt = response.startedAt ?? response.occurredAt; // A delayed completion-only receipt cannot extend a settled work clock. group.work!.responseStartedAt = settledAt && Date.parse(responseAt) > Date.parse(settledAt) ? settledAt : responseAt; // A late completion may belong to an older turn. Keep its work at its // original boundary rather than moving it across a newer turn's input. // The work row leads the first visible reply of the turn. A live turn // whose answer is already declared puts the row there too, so the answer // being read stays put when the turn settles instead of hopping past it. const anchor = Math.min( ...[...responses].map((message) => positions.get(message) ?? groups.length), ); if (anchor < (nextBoundary.get(group) ?? groups.length)) { beforeRows.set(groups[anchor]!, group); movedRows.add(group); continue; } } if (!settledAt) { // Keep the same row at the tail of this turn, after every live progress // message and attention surface, without crossing a newer input/turn. // A visible steer is not a turn boundary when later prose still belongs // to this turn. New input without that evidence remains a hard boundary. const lastProseIndex = Math.max(...prose.map((message) => positions.get(message) ?? -1)); let tailIndex = groups.length - 1; for (let index = (positions.get(group) ?? 0) + 1; index <= tailIndex; index += 1) { const entry = groups[index]!; if ( (entry.kind === "activity" && entry.work) || (entry.kind === "item" && (("turnId" in entry.item && entry.item.turnId && entry.item.turnId !== key) || (index > lastProseIndex && (entry.item.kind === "user-message" || entry.item.kind === "machine-input-batch")))) ) { tailIndex = index - 1; break; } } const tail = groups[tailIndex]; if (tail && tail !== group) { afterRows.set(tail, group); movedRows.add(group); } } } // The work of every turn that answers a person stays visible even when it // said nothing; quiet-cycle folding must never take it. const personWork = new Set( [...answersPerson].flatMap((key) => { const work = turns.get(key); return work ? [work] : []; }), ); return compactQuietCycles( groups.flatMap((group) => { if (group.kind === "activity" && movedRows.has(group)) return []; const hidden = group.kind === "item" && group.item.kind === "agent-message" && foldedProse.has(group.item); const before = beforeRows.get(group); const after = afterRows.get(group); return [...(before ? [before] : []), ...(hidden ? [] : [group]), ...(after ? [after] : [])]; }), personWork, ); } type WorkGroup = Extract & { work: NonNullable["work"]>; }; /** * Settled, successful work whose turn left no visible reply, image or blocker. * A failure it recovered from, a failed startup phase or a context compaction * is something to see, so that work is not quiet either. */ function isQuietWork(group: TimelineGroup): group is WorkGroup { if (group.kind !== "activity" || !group.work?.endedAt || group.work.waiting) return false; if (group.work.cycles || (group.outcome && group.outcome !== "complete")) return false; if ( group.items.some( (item) => item.kind === "startup-phase" && (item.status === "failed" || item.status === "cancelled"), ) ) { return false; } const quiet = (entry: TimelineGroup): boolean => entry.kind === "item" ? entry.item.kind !== "auth-needed" && entry.item.kind !== "context-compaction" && entry.item.kind !== "human-input" && !(entry.item.kind === "notice" && entry.item.tone !== "waiting") && !(entry.item.kind === "agent-message" && entry.item.text.includes("![")) : entry.kind === "activity" ? isQuietDetail(entry, quiet) : false; return !publishesOutput(group) && group.work.details.every(quiet); } function isQuietDetail( group: Extract, quiet: (entry: TimelineGroup) => boolean, ): boolean { return ( (!group.outcome || group.outcome === "complete") && (group.work?.details ?? []).every(quiet) ); } /** * Work that generated an image or published a file produced output worth * seeing. Checked by tool name only, so the projection stays free of the * renderer's image presentation rules. */ function publishesOutput(group: TimelineGroup): boolean { if (group.kind !== "activity") return false; return ( group.items.some((item) => { if (item.kind !== "tool-call") return false; const name = mcpToolLeaf(item.name); return ( name === "generate_image" || name === "image_generation_call" || name === "sandbox_file_publish" ); }) || (group.work?.details ?? []).some(publishesOutput) ); } /** * Whether the work at `index` follows a person's message or answer with only * routine input in between. Complements the turn bookkeeping for legacy rows. */ function answersPersonAt(groups: TimelineGroup[], index: number): boolean { for (let cursor = index - 1; cursor >= 0; cursor -= 1) { const group = groups[cursor]!; if ( group.kind === "item" && (group.item.kind === "user-message" || group.item.kind === "human-input") ) { return true; } if (!isRoutineInput(group)) return false; } return false; } /** * A routine machine input (agent update, child result, wait timeout, ...). An * agent that failed, paused or needs action is news, not routine. */ function isRoutineInput(group: TimelineGroup): boolean { return ( group.kind === "item" && group.item.kind === "machine-input-batch" && !!group.item.compact && group.item.members.every( (member) => member.kind !== "child_paused" && member.classification !== "failure" && // A parent's direction is always recorded as action_required for the // agent; it is routine orchestration, not something a person must do. (member.classification !== "action_required" || member.kind === "agent_steer_instruction"), ) ); } /** A recorded wait that later input already ended. */ function isFinishedWait(group: TimelineGroup): group is Extract & { item: NoticeItem; } { return ( group.kind === "item" && group.item.kind === "notice" && group.item.tone === "waiting" && !!group.item.recordedOutcome && !!group.item.waitEndedAt ); } /** * Long orchestration runs repeat "agent update / worked / waited" many times * without saying anything. Fold two or more such consecutive quiet cycles into * one work row: "7 updates over 6h 12m", with the latest wait reason under it. * Expanding it shows the original rows unchanged. Visible replies, people's * messages, failures, approvals, live work and the current wait are never folded * and end a run. */ function compactQuietCycles( groups: TimelineGroup[], personWork: ReadonlySet, ): TimelineGroup[] { const result: TimelineGroup[] = []; let index = 0; while (index < groups.length) { // A run starts at routine input, never at a leading wait (it belongs to // the reply before it) or at bare work (it belongs to the visible row // before it, such as an agent's failure). Later cycles may lack input, // for example after a wait timeout. let end = index; let lastCycleEnd = index; let cycles = 0; while (end < groups.length) { const input = isRoutineInput(groups[end]!) ? end : -1; if (input < 0 && cycles === 0) break; const workIndex = input >= 0 ? end + 1 : end; const work = groups[workIndex]; if (!work || !isQuietWork(work)) break; // Work that follows a person's message is that message's turn, even // when it said nothing; it stays visible. if (personWork.has(work) || answersPersonAt(groups, workIndex)) break; const next = groups[workIndex + 1]; // Work directly followed by its reply is not quiet. if (next?.kind === "item" && next.item.kind === "agent-message") break; if (!next || !isFinishedWait(next)) break; cycles += 1; end = workIndex + 2; lastCycleEnd = end; } if (cycles < 2) { result.push(groups[index]!); index += 1; continue; } const run = groups.slice(index, lastCycleEnd); const works = run.filter((group): group is WorkGroup => isQuietWork(group)); const waits = run.filter(isFinishedWait); const first = works[0]!; const lastWait = waits.at(-1)!; result.push({ kind: "activity", id: `cycles-${first.id}`, items: works.flatMap((group) => group.items), outcome: "complete", work: { startedAt: first.work.startedAt, endedAt: lastWait.item.waitEndedAt!, details: run, cycles: { count: cycles, ...(lastWait.item.text.trim() ? { summary: lastWait.item.text.trim() } : {}), }, }, }); index = lastCycleEnd; } return result; } /** Machine inputs that continue the current exchange rather than start one. */ const ROUTINE_MACHINE_INPUT_KINDS: ReadonlySet = new Set([ "goal_continuation", "background_command_result", "session_wait_timeout", "agent_message", "agent_steer_instruction", "child_terminal_result", "child_requires_action_resolved", "child_paused", "child_waiting_capacity", "child_progress", ]); /* --- helpers ---------------------------------------------------------------- */ type TurnAnchorPrescan = { queuedTurnByTrigger: Map; startSeqByTrigger: Map; cancelledBeforeStartTriggers: Set; directChatSequenceByTrigger: Map; explicitQueuedTriggers: Set; startedTurnIds: Set; }; /** Navigation uses the same queue/start/withdrawal rules as transcript ordering. */ export function timelineQuestionPlacement(question: SessionEvent, evidence: SessionEvent[]) { const anchors = prescanTurnAnchors([question, ...evidence]); const direct = anchors.directChatSequenceByTrigger.get(question.id); if (direct !== undefined) return { kind: "visible" as const, sequence: direct }; if (anchors.cancelledBeforeStartTriggers.has(question.id)) return { kind: "withdrawn" as const }; const start = anchors.startSeqByTrigger.get(question.id); if (start !== undefined) return { kind: "visible" as const, sequence: start }; if ( anchors.queuedTurnByTrigger.has(question.id) || anchors.explicitQueuedTriggers.has(question.id) ) return { kind: "pending" as const }; return { kind: "visible" as const, sequence: question.sequence }; } function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan { const ordered = [...events].sort((a, b) => a.sequence - b.sequence); const queuedTurnByTrigger = new Map(); const startSeqByTrigger = new Map(); const cancelledTurnIds = new Set(); const withdrawnTurnIds = new Set(); const fallbackSeqByTurn = new Map(); const directChatSequenceByTrigger = new Map(); const explicitQueuedTriggers = new Set(); const queueSteerSequenceByTurn = new Map(); const startedTurnIds = new Set(); const userMessageSequenceById = new Map(); for (const event of ordered) { const payload = asRecord(event.payload); const turnId = event.turnId ?? null; if (event.type === "user.message") { userMessageSequenceById.set(event.id, event.sequence); if (payload.routing === "queued_for_execution") { explicitQueuedTriggers.add(event.id); } else if ( payload.routing === "accepted_for_execution" || payload.routing === "accepted_for_steering" ) { directChatSequenceByTrigger.set(event.id, event.sequence); } } if (event.type === "user.message" && payload.delivery === "steer") { directChatSequenceByTrigger.set(event.id, event.sequence); } if (event.type === "session.control.steer_requested") { const targetTurnId = typeof payload.targetTurnId === "string" ? payload.targetTurnId : null; if (targetTurnId && !queueSteerSequenceByTurn.has(targetTurnId)) { queueSteerSequenceByTurn.set(targetTurnId, event.sequence); } } if ( event.type === "session.queue.changed" && (payload.operation === "edit" || payload.operation === "delete") ) { const withdrawnTurnId = typeof payload.turnId === "string" ? payload.turnId : (turnId ?? null); if (withdrawnTurnId) withdrawnTurnIds.add(withdrawnTurnId); } if (event.type === "turn.queued") { const triggerEventId = typeof payload.triggerEventId === "string" ? payload.triggerEventId : null; const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId; if (triggerEventId && queuedTurnId) { queuedTurnByTrigger.set(triggerEventId, queuedTurnId); if ( (payload.routing === "accepted_for_execution" || payload.routing === "accepted_for_steering") && !directChatSequenceByTrigger.has(triggerEventId) ) { directChatSequenceByTrigger.set( triggerEventId, userMessageSequenceById.get(triggerEventId) ?? event.sequence, ); } } continue; } if (event.type === "turn.started") { const triggerEventId = typeof payload.triggerEventId === "string" ? payload.triggerEventId : null; if (triggerEventId && !startSeqByTrigger.has(triggerEventId)) { startSeqByTrigger.set(triggerEventId, event.sequence); } if (turnId) { startedTurnIds.add(turnId); } } else if (event.type === "turn.cancelled" && turnId) { cancelledTurnIds.add(turnId); } if (turnId && isTurnExecutionEvidence(event.type)) { const previous = fallbackSeqByTurn.get(turnId); if (previous === undefined || event.sequence < previous) { fallbackSeqByTurn.set(turnId, event.sequence); } } if ( turnId && isTurnExecutionEvidence(event.type) && !(event.type === "turn.superseded" && payload.beforeAttempt === true) ) { startedTurnIds.add(turnId); } } for (const [triggerEventId, turnId] of queuedTurnByTrigger) { const queueSteerSequence = queueSteerSequenceByTurn.get(turnId); if (queueSteerSequence !== undefined) { directChatSequenceByTrigger.set(triggerEventId, queueSteerSequence); } if (!startSeqByTrigger.has(triggerEventId)) { const fallbackSeq = fallbackSeqByTurn.get(turnId); if (fallbackSeq !== undefined) { startSeqByTrigger.set(triggerEventId, fallbackSeq); } } } const cancelledBeforeStartTriggers = new Set(); for (const [triggerEventId, turnId] of queuedTurnByTrigger) { if ( (cancelledTurnIds.has(turnId) || withdrawnTurnIds.has(turnId)) && !startSeqByTrigger.has(triggerEventId) && !fallbackSeqByTurn.has(turnId) ) { cancelledBeforeStartTriggers.add(triggerEventId); } } return { queuedTurnByTrigger, startSeqByTrigger, cancelledBeforeStartTriggers, directChatSequenceByTrigger, explicitQueuedTriggers, startedTurnIds, }; } function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan): SessionEvent[] { const ordered = [...events].sort((a, b) => a.sequence - b.sequence); const insertions = new Map(); for (const event of ordered) { if (event.type !== "user.message") { continue; } // Steer is an accepted direction change, not waiting queue copy. Keep it // directly in chat across refresh/reconnect even before its turn starts. const directChatSequence = prescan.directChatSequenceByTrigger.get(event.id); if (directChatSequence !== undefined) { pushInsertion(insertions, directChatSequence, event); continue; } const queuedTurnId = prescan.queuedTurnByTrigger.get(event.id); if (!queuedTurnId) { if (prescan.explicitQueuedTriggers.has(event.id)) { const startSeq = prescan.startSeqByTrigger.get(event.id); if (startSeq !== undefined) { pushInsertion(insertions, startSeq, event); } continue; } pushInsertion(insertions, event.sequence, event); continue; } if (prescan.cancelledBeforeStartTriggers.has(event.id)) { continue; } const startSeq = prescan.startSeqByTrigger.get(event.id); if (startSeq !== undefined) { pushInsertion(insertions, startSeq, event); continue; } // A waiting prompt belongs only in the prompt queue. It enters the // timeline at turn.started (or the first same-turn activity fallback), // never as a second queued representation. } const projected: SessionEvent[] = []; for (const event of ordered) { const before = insertions.get(event.sequence); if (before) { projected.push(...before); } if (event.type !== "user.message") { projected.push(event); } } return projected; } function pushInsertion( insertions: Map, sequence: number, event: SessionEvent, ): void { const bucket = insertions.get(sequence); if (bucket) { bucket.push(event); } else { insertions.set(sequence, [event]); } } function isAgentActivityEvent(type: string): boolean { return ( type.startsWith("agent.") || type.startsWith("sandbox.") || type === "subscription.account.selected" || type === "subscription.account.switched" ); } /** * Evidence that a queued prompt crossed the execution boundary when an older * or partially recovered ledger is missing its canonical `turn.started`. * Queue/control bookkeeping intentionally does not qualify: moving, editing, * steering, or deleting a waiting row must never make its prompt appear in the * transcript as though inference had begun. */ export function isTurnExecutionEvidence(type: string): boolean { return ( isAgentActivityEvent(type) || type === "turn.completed" || type === "turn.failed" || // An admission refusal can settle a queued turn before it ever starts: // its prompt still belongs above the "usage limit reached" row. type === "usage.exhausted" || type === "turn.superseded" || type === "turn.recovery.requested" || type === "turn.capacity_waiting" || type === "session.requiresAction" || type === "tool.auth_needed" || type === "credential.auth_needed" || type.startsWith("turn.startup.phase.") || type.startsWith("rig.setup.") || type === "codex.capacity.waiting" || type === "codex.capacity.resumed" || type === "codex.fleet.decision" ); } function turnEndItem( event: SessionEvent, outcome: TurnEndItem["outcome"], failureText: string | null, ): TurnEndItem { return { kind: "turn-end", id: `${event.id}-turn-end`, turnId: event.turnId ?? null, outcome, failureText, occurredAt: event.occurredAt, }; } /** Plain text for a usage-limit stop whose marker carried no readable amounts. */ const SESSION_USAGE_LIMIT_STOP_TEXT = (t: OpenGeniText) => t("timeline.projection.thisTaskSUsageLimit"); /** * The stop row for a `segmentLimit: "session_usage_limit"` completion. Only the * typed amounts are kept (never upstream prose), and a malformed marker still * reads as the same neutral stop instead of a credit or failure notice. */ function sessionUsageLimitNotice( t: OpenGeniText, event: SessionEvent, payload: Record, ): NoticeItem { const refusal = parseSessionUsageLimitReachedRefusal(payload); return { kind: "notice", id: `${event.id}-session-usage-limit`, // Neutral: not a failure, and not an open wait the turn header should claim. tone: "cancelled", text: refusal ? sessionUsageLimitReachedMessage(refusal) : SESSION_USAGE_LIMIT_STOP_TEXT(t), sessionUsageLimit: { rootSessionId: refusal?.rootSessionId ?? null, limitMicros: refusal?.limitMicros ?? null, usedMicros: refusal?.usedMicros ?? null, }, occurredAt: event.occurredAt, }; } function hasTurnActivity(items: TimelineItem[], turnId: string | null): boolean { if (turnId) { return items.some((item) => isActivityItem(item) && item.turnId === turnId); } for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if (!item || item.kind === "turn-end" || item.kind === "user-message") { return false; } if (isActivityItem(item)) { return true; } } return false; } function stampTurnOutcome(groups: TimelineGroup[], turnEnd: TurnEndItem): void { if (turnEnd.turnId === null) { const trailing = groups[groups.length - 1]; if (trailing?.kind === "activity" && trailing.outcome === undefined) { applyTurnOutcome(trailing, turnEnd); } return; } for (const group of groups) { if (group.kind !== "activity" || group.outcome !== undefined) { continue; } if (group.items.some((activity) => activity.turnId === turnEnd.turnId)) { applyTurnOutcome(group, turnEnd); } } } function applyTurnOutcome( group: Extract, turnEnd: TurnEndItem, ): void { // A sub-cluster reports ITS OWN outcome, not the turn's. When a turn fails // at step 7, clusters 1–6 completed — painting them all red says "everything // broke" when one thing did. The turn-level fold carries the turn outcome; // a cluster goes red only if an item inside it actually failed, and reads // "cancelled" (interrupted) only when it holds the items cut off mid-flight. if (turnEnd.outcome === "complete") { group.outcome = "complete"; return; } const hasFailed = group.items.some((item) => "status" in item && item.status === "failed"); const hasInterrupted = group.items.some( (item) => "status" in item && item.status === "cancelled", ); group.outcome = hasFailed ? "failed" : hasInterrupted ? "cancelled" : "complete"; if (turnEnd.failureText && hasFailed) { group.failureText = turnEnd.failureText; } } function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void { let startIndex = groups.length; let stoppedAtForeignTurn = false; while (startIndex > 0) { const previous = groups[startIndex - 1]; if (isTurnBoundary(previous)) { break; } if (belongsToDifferentTurn(previous, turnEnd.turnId)) { stoppedAtForeignTurn = true; break; } startIndex -= 1; } if (stoppedAtForeignTurn) { while (startIndex < groups.length && isBetweenTurnDivider(groups[startIndex])) { startIndex += 1; } } const collected = groups.slice(startIndex); if (collected.length === 0) { return; } const collectedLength = collected.length; // Resolve only inside this turn's collected suffix. A user/machine-input // boundary or an interleaved foreign turn must never donate its answer. const waitMessages = turnEnd.preserveWaitResponse ? collected.filter( (group): group is Extract => group.kind === "item" && group.item.kind === "agent-message" && !group.item.streaming && group.item.text.trim().length > 0 && belongsToTurn(group.item, turnEnd.turnId), ) : []; // A completed answer outranks later commentary. Otherwise use the latest // visible existing prose, ignoring whitespace and opaque-only stream tails. const waitResponse = waitMessages .slice() .reverse() .find( (group) => group.item.kind === "agent-message" && group.item.phase !== "commentary" && group.item.annotationSource?.eventType === "agent.message.completed", ) ?? waitMessages.at(-1); const finalMessage = waitResponse ?? extractFinalAgentMessage(collected, turnEnd); const fallbackMessage = finalMessage || hasOrdinaryFinalAgentMessage(collected, turnEnd) ? null : extractLatestCompletedCommentary(collected, turnEnd); const visibleMessage = finalMessage ?? fallbackMessage; const visibleMessages = visibleMessage ? [visibleMessage] : []; // A turn that answers a person but declares no final answer (typically one // that yields to a wait) keeps its first message visible beside the latest: // that is the immediate answer, not progress narration. if ( visibleMessage && !hasOrdinaryFinalAgentMessage(collected, turnEnd) && turnAnswersPerson(groups, startIndex) ) { const first = collected.find( (group): group is Extract => group.kind === "item" && group.item.kind === "agent-message" && !group.item.streaming && group.item.text.trim().length > 0 && belongsToTurn(group.item, turnEnd.turnId), ); if (first && first !== visibleMessage) visibleMessages.unshift(first); } // A change the person asked to keep that was not saved stays in view beside // the answer, whatever the agent said about it. const keptNotices = collected.filter( (group) => group.kind === "item" && group.item.kind === "notice" && !!group.item.learningRefusal, ); const body = collected.filter( (group) => !visibleMessages.includes(group as never) && !keptNotices.includes(group), ); if (body.length === 0) { return; } const firstOccurredAt = groupStartedAt(body[0]) ?? turnEnd.occurredAt; const prior = startIndex > 0 ? groups[startIndex - 1] : undefined; const contextCompactionCount = prior?.kind === "item" && prior.item.kind === "context-compaction" && prior.item.phase === "compacted" ? 1 : 0; const turnGroup: TimelineGroup = { kind: "turn", id: `turn-${turnEnd.turnId ?? turnEnd.id}`, outcome: turnEnd.outcome, startedAt: firstOccurredAt, endedAt: turnEnd.occurredAt, groups: body, ...(contextCompactionCount > 0 ? { contextCompactionCount } : {}), }; if (turnEnd.failureText) { turnGroup.failureText = turnEnd.failureText; } groups.splice(startIndex, collectedLength, turnGroup, ...keptNotices, ...visibleMessages); } /** Whether the input boundary before a turn includes a person's message. */ function turnAnswersPerson(groups: TimelineGroup[], startIndex: number): boolean { for (let index = startIndex - 1; index >= 0; index -= 1) { const group = groups[index]; if (group?.kind !== "item") return false; if (group.item.kind === "user-message") return true; if ( group.item.kind !== "machine-input-batch" && group.item.kind !== "session-status" && group.item.kind !== "context-compaction" && !(group.item.kind === "notice" && group.item.tone === "input") ) { return false; } } return false; } function isTurnBoundary(group: TimelineGroup | undefined): boolean { return ( group?.kind === "turn" || (group?.kind === "item" && (group.item.kind === "user-message" || group.item.kind === "human-input" || group.item.kind === "context-compaction" || group.item.kind === "machine-input-batch" || (group.item.kind === "notice" && group.item.tone === "input"))) ); } function belongsToDifferentTurn(group: TimelineGroup | undefined, turnId: string | null): boolean { if (!group || !turnId) { return false; } if (group.kind === "activity") { return ( group.items.length > 0 && group.items.every((item) => item.turnId !== null && item.turnId !== turnId) ); } return ( group.kind === "item" && group.item.kind === "agent-message" && group.item.turnId !== null && group.item.turnId !== turnId ); } function isBetweenTurnDivider(group: TimelineGroup | undefined): boolean { return ( group?.kind === "item" && group.item.kind === "session-status" && group.item.status !== "running" ); } function extractFinalAgentMessage( groups: TimelineGroup[], turnEnd: TurnEndItem, ): Extract | null { const tail = groups[groups.length - 1]; if (tail?.kind !== "item" || tail.item.kind !== "agent-message" || tail.item.streaming) { return null; } if (tail.item.turnId && turnEnd.turnId && tail.item.turnId !== turnEnd.turnId) { return null; } return tail; } function hasOrdinaryFinalAgentMessage(groups: TimelineGroup[], turnEnd: TurnEndItem): boolean { return groups.some((group) => { if (group.kind !== "item" || group.item.kind !== "agent-message") return false; const message = group.item; return ( !message.streaming && message.phase !== "commentary" && message.text.trim().length > 0 && belongsToTurn(message, turnEnd.turnId) ); }); } function extractLatestCompletedCommentary( groups: TimelineGroup[], turnEnd: TurnEndItem, ): Extract | null { for (let index = groups.length - 1; index >= 0; index -= 1) { const group = groups[index]; if (group?.kind !== "item" || group.item.kind !== "agent-message") continue; const message = group.item; if ( !message.streaming && message.phase === "commentary" && message.text.trim().length > 0 && belongsToTurn(message, turnEnd.turnId) ) { return group; } } return null; } function belongsToTurn(message: AgentMessageItem, turnId: string | null): boolean { return !message.turnId || !turnId || message.turnId === turnId; } function groupStartedAt(group: TimelineGroup | undefined): string | undefined { if (!group) { return undefined; } switch (group.kind) { case "item": return group.item.occurredAt; case "activity": return group.items[0]?.occurredAt; case "turn": return group.startedAt; } } function asRecord(value: unknown): Record { return value !== null && typeof value === "object" ? (value as Record) : {}; } function stringValue(value: unknown): string { return typeof value === "string" ? value : ""; } function assistantMessagePhase(value: unknown): AgentMessageItem["phase"] { return value === "commentary" || value === "final_answer" ? value : undefined; } function numberOrNull(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } /** * Read the closed provider-rejection record a `summarization_failed` skip * carries. Only bounded identifiers are accepted; a provider message is never * part of the payload and would be ignored here anyway. */ function compactionProviderRejection( payload: Record, ): ContextCompactionItem["providerRejection"] { const record = asRecord(payload.providerRejection); if (typeof record.httpStatus !== "number" || !Number.isFinite(record.httpStatus)) { return null; } const text = (value: unknown): string | null => typeof value === "string" && value.trim() ? value : null; return { httpStatus: record.httpStatus, type: text(record.type), code: text(record.code), param: text(record.param), requestId: text(record.requestId), }; } function compactionTrigger(payload: Record): ContextCompactionItem["trigger"] { const trigger = payload.trigger; return trigger === "auto" || trigger === "operator" || trigger === "proactive" || trigger === "overflow" ? trigger : null; } /** * Keep one landmark per turn: a later started/compacted/skipped settles the * open started row instead of stacking notices. */ function settleOrPushContextCompaction( items: TimelineItem[], next: Omit, ): void { const openIndex = findOpenContextCompactionIndex(items, next.turnId); if (openIndex >= 0) { const open = items[openIndex]; if (open?.kind === "context-compaction") { items[openIndex] = { ...open, ...next, // Prefer the settled event id so keys stay stable with the finish row. id: next.phase === "started" ? open.id : next.id, trigger: next.trigger ?? open.trigger, estimatedTokensBefore: next.estimatedTokensBefore ?? open.estimatedTokensBefore, implementation: next.implementation ?? open.implementation, }; return; } } items.push({ kind: "context-compaction", ...next }); } function findOpenContextCompactionIndex(items: TimelineItem[], turnId: string | null): number { for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]; if (item?.kind !== "context-compaction" || item.phase !== "started") { continue; } if (turnId && item.turnId && item.turnId !== turnId) { continue; } return index; } return -1; } function machineInputMembers(value: unknown): MachineInputBatchItem["members"] { const kinds = new Set([ "scheduled_occurrence", "goal_continuation", "background_command_result", "session_wait_timeout", "agent_message", "agent_steer_instruction", "child_terminal_result", "media_generation_result", "child_requires_action", "child_requires_action_resolved", "child_paused", "child_waiting_capacity", "child_progress", ]); const classifications = new Set([ "success", "failure", "action_required", "info", ]); return Array.isArray(value) ? value.flatMap((candidate) => { const member = asRecord(candidate); return typeof member.id === "string" && typeof member.kind === "string" && kinds.has(member.kind as MachineInputBatchItem["members"][number]["kind"]) && typeof member.classification === "string" && classifications.has( member.classification as MachineInputBatchItem["members"][number]["classification"], ) && typeof member.sourceId === "string" ? (() => { const kind = member.kind as MachineInputBatchItem["members"][number]["kind"]; const result = kind === "media_generation_result" ? parseMediaGenerationResult(member.result) : null; return [ { id: member.id, kind, classification: member.classification as MachineInputBatchItem["members"][number]["classification"], sourceId: member.sourceId, summary: stringValue(member.summary), ...(result ? { result } : {}), }, ]; })() : []; }) : []; } const SESSION_STATUSES: readonly SessionStatus[] = [ "queued", "running", "idle", "requires_action", "recovering", "waiting_capacity", "failed", "cancelled", ]; /** Statuses that demand the reader's attention and so earn a timeline divider. */ const ATTENTION_STATUSES: ReadonlySet = new Set([ "requires_action", "failed", "cancelled", ]); /** Keep only entries that match the wire shapes; user payloads are untyped. */ function resourceRefs(value: unknown): import("@opengeni/sdk").ResourceRef[] { if (!Array.isArray(value)) { return []; } return value.filter((entry): entry is import("@opengeni/sdk").ResourceRef => { const record = asRecord(entry); if (record.kind === "repository") { return typeof record.uri === "string" && typeof record.ref === "string"; } return record.kind === "file" && typeof record.fileId === "string"; }); } /** The person behind a message, when its initiator is a signed-in subject. */ function messageSender(value: unknown): import("./types").MessageSender | undefined { const record = asRecord(value); if (record.kind !== "subject" || typeof record.subjectId !== "string" || !record.subjectId) return undefined; const label = typeof record.label === "string" && record.label.trim() ? record.label : null; return { subjectId: record.subjectId, label }; } function timelineAnnotations(value: unknown): TimelineAnnotation[] { if (!Array.isArray(value)) return []; return value.filter((candidate): candidate is TimelineAnnotation => { const annotation = asRecord(candidate); const source = asRecord(annotation.source); return ( typeof annotation.id === "string" && typeof annotation.ordinal === "number" && typeof annotation.quote === "string" && typeof annotation.note === "string" && typeof source.eventId === "string" && typeof source.eventType === "string" && typeof source.sequence === "number" && typeof source.startOffset === "number" && typeof source.endOffset === "number" ); }); } function toolRefs(value: unknown): import("@opengeni/sdk").ToolRef[] { if (!Array.isArray(value)) { return []; } return value.filter((entry): entry is import("@opengeni/sdk").ToolRef => { const record = asRecord(entry); return record.kind === "mcp" && typeof record.id === "string"; }); } function workerCompletionPayload(value: unknown): { childSessionId: string; childStatus: string; goalStatus: string | null; goalText: string | null; evidence: string | null; pausedReason: string | null; } | null { const payload = asRecord(value); const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if ( typeof payload.childSessionId !== "string" || !uuidPattern.test(payload.childSessionId) || typeof payload.status !== "string" || payload.status.trim() === "" ) { return null; } const goal = asRecord(payload.goal); return { childSessionId: payload.childSessionId, childStatus: payload.status, goalStatus: typeof goal.status === "string" ? goal.status : null, goalText: typeof goal.text === "string" ? goal.text : null, evidence: typeof goal.evidence === "string" ? goal.evidence : null, // Console pauses and several server paths put the human explanation on // `rationale`, not `pausedReason` — same fallback the goal pill uses, so a // paused worker card never shows "Worker paused" with the reason missing. pausedReason: typeof goal.pausedReason === "string" ? goal.pausedReason : typeof goal.rationale === "string" ? goal.rationale : null, }; } function isSessionStatus(value: unknown): value is SessionStatus { return typeof value === "string" && (SESSION_STATUSES as readonly string[]).includes(value); } /** Does this tool output represent an error (explicit flag or MCP `isError`)? */ function isErrorOutput(payload: Record): boolean { if (payload.error === true || payload.failed === true) { return true; } const output = payload.output; // Older receipts predate the explicit event error flag. if (isConnectedMachineOutcomeUnknownOutput(output)) return true; return ( !!output && typeof output === "object" && (output as { isError?: unknown }).isError === true ); } function toolCallTruncation(payload: Record): ToolCallTruncation | null { const truncation = asRecord(payload.truncation); const fullEvidence = asRecord(truncation.fullEvidence); const surface = stringValue(truncation.surface); const reason = stringValue(truncation.reason); if ( truncation.truncated !== true || !surface || !reason || typeof fullEvidence.available !== "boolean" ) { return null; } return { truncated: true, surface, reason, omittedBytes: numberOrNull(truncation.omittedBytes), fullEvidence: { available: fullEvidence.available, reason: stringValue(fullEvidence.reason) || null, }, }; } function findOpenCall( items: TimelineItem[], callId: string | null, callsById: ReadonlyMap, ): ToolCallItem | WorkerItem | undefined { if (callId) { // An explicit provider correlation id is authoritative. Falling back to // an unrelated running call corrupts its status/output when compact pages // or interleaved providers deliver an unmatched historical result. return callsById.get(callId); } for (let index = items.length - 1; index >= 0; index -= 1) { const item = items[index]!; if ((item.kind === "tool-call" || item.kind === "worker") && item.status === "running") { return item; } } return undefined; } function findOpenSandbox(items: TimelineItem[], name: string): SandboxItem | undefined { return [...items] .reverse() .find( (item): item is SandboxItem => item.kind === "sandbox" && item.name === name && item.status === "running", ); } function startupPhaseForSandboxOperation(name: string): StartupPhase | null { return SANDBOX_STARTUP_PHASES[name] ?? null; } function startupPhaseFromPayload(value: unknown): StartupPhase | null { return typeof value === "string" && STARTUP_PHASES.has(value as StartupPhase) ? (value as StartupPhase) : null; } function startupLifecycleWaitReason( payload: Record, ): StartupPhaseItem["blockedReason"] { // The first model request waited for context compaction: an expected step // of preparation that reopens after compacting, never a failure. if ( payload.expectedTransition === true && payload.failureStage === "context_compaction" && payload.failureCategory === "context_compaction" && payload.failureCode === "compaction_required" ) { return "compaction_required"; } if ( payload.expectedTransition !== true || payload.failureStage !== "lifecycle_wait" || payload.failureCategory !== "drain_capture_wait" ) { return undefined; } const code = payload.failureCode; return code === "capture_in_progress" || code === "rotation_in_progress" || code === "provider_recovery_in_progress" ? code : undefined; } const SANDBOX_STARTUP_PHASES: Record = { "sandbox.provision": "sandbox", "repository-clone": "repository", "file-resource-download": "files", }; const STARTUP_PHASES = new Set([ "queue", "sandbox", "rig", "repository", "files", "tools", "model_preparation", "provider_first_byte", ]); function elapsedDurationMs(startedAt: string, completedAt: string): number | null { const start = Date.parse(startedAt); const end = Date.parse(completedAt); return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null; } function failureMessage(t: OpenGeniText, payload: Record): string | null { return presentFailure(t, payload).reason; } function goalText(payload: Record): string | null { if (typeof payload.text === "string" && payload.text) { return payload.text; } const goal = asRecord(payload.goal); if (typeof goal.text === "string" && goal.text) { return goal.text; } if (typeof payload.prompt === "string" && payload.prompt) { return payload.prompt; } return null; } function rememberPendingWaitOutcome( pending: Map, event: SessionEvent, payload: Record, eventTurnId: string | null, ): void { const reason = stringValue(payload.reason).trim(); if (!reason) return; const turnId = eventTurnId || stringValue(payload.waitTurnId) || stringValue(payload.turnId) || null; pending.set(turnId, { id: event.id, reason, occurredAt: event.occurredAt }); } /** Wait reasons that need a person or a policy change, not only time. */ const ACTION_CAPACITY_WAIT_REASONS: ReadonlySet = new Set([ "pinned_account_disabled", "pinned_account_ineligible", "model_not_allowed", // Only sending the work again ends it (design 5.3 decision 4). "accepted_authority_unavailable", ]); /** * Recorded wait codes, for any subscription provider, that a person must * resolve: every account disabled for allocation, or the serving account * needing reconnection or refusing the request. None of them is a limit. */ const ACTION_CAPACITY_WAIT_CODE = /_(?:allocator_disabled|relogin_required|account_forbidden)$/; /** * A quota or capacity wait reads as "Usage limit reached" with one plain line * of guidance. It never promises that the work continues by itself: the * reader decides whether to wait for the quota to reset or to switch. Only a * wait that a person must resolve (reconnect, re-enable, choose another * account) keeps its recorded, actionable reason and is not labelled a limit. */ function capacityWaitPresentation( t: OpenGeniText, type: string, payload: Record, turnId: string | null, ): { turnId: string | null; label: string; detail: string; text: string } { if ( ACTION_CAPACITY_WAIT_REASONS.has(stringValue(payload.waitReason)) || ACTION_CAPACITY_WAIT_CODE.test(stringValue(payload.code)) ) { // Claude/SuperGrok waits record the readable sentence as `error` and an // internal note as `detail`; Codex waits keep their recorded order. const [first, second] = type === "turn.capacity_waiting" ? [payload.error, payload.detail] : [payload.detail, payload.error]; const detail = stringValue(first) || stringValue(second) || t("timeline.projection.waitingForAnAccount"); return { turnId, label: stringValue(payload.waitReason) === "pinned_account_disabled" ? t("timeline.projection.needsYou") : t("timeline.projection.waiting"), detail, text: detail, }; } return { turnId, label: CAPACITY_LIMIT_LABEL(t), detail: CAPACITY_LIMIT_DETAIL(t), text: `${CAPACITY_LIMIT_LABEL(t)}. ${CAPACITY_LIMIT_DETAIL(t)}`, }; } const CAPACITY_LIMIT_LABEL = (t: OpenGeniText) => t("timeline.projection.usageLimitReached"); const CAPACITY_LIMIT_DETAIL = (t: OpenGeniText) => t("timeline.projection.tryAgainAfterYourQuota"); /** Lifecycle evidence that a capacity wait is over. */ function endsCapacityWait(event: SessionEvent, payload: Record): boolean { if (event.duplicateOfEventId || (event.turnAssociation && event.turnAssociation !== "current")) return false; switch (event.type) { case "codex.capacity.resumed": case "codex.capacity.superseded": case "turn.completed": case "turn.failed": case "turn.cancelled": case "turn.superseded": return true; case "session.status.changed": return isSessionStatus(payload.status) && payload.status !== "waiting_capacity"; default: return false; } } function waitingOutcomeText(t: OpenGeniText, reason: string): string { return /^waiting\b/i.test(reason) ? reason : t("timeline.projection.waitingReason", { reason }); } /** * Agent-owned goal mutations already have an in-cluster tool row. Suppress the * breakaway landmark for those only. Empty wait turns retain the durable hold * reason and surface it after the folded steps. `goal.completed` has no actor * field today and is only emitted by the agent tool, so it is always * suppressed. API / system / create-session / continuation landmarks stay * visible. */ function shouldSuppressAgentGoalLandmark(type: string, payload: Record): boolean { if (type === "goal.completed") { return true; } if ( type === "goal.set" || type === "goal.updated" || type === "goal.paused" || type === "goal.held" ) { return payload.actor === "agent"; } return false; } /** * Fold a `memory.saved` / `memory.corrected` event into a {@link MemoryItem}. * Reads DEFENSIVELY (the payload is untyped `unknown`, no Zod schema): a missing * memory id means a malformed event, so we return null and the case drops it. */ function memoryItem( id: string, type: string, turnId: string | null, payload: Record, occurredAt: string, ): MemoryItem | null { const memoryId = typeof payload.memoryId === "string" && payload.memoryId ? payload.memoryId : null; if (!memoryId) { return null; } const replacementPreview = typeof payload.replacementPreview === "string" ? payload.replacementPreview : undefined; const replacementMemoryId = typeof payload.replacementMemoryId === "string" ? payload.replacementMemoryId : undefined; const action = typeof payload.action === "string" ? payload.action : undefined; return { kind: "memory", id, turnId, variant: type === "memory.corrected" ? "corrected" : "saved", memoryKind: stringValue(payload.kind), preview: stringValue(payload.preview), ...(payload.deduped === true ? { deduped: true } : {}), ...(replacementPreview ? { replacementPreview } : {}), ...(action ? { action } : {}), memoryId, ...(replacementMemoryId ? { replacementMemoryId } : {}), occurredAt, }; } const AUTH_NEEDED_REASONS: ReadonlySet = new Set([ "missing_connection", "expired", "insufficient_scope", "refresh_failed", "personal_authority_unavailable", "unsupported_auth", "resource_scope_unavailable", "designated_credential_unavailable", ]); function authNeededReason(value: unknown): AuthNeededItem["reason"] { return typeof value === "string" && AUTH_NEEDED_REASONS.has(value) ? (value as AuthNeededItem["reason"]) : null; } function capabilityAuthorizationRequest( value: unknown, ): NonNullable | null { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const record = value as Record; const kind = record.kind; const source = record.source; const action = record.action; if ( typeof record.id !== "string" || typeof record.name !== "string" || !["mcp", "api", "skill", "plugin"].includes(String(kind)) || !["built_in", "library", "configured", "public_registry", "registry", "manual"].includes( String(source), ) || !["connect", "add_credentials", "enable"].includes(String(action)) || typeof record.rationale !== "string" ) { return null; } return { id: record.id, name: record.name, kind: kind as NonNullable["kind"], source: source as NonNullable["source"], action: action as NonNullable["action"], rationale: record.rationale, requiredVariables: stringList(record.requiredVariables), }; } function customMcpSetupRequest(value: unknown): NonNullable | null { return parseCustomMcpSetupRequest(value); } function stringList(value: unknown): string[] { return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) : []; } /** * Some providers stream each reasoning summary part as its own delta * ("**Checking the workspace**", then "**Inspecting guidance**"). Joined * as-is the parts run together and their bold markers collide ("****"), so a * new part that opens bold right after one that closed bold starts a new * paragraph. Ordinary token deltas are joined unchanged. The worker's * write-time merge and the read-side coalescer apply the same rule * (`reasoningDeltaSeparator` in @opengeni/events), so stored, coalesced and * live fragments render identically. */ function reasoningDeltaJoiner(previous: string, next: string): string { return previous.endsWith("**") && next.startsWith("**") ? "\n\n" : ""; } function reasoningText(payload: unknown): string { const record = asRecord(payload); if (typeof record.text === "string") { return record.text; } const content = asRecord(asRecord(record.item).rawItem).content; if (!Array.isArray(content)) { return ""; } return content .map((part) => { const text = asRecord(part).text; return stringValue(text); }) .join(""); } /** The worker's initial/sent message from `session_create`/`session_send_message` args. */ function workerPrompt(args: unknown): string | null { const record = asRecord(typeof args === "string" ? tryParseJson(args) : args); for (const key of ["initialMessage", "message", "text", "prompt"] as const) { const value = record[key]; if (typeof value === "string" && value.trim().length > 0) { return value; } } return null; } const WORKER_TITLE_MAX_LENGTH = 120; /** The optional `session_create` title, as one bounded display line. */ function workerTitle(args: unknown): string | null { const record = asRecord(typeof args === "string" ? tryParseJson(args) : args); if (typeof record.title !== "string") return null; const title = record.title.replace(/\s+/g, " ").trim(); if (!title) return null; return title.length > WORKER_TITLE_MAX_LENGTH ? `${title.slice(0, WORKER_TITLE_MAX_LENGTH - 1).trimEnd()}…` : title; } function boundedWorkerFailureMessage(value: string): string | null { const normalized = value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim(); if (!normalized) return null; const encoded = new TextEncoder().encode(normalized); if (encoded.byteLength <= WORKER_FAILURE_MESSAGE_MAX_UTF8_BYTES) return normalized; let end = WORKER_FAILURE_MESSAGE_MAX_UTF8_BYTES; while (end > 0 && (encoded[end]! & 0xc0) === 0x80) end -= 1; return new TextDecoder().decode(encoded.slice(0, end)).trim(); } function boundedWorkerFailure(value: unknown): WorkerItem["failure"] { const record = asRecord(value); const code = typeof record.code === "string" ? record.code.trim() : ""; const message = typeof record.message === "string" ? boundedWorkerFailureMessage(record.message) : null; if (!/^[A-Za-z0-9._:-]{1,128}$/.test(code) || !message) return null; return { code: code.slice(0, WORKER_FAILURE_CODE_MAX_LENGTH), message }; } /** * Extract the API's structured orchestration failure from raw objects, JSON * text, or retained MCP CallToolResult wrappers. Legacy flat strings remain * unprojected rather than being mistaken for the trusted diagnostic contract. */ function workerFailure(value: unknown, depth = 0): WorkerItem["failure"] { if (depth > 6 || value === null || value === undefined) return null; if (typeof value === "string") { const parsed = tryParseJson(value); return parsed === value ? null : workerFailure(parsed, depth + 1); } if (Array.isArray(value)) { for (const entry of value) { const failure = workerFailure(entry, depth + 1); if (failure) return failure; } return null; } if (typeof value !== "object") return null; const record = value as Record; const direct = boundedWorkerFailure(record.error); if (direct) return direct; for (const key of ["structuredContent", "content", "result", "output"] as const) { if (!(key in record)) continue; const failure = workerFailure(record[key], depth + 1); if (failure) return failure; } if (record.type === "text" && typeof record.text === "string") { return workerFailure(record.text, depth + 1); } return null; } /** * Find a session id in orchestration tool arguments or output. Handles raw * objects, JSON strings, and MCP tool results (`{ content: [{ type: "text", * text: "{...}" }], structuredContent? }`). */ export function extractSessionRef(value: unknown, depth = 0): string | null { if (depth > 6 || value === null || value === undefined) { return null; } if (typeof value === "string") { return extractSessionRef(tryParseJson(value), depth + 1); } if (Array.isArray(value)) { for (const entry of value) { const found = extractSessionRef(entry, depth + 1); if (found) { return found; } } return null; } if (typeof value !== "object") { return null; } const record = value as Record; const receiptResource = asRecord(record.resource); if ( record.receiptVersion === "mcp-mutation-receipt.v1" && receiptResource.type === "session" && typeof receiptResource.id === "string" && looksLikeId(receiptResource.id) ) { return receiptResource.id; } if (typeof record.sessionId === "string" && looksLikeId(record.sessionId)) { return record.sessionId; } if ( typeof record.id === "string" && looksLikeId(record.id) && ("status" in record || "workspaceId" in record || "initialMessage" in record) ) { return record.id; } for (const key of ["structuredContent", "session", "result", "content"] as const) { if (key in record) { const found = extractSessionRef(record[key], depth + 1); if (found) { return found; } } } if (typeof record.text === "string") { return extractSessionRef(tryParseJson(record.text), depth + 1); } return null; } function looksLikeId(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); }