import { type OrchestrationEvent, type OrchestrationMessage, type OrchestrationProposedPlan, type ProjectId, type ProviderKind, ThreadId, type OrchestrationReadModel, type OrchestrationSession, type OrchestrationCheckpointSummary, type OrchestrationThread, type OrchestrationSessionStatus,} from "@t3tools/contracts";import { resolveModelSlugForProvider } from "@t3tools/shared/model";import { create } from "zustand";import { findLatestProposedPlan, hasActionableProposedPlan, derivePendingApprovals, derivePendingUserInputs,} from "./session-logic";import { sanitizeThreadErrorMessage } from "./rpc/transportError";import { type ChatMessage, type Project, type SidebarThreadSummary, type Thread } from "./types"; // ── State ──────────────────────────────────────────────────────────── export interface AppState { projects: Project[]; threads: Thread[]; sidebarThreadsById: Record<string, SidebarThreadSummary>; threadIdsByProjectId: Record<string, ThreadId[]>; bootstrapComplete: boolean;} const initialState: AppState = { projects: [], threads: [], sidebarThreadsById: {}, threadIdsByProjectId: {}, bootstrapComplete: false,};const MAX_THREAD_MESSAGES = 2_000;const MAX_THREAD_CHECKPOINTS = 500;const MAX_THREAD_PROPOSED_PLANS = 200;const MAX_THREAD_ACTIVITIES = 500;const EMPTY_THREAD_IDS: ThreadId[] = []; // ── Pure helpers ────────────────────────────────────────────────────── function updateThread( threads: Thread[], threadId: ThreadId, updater: (t: Thread) => Thread,): Thread[] { let changed = false; const next = threads.map((t) => { if (t.id !== threadId) return t; const updated = updater(t); if (updated !== t) changed = true; return updated; }); return changed ? next : threads;} function updateProject( projects: Project[], projectId: Project["id"], updater: (project: Project) => Project,): Project[] { let changed = false; const next = projects.map((project) => { if (project.id !== projectId) { return project; } const updated = updater(project); if (updated !== project) { changed = true; } return updated; }); return changed ? next : projects;} function normalizeModelSelection<T extends { provider: "codex" | "claudeAgent"; model: string }>( selection: T,): T { return { ...selection, model: resolveModelSlugForProvider(selection.provider, selection.model), };} function mapProjectScripts(scripts: ReadonlyArray<Project["scripts"][number]>): Project["scripts"] { return scripts.map((script) => ({ ...script }));} function mapSession(session: OrchestrationSession): Thread["session"] { return { provider: toLegacyProvider(session.providerName), status: toLegacySessionStatus(session.status), orchestrationStatus: session.status, activeTurnId: session.activeTurnId ?? undefined, createdAt: session.updatedAt, updatedAt: session.updatedAt, ...(session.lastError ? { lastError: session.lastError } : {}), };} function mapMessage(message: OrchestrationMessage): ChatMessage { const attachments = message.attachments?.map((attachment) => ({ type: "image" as const, id: attachment.id, name: attachment.name, mimeType: attachment.mimeType, sizeBytes: attachment.sizeBytes, previewUrl: toAttachmentPreviewUrl(attachmentPreviewRoutePath(attachment.id)), })); return { id: message.id, role: message.role, text: message.text, turnId: message.turnId, createdAt: message.createdAt, streaming: message.streaming, ...(message.streaming ? {} : { completedAt: message.updatedAt }), ...(attachments && attachments.length > 0 ? { attachments } : {}), };} function mapProposedPlan(proposedPlan: OrchestrationProposedPlan): Thread["proposedPlans"][number] { return { id: proposedPlan.id, turnId: proposedPlan.turnId, planMarkdown: proposedPlan.planMarkdown, implementedAt: proposedPlan.implementedAt, implementationThreadId: proposedPlan.implementationThreadId, createdAt: proposedPlan.createdAt, updatedAt: proposedPlan.updatedAt, };} function mapTurnDiffSummary( checkpoint: OrchestrationCheckpointSummary,): Thread["turnDiffSummaries"][number] { return { turnId: checkpoint.turnId, completedAt: checkpoint.completedAt, status: checkpoint.status, assistantMessageId: checkpoint.assistantMessageId ?? undefined, checkpointTurnCount: checkpoint.checkpointTurnCount, checkpointRef: checkpoint.checkpointRef, files: checkpoint.files.map((file) => ({ ...file })), };} function mapThread(thread: OrchestrationThread): Thread { return { id: thread.id, codexThreadId: null, projectId: thread.projectId, title: thread.title, modelSelection: normalizeModelSelection(thread.modelSelection), runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, session: thread.session ? mapSession(thread.session) : null, messages: thread.messages.map(mapMessage), proposedPlans: thread.proposedPlans.map(mapProposedPlan), error: sanitizeThreadErrorMessage(thread.session?.lastError), createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, pendingSourceProposedPlan: thread.latestTurn?.sourceProposedPlan, branch: thread.branch, worktreePath: thread.worktreePath, turnDiffSummaries: thread.checkpoints.map(mapTurnDiffSummary), activities: thread.activities.map((activity) => ({ ...activity })), };} function mapProject(project: OrchestrationReadModel["projects"][number]): Project { return { id: project.id, name: project.title, cwd: project.workspaceRoot, defaultModelSelection: project.defaultModelSelection ? normalizeModelSelection(project.defaultModelSelection) : null, createdAt: project.createdAt, updatedAt: project.updatedAt, scripts: mapProjectScripts(project.scripts), };} function getLatestUserMessageAt( messages: ReadonlyArray<Thread["messages"][number]>,): string | null { let latestUserMessageAt: string | null = null; for (const message of messages) { if (message.role !== "user") { continue; } if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { latestUserMessageAt = message.createdAt; } } return latestUserMessageAt;} function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary { return { id: thread.id, projectId: thread.projectId, title: thread.title, interactionMode: thread.interactionMode, session: thread.session, createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, branch: thread.branch, worktreePath: thread.worktreePath, latestUserMessageAt: getLatestUserMessageAt(thread.messages), hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, hasActionableProposedPlan: hasActionableProposedPlan( findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), ), };} function sidebarThreadSummariesEqual( left: SidebarThreadSummary | undefined, right: SidebarThreadSummary,): boolean { return ( left !== undefined && left.id === right.id && left.projectId === right.projectId && left.title === right.title && left.interactionMode === right.interactionMode && left.session === right.session && left.createdAt === right.createdAt && left.archivedAt === right.archivedAt && left.updatedAt === right.updatedAt && left.latestTurn === right.latestTurn && left.branch === right.branch && left.worktreePath === right.worktreePath && left.latestUserMessageAt === right.latestUserMessageAt && left.hasPendingApprovals === right.hasPendingApprovals && left.hasPendingUserInput === right.hasPendingUserInput && left.hasActionableProposedPlan === right.hasActionableProposedPlan );} function appendThreadIdByProjectId( threadIdsByProjectId: Record<string, ThreadId[]>, projectId: ProjectId, threadId: ThreadId,): Record<string, ThreadId[]> { const existingThreadIds = threadIdsByProjectId[projectId] ?? EMPTY_THREAD_IDS; if (existingThreadIds.includes(threadId)) { return threadIdsByProjectId; } return { ...threadIdsByProjectId, [projectId]: [...existingThreadIds, threadId], };} function removeThreadIdByProjectId( threadIdsByProjectId: Record<string, ThreadId[]>, projectId: ProjectId, threadId: ThreadId,): Record<string, ThreadId[]> { const existingThreadIds = threadIdsByProjectId[projectId] ?? EMPTY_THREAD_IDS; if (!existingThreadIds.includes(threadId)) { return threadIdsByProjectId; } const nextThreadIds = existingThreadIds.filter( (existingThreadId) => existingThreadId !== threadId, ); if (nextThreadIds.length === existingThreadIds.length) { return threadIdsByProjectId; } if (nextThreadIds.length === 0) { const nextThreadIdsByProjectId = { ...threadIdsByProjectId }; delete nextThreadIdsByProjectId[projectId]; return nextThreadIdsByProjectId; } return { ...threadIdsByProjectId, [projectId]: nextThreadIds, };} function buildThreadIdsByProjectId(threads: ReadonlyArray<Thread>): Record<string, ThreadId[]> { const threadIdsByProjectId: Record<string, ThreadId[]> = {}; for (const thread of threads) { const existingThreadIds = threadIdsByProjectId[thread.projectId] ?? EMPTY_THREAD_IDS; threadIdsByProjectId[thread.projectId] = [...existingThreadIds, thread.id]; } return threadIdsByProjectId;} function buildSidebarThreadsById( threads: ReadonlyArray<Thread>,): Record<string, SidebarThreadSummary> { return Object.fromEntries( threads.map((thread) => [thread.id, buildSidebarThreadSummary(thread)]), );} function checkpointStatusToLatestTurnState(status: "ready" | "missing" | "error") { if (status === "error") { return "error" as const; } if (status === "missing") { return "interrupted" as const; } return "completed" as const;} function compareActivities( left: Thread["activities"][number], right: Thread["activities"][number],): number { if (left.sequence !== undefined && right.sequence !== undefined) { if (left.sequence !== right.sequence) { return left.sequence - right.sequence; } } else if (left.sequence !== undefined) { return 1; } else if (right.sequence !== undefined) { return -1; } return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);} function buildLatestTurn(params: { previous: Thread["latestTurn"]; turnId: NonNullable<Thread["latestTurn"]>["turnId"]; state: NonNullable<Thread["latestTurn"]>["state"]; requestedAt: string; startedAt: string | null; completedAt: string | null; assistantMessageId: NonNullable<Thread["latestTurn"]>["assistantMessageId"]; sourceProposedPlan?: Thread["pendingSourceProposedPlan"];}): NonNullable<Thread["latestTurn"]> { const resolvedPlan = params.previous?.turnId === params.turnId ? params.previous.sourceProposedPlan : params.sourceProposedPlan; return { turnId: params.turnId, state: params.state, requestedAt: params.requestedAt, startedAt: params.startedAt, completedAt: params.completedAt, assistantMessageId: params.assistantMessageId, ...(resolvedPlan ? { sourceProposedPlan: resolvedPlan } : {}), };} function rebindTurnDiffSummariesForAssistantMessage( turnDiffSummaries: ReadonlyArray<Thread["turnDiffSummaries"][number]>, turnId: Thread["turnDiffSummaries"][number]["turnId"], assistantMessageId: NonNullable<Thread["latestTurn"]>["assistantMessageId"],): Thread["turnDiffSummaries"] { let changed = false; const nextSummaries = turnDiffSummaries.map((summary) => { if (summary.turnId !== turnId || summary.assistantMessageId === assistantMessageId) { return summary; } changed = true; return { ...summary, assistantMessageId: assistantMessageId ?? undefined, }; }); return changed ? nextSummaries : [...turnDiffSummaries];} function retainThreadMessagesAfterRevert( messages: ReadonlyArray<ChatMessage>, retainedTurnIds: ReadonlySet<string>, turnCount: number,): ChatMessage[] { const retainedMessageIds = new Set<string>(); for (const message of messages) { if (message.role === "system") { retainedMessageIds.add(message.id); continue; } if ( message.turnId !== undefined && message.turnId !== null && retainedTurnIds.has(message.turnId) ) { retainedMessageIds.add(message.id); } } const retainedUserCount = messages.filter( (message) => message.role === "user" && retainedMessageIds.has(message.id), ).length; const missingUserCount = Math.max(0, turnCount - retainedUserCount); if (missingUserCount > 0) { const fallbackUserMessages = messages .filter( (message) => message.role === "user" && !retainedMessageIds.has(message.id) && (message.turnId === undefined || message.turnId === null || retainedTurnIds.has(message.turnId)), ) .toSorted( (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), ) .slice(0, missingUserCount); for (const message of fallbackUserMessages) { retainedMessageIds.add(message.id); } } const retainedAssistantCount = messages.filter( (message) => message.role === "assistant" && retainedMessageIds.has(message.id), ).length; const missingAssistantCount = Math.max(0, turnCount - retainedAssistantCount); if (missingAssistantCount > 0) { const fallbackAssistantMessages = messages .filter( (message) => message.role === "assistant" && !retainedMessageIds.has(message.id) && (message.turnId === undefined || message.turnId === null || retainedTurnIds.has(message.turnId)), ) .toSorted( (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), ) .slice(0, missingAssistantCount); for (const message of fallbackAssistantMessages) { retainedMessageIds.add(message.id); } } return messages.filter((message) => retainedMessageIds.has(message.id));} function retainThreadActivitiesAfterRevert( activities: ReadonlyArray<Thread["activities"][number]>, retainedTurnIds: ReadonlySet<string>,): Thread["activities"] { return activities.filter( (activity) => activity.turnId === null || retainedTurnIds.has(activity.turnId), );} function retainThreadProposedPlansAfterRevert( proposedPlans: ReadonlyArray<Thread["proposedPlans"][number]>, retainedTurnIds: ReadonlySet<string>,): Thread["proposedPlans"] { return proposedPlans.filter( (proposedPlan) => proposedPlan.turnId === null || retainedTurnIds.has(proposedPlan.turnId), );} function toLegacySessionStatus( status: OrchestrationSessionStatus,): "connecting" | "ready" | "running" | "error" | "closed" { switch (status) { case "starting": return "connecting"; case "running": return "running"; case "error": return "error"; case "ready": case "interrupted": return "ready"; case "idle": case "stopped": return "closed"; }} function toLegacyProvider(providerName: string | null): ProviderKind { if (providerName === "codex" || providerName === "claudeAgent") { return providerName; } return "codex";Lines 1–500 of 1161. Load more
import { type OrchestrationEvent, type OrchestrationMessage, type OrchestrationProposedPlan, type ProjectId, type ProviderKind, ThreadId, type OrchestrationReadModel, type OrchestrationSession, type OrchestrationCheckpointSummary, type OrchestrationThread, type OrchestrationSessionStatus, } from "@t3tools/contracts"; import { resolveModelSlugForProvider } from "@t3tools/shared/model"; import { create } from "zustand"; import { findLatestProposedPlan, hasActionableProposedPlan, derivePendingApprovals, derivePendingUserInputs, } from "./session-logic"; import { sanitizeThreadErrorMessage } from "./rpc/transportError"; import { type ChatMessage, type Project, type SidebarThreadSummary, type Thread } from "./types"; // ── State ──────────────────────────────────────────────────────────── export interface AppState { projects: Project[]; threads: Thread[]; sidebarThreadsById: Record<string, SidebarThreadSummary>; threadIdsByProjectId: Record<string, ThreadId[]>; bootstrapComplete: boolean; } const initialState: AppState = { projects: [], threads: [], sidebarThreadsById: {}, threadIdsByProjectId: {}, bootstrapComplete: false, }; const MAX_THREAD_MESSAGES = 2_000; const MAX_THREAD_CHECKPOINTS = 500; const MAX_THREAD_PROPOSED_PLANS = 200; const MAX_THREAD_ACTIVITIES = 500; const EMPTY_THREAD_IDS: ThreadId[] = []; // ── Pure helpers ────────────────────────────────────────────────────── function updateThread( threads: Thread[], threadId: ThreadId, updater: (t: Thread) => Thread, ): Thread[] { let changed = false; const next = threads.map((t) => { if (t.id !== threadId) return t; const updated = updater(t); if (updated !== t) changed = true; return updated; }); return changed ? next : threads; } function updateProject( projects: Project[], projectId: Project["id"], updater: (project: Project) => Project, ): Project[] { let changed = false; const next = projects.map((project) => { if (project.id !== projectId) { return project; } const updated = updater(project); if (updated !== project) { changed = true; } return updated; }); return changed ? next : projects; } function normalizeModelSelection<T extends { provider: "codex" | "claudeAgent"; model: string }>( selection: T, ): T { return { ...selection, model: resolveModelSlugForProvider(selection.provider, selection.model), }; } function mapProjectScripts(scripts: ReadonlyArray<Project["scripts"][number]>): Project["scripts"] { return scripts.map((script) => ({ ...script })); } function mapSession(session: OrchestrationSession): Thread["session"] { return { provider: toLegacyProvider(session.providerName), status: toLegacySessionStatus(session.status), orchestrationStatus: session.status, activeTurnId: session.activeTurnId ?? undefined, createdAt: session.updatedAt, updatedAt: session.updatedAt, ...(session.lastError ? { lastError: session.lastError } : {}), }; } function mapMessage(message: OrchestrationMessage): ChatMessage { const attachments = message.attachments?.map((attachment) => ({ type: "image" as const, id: attachment.id, name: attachment.name, mimeType: attachment.mimeType, sizeBytes: attachment.sizeBytes, previewUrl: toAttachmentPreviewUrl(attachmentPreviewRoutePath(attachment.id)), })); return { id: message.id, role: message.role, text: message.text, turnId: message.turnId, createdAt: message.createdAt, streaming: message.streaming, ...(message.streaming ? {} : { completedAt: message.updatedAt }), ...(attachments && attachments.length > 0 ? { attachments } : {}), }; } function mapProposedPlan(proposedPlan: OrchestrationProposedPlan): Thread["proposedPlans"][number] { return { id: proposedPlan.id, turnId: proposedPlan.turnId, planMarkdown: proposedPlan.planMarkdown, implementedAt: proposedPlan.implementedAt, implementationThreadId: proposedPlan.implementationThreadId, createdAt: proposedPlan.createdAt, updatedAt: proposedPlan.updatedAt, }; } function mapTurnDiffSummary( checkpoint: OrchestrationCheckpointSummary, ): Thread["turnDiffSummaries"][number] { return { turnId: checkpoint.turnId, completedAt: checkpoint.completedAt, status: checkpoint.status, assistantMessageId: checkpoint.assistantMessageId ?? undefined, checkpointTurnCount: checkpoint.checkpointTurnCount, checkpointRef: checkpoint.checkpointRef, files: checkpoint.files.map((file) => ({ ...file })), }; } function mapThread(thread: OrchestrationThread): Thread { return { id: thread.id, codexThreadId: null, projectId: thread.projectId, title: thread.title, modelSelection: normalizeModelSelection(thread.modelSelection), runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, session: thread.session ? mapSession(thread.session) : null, messages: thread.messages.map(mapMessage), proposedPlans: thread.proposedPlans.map(mapProposedPlan), error: sanitizeThreadErrorMessage(thread.session?.lastError), createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, pendingSourceProposedPlan: thread.latestTurn?.sourceProposedPlan, branch: thread.branch, worktreePath: thread.worktreePath, turnDiffSummaries: thread.checkpoints.map(mapTurnDiffSummary), activities: thread.activities.map((activity) => ({ ...activity })), }; } function mapProject(project: OrchestrationReadModel["projects"][number]): Project { return { id: project.id, name: project.title, cwd: project.workspaceRoot, defaultModelSelection: project.defaultModelSelection ? normalizeModelSelection(project.defaultModelSelection) : null, createdAt: project.createdAt, updatedAt: project.updatedAt, scripts: mapProjectScripts(project.scripts), }; } function getLatestUserMessageAt( messages: ReadonlyArray<Thread["messages"][number]>, ): string | null { let latestUserMessageAt: string | null = null; for (const message of messages) { if (message.role !== "user") { continue; } if (latestUserMessageAt === null || message.createdAt > latestUserMessageAt) { latestUserMessageAt = message.createdAt; } } return latestUserMessageAt; } function buildSidebarThreadSummary(thread: Thread): SidebarThreadSummary { return { id: thread.id, projectId: thread.projectId, title: thread.title, interactionMode: thread.interactionMode, session: thread.session, createdAt: thread.createdAt, archivedAt: thread.archivedAt, updatedAt: thread.updatedAt, latestTurn: thread.latestTurn, branch: thread.branch, worktreePath: thread.worktreePath, latestUserMessageAt: getLatestUserMessageAt(thread.messages), hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, hasActionableProposedPlan: hasActionableProposedPlan( findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), ), }; } function sidebarThreadSummariesEqual( left: SidebarThreadSummary | undefined, right: SidebarThreadSummary, ): boolean { return ( left !== undefined && left.id === right.id && left.projectId === right.projectId && left.title === right.title && left.interactionMode === right.interactionMode && left.session === right.session && left.createdAt === right.createdAt && left.archivedAt === right.archivedAt && left.updatedAt === right.updatedAt && left.latestTurn === right.latestTurn && left.branch === right.branch && left.worktreePath === right.worktreePath && left.latestUserMessageAt === right.latestUserMessageAt && left.hasPendingApprovals === right.hasPendingApprovals && left.hasPendingUserInput === right.hasPendingUserInput && left.hasActionableProposedPlan === right.hasActionableProposedPlan ); } function appendThreadIdByProjectId( threadIdsByProjectId: Record<string, ThreadId[]>, projectId: ProjectId, threadId: ThreadId, ): Record<string, ThreadId[]> { const existingThreadIds = threadIdsByProjectId[projectId] ?? EMPTY_THREAD_IDS; if (existingThreadIds.includes(threadId)) { return threadIdsByProjectId; } return { ...threadIdsByProjectId, [projectId]: [...existingThreadIds, threadId], }; } function removeThreadIdByProjectId( threadIdsByProjectId: Record<string, ThreadId[]>, projectId: ProjectId, threadId: ThreadId, ): Record<string, ThreadId[]> { const existingThreadIds = threadIdsByProjectId[projectId] ?? EMPTY_THREAD_IDS; if (!existingThreadIds.includes(threadId)) { return threadIdsByProjectId; } const nextThreadIds = existingThreadIds.filter( (existingThreadId) => existingThreadId !== threadId, ); if (nextThreadIds.length === existingThreadIds.length) { return threadIdsByProjectId; } if (nextThreadIds.length === 0) { const nextThreadIdsByProjectId = { ...threadIdsByProjectId }; delete nextThreadIdsByProjectId[projectId]; return nextThreadIdsByProjectId; } return { ...threadIdsByProjectId, [projectId]: nextThreadIds, }; } function buildThreadIdsByProjectId(threads: ReadonlyArray<Thread>): Record<string, ThreadId[]> { const threadIdsByProjectId: Record<string, ThreadId[]> = {}; for (const thread of threads) { const existingThreadIds = threadIdsByProjectId[thread.projectId] ?? EMPTY_THREAD_IDS; threadIdsByProjectId[thread.projectId] = [...existingThreadIds, thread.id]; } return threadIdsByProjectId; } function buildSidebarThreadsById( threads: ReadonlyArray<Thread>, ): Record<string, SidebarThreadSummary> { return Object.fromEntries( threads.map((thread) => [thread.id, buildSidebarThreadSummary(thread)]), ); } function checkpointStatusToLatestTurnState(status: "ready" | "missing" | "error") { if (status === "error") { return "error" as const; } if (status === "missing") { return "interrupted" as const; } return "completed" as const; } function compareActivities( left: Thread["activities"][number], right: Thread["activities"][number], ): number { if (left.sequence !== undefined && right.sequence !== undefined) { if (left.sequence !== right.sequence) { return left.sequence - right.sequence; } } else if (left.sequence !== undefined) { return 1; } else if (right.sequence !== undefined) { return -1; } return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id); } function buildLatestTurn(params: { previous: Thread["latestTurn"]; turnId: NonNullable<Thread["latestTurn"]>["turnId"]; state: NonNullable<Thread["latestTurn"]>["state"]; requestedAt: string; startedAt: string | null; completedAt: string | null; assistantMessageId: NonNullable<Thread["latestTurn"]>["assistantMessageId"]; sourceProposedPlan?: Thread["pendingSourceProposedPlan"]; }): NonNullable<Thread["latestTurn"]> { const resolvedPlan = params.previous?.turnId === params.turnId ? params.previous.sourceProposedPlan : params.sourceProposedPlan; return { turnId: params.turnId, state: params.state, requestedAt: params.requestedAt, startedAt: params.startedAt, completedAt: params.completedAt, assistantMessageId: params.assistantMessageId, ...(resolvedPlan ? { sourceProposedPlan: resolvedPlan } : {}), }; } function rebindTurnDiffSummariesForAssistantMessage( turnDiffSummaries: ReadonlyArray<Thread["turnDiffSummaries"][number]>, turnId: Thread["turnDiffSummaries"][number]["turnId"], assistantMessageId: NonNullable<Thread["latestTurn"]>["assistantMessageId"], ): Thread["turnDiffSummaries"] { let changed = false; const nextSummaries = turnDiffSummaries.map((summary) => { if (summary.turnId !== turnId || summary.assistantMessageId === assistantMessageId) { return summary; } changed = true; return { ...summary, assistantMessageId: assistantMessageId ?? undefined, }; }); return changed ? nextSummaries : [...turnDiffSummaries]; } function retainThreadMessagesAfterRevert( messages: ReadonlyArray<ChatMessage>, retainedTurnIds: ReadonlySet<string>, turnCount: number, ): ChatMessage[] { const retainedMessageIds = new Set<string>(); for (const message of messages) { if (message.role === "system") { retainedMessageIds.add(message.id); continue; } if ( message.turnId !== undefined && message.turnId !== null && retainedTurnIds.has(message.turnId) ) { retainedMessageIds.add(message.id); } } const retainedUserCount = messages.filter( (message) => message.role === "user" && retainedMessageIds.has(message.id), ).length; const missingUserCount = Math.max(0, turnCount - retainedUserCount); if (missingUserCount > 0) { const fallbackUserMessages = messages .filter( (message) => message.role === "user" && !retainedMessageIds.has(message.id) && (message.turnId === undefined || message.turnId === null || retainedTurnIds.has(message.turnId)), ) .toSorted( (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), ) .slice(0, missingUserCount); for (const message of fallbackUserMessages) { retainedMessageIds.add(message.id); } } const retainedAssistantCount = messages.filter( (message) => message.role === "assistant" && retainedMessageIds.has(message.id), ).length; const missingAssistantCount = Math.max(0, turnCount - retainedAssistantCount); if (missingAssistantCount > 0) { const fallbackAssistantMessages = messages .filter( (message) => message.role === "assistant" && !retainedMessageIds.has(message.id) && (message.turnId === undefined || message.turnId === null || retainedTurnIds.has(message.turnId)), ) .toSorted( (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), ) .slice(0, missingAssistantCount); for (const message of fallbackAssistantMessages) { retainedMessageIds.add(message.id); } } return messages.filter((message) => retainedMessageIds.has(message.id)); } function retainThreadActivitiesAfterRevert( activities: ReadonlyArray<Thread["activities"][number]>, retainedTurnIds: ReadonlySet<string>, ): Thread["activities"] { return activities.filter( (activity) => activity.turnId === null || retainedTurnIds.has(activity.turnId), ); } function retainThreadProposedPlansAfterRevert( proposedPlans: ReadonlyArray<Thread["proposedPlans"][number]>, retainedTurnIds: ReadonlySet<string>, ): Thread["proposedPlans"] { return proposedPlans.filter( (proposedPlan) => proposedPlan.turnId === null || retainedTurnIds.has(proposedPlan.turnId), ); } function toLegacySessionStatus( status: OrchestrationSessionStatus, ): "connecting" | "ready" | "running" | "error" | "closed" { switch (status) { case "starting": return "connecting"; case "running": return "running"; case "error": return "error"; case "ready": case "interrupted": return "ready"; case "idle": case "stopped": return "closed"; } } function toLegacyProvider(providerName: string | null): ProviderKind { if (providerName === "codex" || providerName === "claudeAgent") { return providerName; } return "codex";