import type { ThreadStructure, ThreadStructureRuntime } from "@t3tools/client-runtime/state/shell"; import { resolveThreadWorkingStartedAt } from "@t3tools/client-runtime/state/models"; import { type AssetResource, type OrchestrationV2Checkpoint, type OrchestrationV2ExecutionNode, type OrchestrationV2PlanArtifact, type OrchestrationV2ProjectedTurnItem, type OrchestrationV2RunAttempt, type OrchestrationV2TurnItem, type PlanId, type RunId, type ToolActivitySurface, type ToolActivityIcon, type ToolActivitySource, type ThreadId, type TurnItemId, } from "@t3tools/contracts"; import { extractToolActivityPresentation } from "@t3tools/client-runtime/work-log/tool-presentation"; import { classifyToolActivity, collectToolFilePaths, formatReadToolLabel, formatSearchToolLabel, } from "@t3tools/shared/toolActivity"; import type { HtmlRenderReference } from "@t3tools/shared/htmlRender"; import { turnItemDetailRevision } from "@t3tools/client-runtime/work-log/item-detail"; import type { McpAppReference } from "@t3tools/shared/mcpApp"; import { htmlRenderFromToolItem, mcpAppFromToolItem } from "@t3tools/shared/toolOutput"; import { contextCompactionLabel, workEntryIndicatesToolFailure, } from "@t3tools/client-runtime/work-log/presentation"; import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; import type { ThreadPendingApproval, ThreadPendingUserInput, } from "@t3tools/client-runtime/state/thread-requests"; import type { ThreadRunSummary, ThreadRuntimeSummary } from "@t3tools/client-runtime/state/shell"; import { threadRuntimeHasInterruptibleRun } from "@t3tools/client-runtime/state/thread-execution"; import { turnItemIsWorkspacePreparation } from "@t3tools/client-runtime/state/turn-item-presentation"; import { isImageAttachment, type ChatAttachment, type ChatMessage, type ProposedPlan, type SessionPhase, type TurnDiffSummary, } from "./types"; import * as DateTime from "effect/DateTime"; import * as Equal from "effect/Equal"; import { shallow } from "zustand/vanilla/shallow"; export { formatDuration } from "@t3tools/shared/orchestrationTiming"; export { workEntryDisplayIndicatesToolFailure, workEntryIndicatesToolFailure, } from "@t3tools/client-runtime/work-log/presentation"; export type WorkLogToolLifecycleStatus = | "idle" | "inProgress" | "completed" | "failed" | "declined" | "stopped"; export interface WorkLogEntry { readonly questionAnswer?: import("@t3tools/contracts").UserInputAttachmentAnswerPayload; readonly id: string; readonly createdAt: string; readonly runId?: RunId | null; readonly label: string; readonly detail?: string; readonly command?: string; readonly rawCommand?: string; readonly changedFiles?: ReadonlyArray; readonly tone: "thinking" | "tool" | "info" | "error"; readonly toolTitle?: string; readonly toolCallId?: string; readonly viewedImagePath?: string; readonly toolSurface?: ToolActivitySurface; readonly toolIcon?: ToolActivityIcon; readonly toolSource?: ToolActivitySource; readonly sourceActivityKind?: string; readonly taskId?: string; readonly agentRole?: string; readonly toolData?: unknown; readonly requestKind?: string; readonly itemType?: OrchestrationV2TurnItem["type"]; readonly toolLifecycleStatus?: WorkLogToolLifecycleStatus; readonly structuredPayload?: OrchestrationV2TurnItem; readonly sourceItemType?: OrchestrationV2TurnItem["type"]; readonly projectedItem?: OrchestrationV2ProjectedTurnItem; } export type PendingApproval = ThreadPendingApproval; export type PendingUserInput = ThreadPendingUserInput; export interface LatestProposedPlanState { readonly id: PlanId; readonly createdAt: string; readonly updatedAt: string; readonly runId: RunId | null; readonly planMarkdown: string; readonly status: OrchestrationV2PlanArtifact["status"]; } export type TimelineAttempt = Pick< OrchestrationV2RunAttempt, "id" | "runId" | "attemptOrdinal" | "rootNodeId" | "status" | "completedAt" >; export type TimelineEntry = ( | { readonly id: string; readonly kind: "message"; readonly createdAt: string; readonly message: ChatMessage; readonly projectedItem?: OrchestrationV2ProjectedTurnItem; } | { readonly id: string; readonly kind: "proposed-plan"; readonly createdAt: string; readonly proposedPlan: ProposedPlan; } | { readonly id: string; readonly kind: "html-render"; readonly createdAt: string; readonly runId: RunId | null; readonly htmlRender: HtmlRenderReference; } | { readonly id: string; readonly kind: "mcp-app"; readonly createdAt: string; readonly runId: RunId | null; readonly sourceThreadId: ThreadId; readonly itemId: TurnItemId; readonly revision: string; readonly mcpApp: McpAppReference; } | { readonly id: string; readonly kind: "work"; readonly createdAt: string; readonly entry: WorkLogEntry; } | { readonly id: string; readonly kind: "event"; readonly createdAt: string; readonly projectedItem: OrchestrationV2ProjectedTurnItem; } ) & { readonly attempt?: TimelineAttempt; }; export function workLogEntryIsToolLike(entry: WorkLogEntry): boolean { return ( entry.tone === "tool" || entry.tone === "thinking" || entry.tone === "error" || entry.command !== undefined || entry.requestKind !== undefined ); } export function workEntrySignalsSevereFailure(entry: WorkLogEntry): boolean { return entry.itemType === "error"; } export function workEntryIndicatesToolSuccess(entry: WorkLogEntry): boolean { if ( !workLogEntryIsToolLike(entry) || workEntryIndicatesToolFailure(entry) || (entry.tone === "thinking" && entry.itemType !== "reasoning") ) { return false; } const status = entry.toolLifecycleStatus; return ( status !== "failed" && status !== "declined" && status !== "inProgress" && status !== "stopped" && status !== "idle" ); } export function workEntryIndicatesToolNeutralStatus(entry: WorkLogEntry): boolean { return ( workLogEntryIsToolLike(entry) && !workEntryIndicatesToolFailure(entry) && !workEntryIndicatesToolSuccess(entry) ); } export function isLatestRunSettled( latestRun: Pick | null, runtime: Pick | null, ): boolean { if (latestRun === null) return false; if ( latestRun.status === "preparing" || latestRun.status === "queued" || latestRun.status === "starting" || latestRun.status === "running" || latestRun.status === "waiting" ) return false; return runtime?.activeRunId !== latestRun.runId; } export function deriveActiveWorkStartedAt( latestRun: Pick< ThreadRunSummary, "runId" | "startedAt" | "requestedAt" | "completedAt" | "status" > | null, runtime: Pick | null, sendStartedAt: string | null, ): string | null { const startedAt = resolveThreadWorkingStartedAt({ latestRun, runtime }); return startedAt ?? (runtime?.activeRunId == null ? sendStartedAt : null); } export function derivePendingApprovals( approvals: ReadonlyArray, ): ThreadPendingApproval[] { return [...approvals].toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)); } export function derivePendingUserInputs( inputs: ReadonlyArray, ): ThreadPendingUserInput[] { return [...inputs].toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)); } function planItemTime( projection: ThreadStructure, plan: Pick, ): string { const item = projection.turnItems.findLast( (candidate) => (candidate.type === "proposed_plan" || candidate.type === "todo_list") && candidate.planId === plan.id, ); if (item?.type === "proposed_plan" || item?.type === "todo_list") { return DateTime.formatIso(item.updatedAt); } // Without a plan item, the plan's run dates it; the thread's own timestamps move while a reply streams. const run = projection.runs.find((candidate) => candidate.id === plan.runId); return DateTime.formatIso( run?.completedAt ?? run?.startedAt ?? run?.requestedAt ?? projection.thread.createdAt, ); } function toLatestProposedPlanState( projection: ThreadStructure, plan: Extract, ): LatestProposedPlanState { const updatedAt = planItemTime(projection, plan); return { id: plan.id, createdAt: updatedAt, updatedAt, runId: plan.runId, planMarkdown: plan.markdown, status: plan.status, }; } export function findLatestProposedPlan( projection: ThreadStructure | null, latestRunId: RunId | string | null | undefined, ): LatestProposedPlanState | null { if (projection === null) return null; const plans = projection.plans.filter((plan) => plan.kind === "proposed_plan"); const candidates = latestRunId ? plans.filter((plan) => plan.runId === latestRunId) : plans; const plan = [...(candidates.length > 0 ? candidates : plans)] .toSorted( (left, right) => planItemTime(projection, left).localeCompare(planItemTime(projection, right)) || left.id.localeCompare(right.id), ) .at(-1); return plan === undefined ? null : toLatestProposedPlanState(projection, plan); } export function hasActionableProposedPlan(plan: LatestProposedPlanState | null): boolean { return plan?.status === "active"; } const STANDALONE_V2_ITEM_TYPES = new Set([ "fork", "handoff", "run_interrupt_request", "run_interrupt_result", "secret_request", "subagent", ]); const PERSISTENT_RESOURCE_V2_ITEM_TYPES = new Set([ "fork", "thread_created", // Still answerable after a steer supersedes the attempt that asked. "secret_request", ]); export function timelineEntryIsPersistentResourceCard(entry: TimelineEntry): boolean { return ( entry.kind === "event" && PERSISTENT_RESOURCE_V2_ITEM_TYPES.has(entry.projectedItem.item.type) ); } function projectedItemCreatedAt(row: OrchestrationV2ProjectedTurnItem): string { return DateTime.formatIso(row.item.startedAt ?? row.item.updatedAt); } function projectedWorkEntryStatus( item: OrchestrationV2TurnItem, ): NonNullable { switch (item.status) { case "pending": case "running": case "waiting": return "inProgress"; case "completed": return "completed"; case "idle": return "idle"; case "failed": return "failed"; case "cancelled": case "interrupted": return "stopped"; } } function projectedWorkEntryTone(item: OrchestrationV2TurnItem): WorkLogEntry["tone"] { if (item.type === "error") return "info"; if (item.type === "reasoning") return "thinking"; switch (item.type) { case "command_execution": case "file_change": case "file_search": case "web_search": case "dynamic_tool": case "subagent": case "thread_created": case "user_input_request": case "approval_request": return "tool"; default: return "info"; } } export function providerErrorPresentation( item: Extract, ): { readonly label: string; readonly detail: string } { if (item.retry === undefined) { return { label: item.failure.class === "usage_limit" ? "Usage limit reached" : item.title?.trim() || "Provider error", detail: item.failure.message, }; } const progress = item.retry.maxAttempts === null ? `${item.retry.attempt}` : `${item.retry.attempt}/${item.retry.maxAttempts}`; const label = item.status === "running" ? `Retrying provider (${progress})` : item.status === "completed" ? `Provider recovered (${progress} retries)` : item.status === "failed" ? `${item.failure.class === "usage_limit" ? "Usage limit reached" : "Provider error"} after ${progress} retries` : `Provider retry stopped (${progress})`; const retryDelay = item.status === "running" && item.retry.retryDelayMs !== null && item.retry.retryDelayMs > 0 ? item.retry.retryDelayMs < 1_000 ? ` Retrying in ${item.retry.retryDelayMs}ms.` : ` Retrying in ${(item.retry.retryDelayMs / 1_000).toFixed(1).replace(/\.0$/u, "")}s.` : ""; return { label, detail: `${item.failure.message}${retryDelay}`, }; } function projectedWorkEntry(row: OrchestrationV2ProjectedTurnItem): WorkLogEntry { const { item } = row; const title = item.title?.trim() || null; const common = { id: item.id, createdAt: projectedItemCreatedAt(row), runId: item.runId, tone: projectedWorkEntryTone(item), itemType: item.type, toolLifecycleStatus: projectedWorkEntryStatus(item), structuredPayload: item, projectedItem: row, ...extractToolActivityPresentation(item), } as const; switch (item.type) { case "thread_created": return { ...common, label: "Created thread", }; case "compaction": return { ...common, label: contextCompactionLabel(item), sourceActivityKind: "context-compaction", ...(item.summary ? { detail: item.summary } : {}), }; case "reasoning": return { ...common, label: title ?? "Thinking", ...(item.text ? { detail: item.text } : {}), }; case "command_execution": return { ...common, label: title ?? "Ran command", command: item.input, rawCommand: item.input, toolTitle: title ?? "Command", toolData: item, }; case "file_change": { return { ...common, label: title ?? (item.changes !== undefined && item.changes.length > 1 ? `Changed ${item.changes.length} files` : `Changed ${item.fileName}`), changedFiles: item.changes?.map((change) => change.path) ?? [item.fileName], toolTitle: title ?? "File change", toolData: item, }; } case "file_search": return { ...common, label: title ?? formatSearchToolLabel(item) ?? "Searched files", ...(item.pattern ? { detail: item.pattern } : {}), toolTitle: title ?? "File search", toolData: item, }; case "web_search": return { ...common, label: title ?? "Searched the web", ...(item.patterns?.length ? { detail: item.patterns.join(", ") } : {}), toolTitle: title ?? "Web search", toolData: item, }; case "checkpoint": return { ...common, label: title ?? "Checkpoint captured", changedFiles: item.files.map((file) => file.path), toolData: item, }; case "system_notice": return { ...common, label: item.message, sourceActivityKind: "runtime.warning", }; case "error": { const presentation = providerErrorPresentation(item); return { ...common, ...presentation, ...(item.failure.class === "usage_limit" && item.status !== "completed" ? { sourceActivityKind: "runtime.warning" } : item.retry === undefined ? { sourceActivityKind: "runtime.error" } : {}), toolData: item, }; } case "dynamic_tool": { const classified = classifyToolActivity({ itemType: "dynamic_tool_call", data: { toolName: item.toolName ?? undefined, input: item.input }, }); const [readPath] = collectToolFilePaths({ input: item.input }); return { ...common, label: title ?? (classified === "read" ? formatReadToolLabel(readPath ?? "") : classified === "search" ? (formatSearchToolLabel({ input: item.input }) ?? item.toolName ?? "Tool call") : (item.toolName ?? "Tool call")), toolTitle: title ?? item.toolName ?? "Tool", toolData: { input: item.input, output: item.output }, }; } case "todo_list": { // Each todo update is a quiet row where the agent wrote it; it opens to every step. const done = item.steps.filter((step) => step.status === "completed").length; const current = item.steps.find((step) => step.status === "running") ?? item.steps.find((step) => step.status === "pending"); return { ...common, label: `Tasks ${done}/${item.steps.length}${current ? ` ยท ${current.text}` : ""}`, }; } case "approval_request": return { ...common, label: title ?? "Approval requested", detail: item.prompt ?? item.requestKind, toolData: item, }; case "user_input_request": return { ...common, label: title ?? (item.questionAnswer ? "Answered questions" : "Input requested"), ...(item.questionAnswer ? { questionAnswer: item.questionAnswer } : {}), toolData: item, }; default: return { ...common, label: title ?? item.type.replaceAll("_", " "), toolData: item, }; } } /** Preserve visibleTurnItems' committed order; interleave client messages by timestamp. */ export interface TimelineEntriesInput { readonly visibleTurnItems: ReadonlyArray; readonly optimisticMessages: ReadonlyArray; readonly anchoredMessages?: ReadonlyArray; readonly attachmentUrlById?: ReadonlyMap; readonly attempts?: ReadonlyArray; readonly nodes?: ReadonlyArray; readonly plans?: ReadonlyArray; } export interface TimelineEntriesProjection { readonly input: TimelineEntriesInput; readonly entries: TimelineEntry[]; } export function deriveTimelineEntriesFromVisibleTurnItems( input: TimelineEntriesInput, ): TimelineEntry[] { const committedMessageIds = new Set(); const entries: TimelineEntry[] = []; const attemptByRootNodeId = new Map( (input.attempts ?? []).map((attempt) => [attempt.rootNodeId, attempt] as const), ); const nodeById = new Map((input.nodes ?? []).map((node) => [node.id, node] as const)); const planById = new Map((input.plans ?? []).map((plan) => [plan.id, plan] as const)); const resolveAttempt = (item: OrchestrationV2TurnItem): TimelineAttempt | undefined => { if (item.nodeId === null || item.runId === null) return undefined; let nodeId: OrchestrationV2ExecutionNode["id"] | null = item.nodeId; const visited = new Set(); while (nodeId !== null && !visited.has(nodeId)) { visited.add(nodeId); const directAttempt = attemptByRootNodeId.get(nodeId); if (directAttempt?.runId === item.runId) return directAttempt; const node = nodeById.get(nodeId); if (node === undefined) return undefined; const rootAttempt = attemptByRootNodeId.get(node.rootNodeId); if (rootAttempt?.runId === item.runId) return rootAttempt; nodeId = node.parentNodeId; } return undefined; }; const foldedAnswerMessageIds = new Set( input.visibleTurnItems.flatMap(({ item }) => item.type === "user_input_request" && item.questionAnswer ? [`async-answer:${item.questionAnswer.requestId}`] : [], ), ); for (const row of input.visibleTurnItems) { const { item } = row; if (turnItemIsWorkspacePreparation(item)) continue; if (item.type === "checkpoint") continue; if (item.type === "user_message" && foldedAnswerMessageIds.has(item.messageId)) continue; const createdAt = projectedItemCreatedAt(row); const attempt = resolveAttempt(item); const attemptMetadata = attempt === undefined ? {} : { attempt }; if (item.type === "notification") { entries.push({ id: item.id, kind: "work", createdAt, entry: { id: item.id, createdAt, runId: item.runId, label: item.summary, tone: "info", itemType: item.type, structuredPayload: item, projectedItem: row, }, ...attemptMetadata, }); continue; } if (item.type === "user_message" || item.type === "assistant_message") { const message: ChatMessage = { id: item.messageId, role: item.type === "user_message" ? "user" : "assistant", text: item.text, ...(item.type === "user_message" && item.context ? { context: item.context } : {}), ...((item.attachments?.length ?? 0) > 0 ? { attachments: (item.attachments ?? []).map((attachment) => { const previewUrl = input.attachmentUrlById?.get(attachment.id); return previewUrl ? { ...attachment, previewUrl } : attachment; }), } : {}), runId: item.runId, streaming: item.type === "assistant_message" && item.streaming, ...(item.type === "user_message" ? { createdBy: item.createdBy, creationSource: item.creationSource, ...(item.senderThreadId !== undefined ? { senderThreadId: item.senderThreadId } : {}), ...(item.idleCompaction !== undefined ? { idleCompaction: item.idleCompaction } : {}), ...(item.scheduledTaskId !== undefined ? { scheduledTaskId: item.scheduledTaskId } : {}), } : {}), createdAt, updatedAt: DateTime.formatIso(item.updatedAt), ...(item.type === "user_message" ? { inputIntent: item.inputIntent } : {}), }; committedMessageIds.add(message.id); entries.push({ id: message.id, kind: "message", createdAt, message, projectedItem: row, ...attemptMetadata, }); continue; } if (item.type === "proposed_plan") { const plan = planById.get(item.planId); const proposedPlan = { id: item.planId, runId: item.runId, planMarkdown: item.markdown, status: plan?.kind === "proposed_plan" ? plan.status : ("active" as const), createdAt, updatedAt: DateTime.formatIso(item.updatedAt), }; entries.push({ id: item.id, kind: "proposed-plan", createdAt, proposedPlan, ...attemptMetadata, }); continue; } const htmlRender = item.type === "dynamic_tool" && item.status === "completed" ? htmlRenderFromToolItem(item) : undefined; if (htmlRender !== undefined) { entries.push({ id: item.id, kind: "html-render", createdAt, runId: item.runId, htmlRender, ...attemptMetadata, }); continue; } const mcpApp = item.type === "dynamic_tool" && item.status === "completed" ? mcpAppFromToolItem(item) : undefined; if (mcpApp !== undefined) { entries.push({ id: item.id, kind: "mcp-app", createdAt, runId: item.runId, sourceThreadId: row.sourceThreadId, itemId: row.sourceItemId, revision: turnItemDetailRevision(item), mcpApp, ...attemptMetadata, }); continue; } if (STANDALONE_V2_ITEM_TYPES.has(item.type)) { entries.push({ id: item.id, kind: "event", createdAt, projectedItem: row, ...attemptMetadata, }); continue; } entries.push({ id: item.id, kind: "work", createdAt, entry: projectedWorkEntry(row), ...attemptMetadata, }); } const retainedMessageIds = new Set([...committedMessageIds, ...foldedAnswerMessageIds]); for (const message of input.anchoredMessages ?? []) { if (retainedMessageIds.has(message.id)) continue; retainedMessageIds.add(message.id); const entry: TimelineEntry = { id: message.id, kind: "message", createdAt: message.createdAt, message, }; const insertionIndex = entries.findIndex( (candidate) => candidate.createdAt > message.createdAt, ); if (insertionIndex === -1) { entries.push(entry); } else { entries.splice(insertionIndex, 0, entry); } } for (const message of input.optimisticMessages) { if (message.inputIntent !== "queued_turn" && !retainedMessageIds.has(message.id)) { retainedMessageIds.add(message.id); entries.push({ id: message.id, kind: "message", createdAt: message.createdAt, message, }); } } return entries; } type AttachmentResource = Extract; const EMPTY_IMAGE_RESOURCES = Object.freeze>([]); export function selectMessageImageResources( attachments: ChatMessage["attachments"], ): ReadonlyArray { const attachmentIds = new Set(); for (const attachment of attachments ?? []) { if (!isImageAttachment(attachment)) continue; const previewUrl = attachment.previewUrl; if (previewUrl?.startsWith("blob:") || previewUrl?.startsWith("data:")) continue; attachmentIds.add(attachment.id); } return attachmentIds.size === 0 ? EMPTY_IMAGE_RESOURCES : Array.from(attachmentIds, (attachmentId) => ({ _tag: "attachment", attachmentId })); } export function selectHandoffImageResources( messages: ReadonlyArray> | undefined, handoffs: Readonly>>, ): ReadonlyArray { if (Object.keys(handoffs).length === 0) return EMPTY_IMAGE_RESOURCES; const attachmentIds = new Set(); for (const message of messages ?? []) { if (message.role !== "user" || !handoffs[message.id]?.length) continue; for (const attachment of message.attachments ?? []) { if (isImageAttachment(attachment)) attachmentIds.add(attachment.id); } } return attachmentIds.size === 0 ? EMPTY_IMAGE_RESOURCES : Array.from(attachmentIds, (attachmentId) => ({ _tag: "attachment", attachmentId })); } export function createMessageAttachmentPreviewProjector() { const attachmentsBySource = new WeakMap< ReadonlyArray, ReadonlyArray >(); const messagesBySource = new WeakMap(); return ( message: ChatMessage, previewUrlFor: (attachment: ChatAttachment) => string | undefined, ): ChatMessage => { const source = message.attachments; if (!source || source.length === 0) return message; const previous = attachmentsBySource.get(source) ?? source; let changed: ChatAttachment[] | undefined; let hasOverrides = false; for (const [index, attachment] of source.entries()) { const previewUrl = previewUrlFor(attachment); const sourceUrl = "previewUrl" in attachment ? attachment.previewUrl : undefined; const previousAttachment = previous[index]!; const previousUrl = "previewUrl" in previousAttachment ? previousAttachment.previewUrl : undefined; const next = !previewUrl || previewUrl === sourceUrl ? attachment : previewUrl === previousUrl ? previousAttachment : { ...attachment, previewUrl }; hasOverrides ||= next !== attachment; if (next !== previousAttachment) { changed ??= previous.slice(); changed[index] = next; } } const attachments = hasOverrides ? (changed ?? previous) : source; attachmentsBySource.set(source, attachments); if (attachments === source) { messagesBySource.delete(message); return message; } const previousMessage = messagesBySource.get(message); if (previousMessage?.attachments === attachments) return previousMessage; const result = { ...message, attachments }; messagesBySource.set(message, result); return result; }; } export function isStreamingMessageTextUpdate(previous: ChatMessage, next: ChatMessage): boolean { if ( previous.role !== "assistant" || next.role !== "assistant" || !previous.streaming || !next.streaming ) { return false; } const { text: _previousText, updatedAt: _previousUpdatedAt, ...previousMetadata } = previous; const { text: _nextText, updatedAt: _nextUpdatedAt, ...nextMetadata } = next; return shallow(previousMetadata, nextMetadata); } export function isStreamingTurnItemTextUpdate( previous: OrchestrationV2ProjectedTurnItem, next: OrchestrationV2ProjectedTurnItem, ): boolean { const { item: previousItem, ...previousSource } = previous; const { item: nextItem, ...nextSource } = next; if ( previousItem.type !== "assistant_message" || nextItem.type !== "assistant_message" || !previousItem.streaming || !nextItem.streaming || !shallow(previousSource, nextSource) || projectedItemCreatedAt(previous) !== projectedItemCreatedAt(next) ) { return false; } const { text: _previousText, updatedAt: _previousUpdatedAt, ...previousMetadata } = previousItem; const { text: _nextText, updatedAt: _nextUpdatedAt, ...nextMetadata } = nextItem; // Wire decoding recreates timestamps and attachments; compare only the changed item's metadata. return Equal.equals(previousMetadata, nextMetadata); } function reuseTimelineEntries( input: TimelineEntriesInput, previous: TimelineEntriesProjection, ): TimelineEntry[] | null { const before = previous.input; if ( input.visibleTurnItems.length < before.visibleTurnItems.length || !shallow(input.optimisticMessages, before.optimisticMessages) || !shallow(input.anchoredMessages, before.anchoredMessages) || !shallow(input.attachmentUrlById, before.attachmentUrlById) || !shallow(input.attempts, before.attempts) || !shallow(input.nodes, before.nodes) || !shallow(input.plans, before.plans) ) { return null; } const appended = input.visibleTurnItems.length > before.visibleTurnItems.length; // Keep full projection until committed items interleave and deduplicate anchored/optimistic messages. if ( appended && (input.optimisticMessages.length > 0 || (input.anchoredMessages?.length ?? 0) > 0) ) { return null; } // Answer rows can replace a message already present in the retained prefix. if ( appended && input.visibleTurnItems .slice(before.visibleTurnItems.length) .some( ({ item }) => (item.type === "user_input_request" && item.questionAnswer !== undefined) || (item.type === "user_message" && item.messageId.startsWith("async-answer:")), ) ) return null; const replacements = new Map< OrchestrationV2ProjectedTurnItem, OrchestrationV2ProjectedTurnItem >(); for (let index = 0; index < before.visibleTurnItems.length; index += 1) { const previousItem = before.visibleTurnItems[index]!; const item = input.visibleTurnItems[index]!; if (item === previousItem) continue; if (!isStreamingTurnItemTextUpdate(previousItem, item)) return null; replacements.set(previousItem, item); } if (replacements.size === 0 && !appended) return previous.entries; const entries = previous.entries.map((entry): TimelineEntry => { const row = entry.kind === "message" && entry.projectedItem !== undefined ? replacements.get(entry.projectedItem) : undefined; if (entry.kind !== "message" || row?.item.type !== "assistant_message") return entry; return { ...entry, projectedItem: row, message: { ...entry.message, text: row.item.text, updatedAt: DateTime.formatIso(row.item.updatedAt), }, }; }); if (appended) { entries.push( ...deriveTimelineEntriesFromVisibleTurnItems({ ...input, visibleTurnItems: input.visibleTurnItems.slice(before.visibleTurnItems.length), }), ); } return entries; } export function deriveTimelineEntriesFromVisibleTurnItemsWithState( input: TimelineEntriesInput, previous: TimelineEntriesProjection | null = null, ): TimelineEntriesProjection { const reused = previous === null ? null : reuseTimelineEntries(input, previous); if (reused !== null) return { input, entries: reused }; const entries = deriveTimelineEntriesFromVisibleTurnItems(input); if (previous === null || !shallow(input.attachmentUrlById, previous.input.attachmentUrlById)) { return { input, entries }; } const previousMessages = new Map( previous.entries.flatMap((entry) => entry.kind === "message" ? [[entry.id, entry] as const] : [], ), ); return { input, entries: entries.map((entry) => { if (entry.kind !== "message" || entry.projectedItem === undefined) return entry; const before = previousMessages.get(entry.id); if (before?.projectedItem !== entry.projectedItem) return entry; return before.attempt === entry.attempt ? before : { ...entry, message: before.message }; }), }; } // Run ordinals keep increasing after a rewind; only visible local runs define predecessors. export function deriveRevertTurnCountByUserMessageId(input: { readonly timelineEntries: ReadonlyArray; readonly checkpoints: ReadonlyArray; readonly threadStartCheckpointReady: boolean; readonly availableRevertTurnCounts?: ReadonlySet | undefined; readonly unsettledRunId?: RunId | null; }): Map { const readyTurnCountByRunId = new Map(); for (const checkpoint of input.checkpoints) { if (checkpoint.status === "ready") { readyTurnCountByRunId.set(checkpoint.runId, checkpoint.checkpointTurnCount); } } const targetByRunId = new Map(); const byUserMessageId = new Map(); let previousRunId: RunId | null = null; for (const entry of input.timelineEntries) { // Inherited history belongs to the source thread, not our checkpoint scope. if (entry.kind !== "message" && entry.kind !== "work") continue; const projectedItem = entry.kind === "message" ? entry.projectedItem : entry.entry.projectedItem; if (projectedItem?.visibility === "inherited") continue; const runId = entry.kind === "message" ? entry.message.runId : entry.entry.itemType === "notification" ? entry.entry.runId : null; if (runId == null) continue; if (!targetByRunId.has(runId)) { targetByRunId.set( runId, previousRunId === null ? input.threadStartCheckpointReady ? 0 : undefined : readyTurnCountByRunId.get(previousRunId), ); previousRunId = runId; } if (entry.kind !== "message") continue; const { message } = entry; if (message.role !== "user" || message.runId === input.unsettledRunId) continue; if (message.inputIntent !== "turn_start" && message.inputIntent !== "queued_turn") continue; const target = targetByRunId.get(runId); if ( target !== undefined && (input.availableRevertTurnCounts === undefined || input.availableRevertTurnCounts.has(target)) ) byUserMessageId.set(message.id, target); } return byUserMessageId; } export function findRevertCheckpoint( checkpoints: ReadonlyArray, turnCount: number, ): OrchestrationV2Checkpoint | undefined { return checkpoints.findLast( (checkpoint) => checkpoint.status === "ready" && (turnCount === 0 ? checkpoint.ordinalWithinScope === 0 && checkpoint.appRunOrdinal === null : checkpoint.appRunOrdinal === turnCount), ); } export function derivePhase(runtime: ThreadStructureRuntime | null): SessionPhase { if (runtime === null) return "disconnected"; if ( runtime.status === "preparing" || runtime.status === "starting" || runtime.status === "queued" ) return "connecting"; if (runtime.status === "running" || runtime.status === "waiting") return "running"; return "ready"; } /** Preparing and starting runs can be interrupted; Stop need not wait for running. */ export function deriveCanInterruptRunningThread( hasActiveThread: boolean, runtime: ThreadStructureRuntime | null, ): boolean { return ( hasActiveThread && (derivePhase(runtime) === "running" || threadRuntimeHasInterruptibleRun(runtime)) ); } export type { TurnDiffSummary };