import type { OrchestrationV2ProjectedTurnItem, OrchestrationV2ThreadProjection, OrchestrationV2TurnItem, } from "@t3tools/contracts"; import { isOrchestrationV2TurnItemVisible } from "@t3tools/shared/orchestrationV2Timeline"; export type ThreadHistoryMeta = { readonly historyCursor: string | null; readonly hasMoreHistory: boolean; readonly loading: boolean; readonly error: string | null; /** True after at least one older page was merged; skip monolithic cache growth. */ readonly expanded: boolean; /** * Max local turn ordinal known for partial progressive windows. Null when * unknown (legacy cache) or when history meta is cleared for a full snapshot. */ readonly latestLocalTurnOrdinal: number | null; }; export const EMPTY_THREAD_HISTORY_META: ThreadHistoryMeta = { historyCursor: null, hasMoreHistory: false, loading: false, error: null, expanded: false, latestLocalTurnOrdinal: null, }; /** Whether a history-page response still matches the cursor that started it. */ export function isActiveHistoryRequestCursor( requestCursor: string, history: { readonly historyCursor: string | null }, ): boolean { return history.historyCursor === requestCursor; } /** * Clear history loading after an interrupted load-earlier only when the * request cursor is still active. Does not touch stream/status/error. */ export function clearActiveHistoryLoading( requestCursor: string, history: ThreadHistoryMeta, ): ThreadHistoryMeta { if (!isActiveHistoryRequestCursor(requestCursor, history) || !history.loading) { return history; } return { ...history, loading: false }; } function projectedItemKey(row: OrchestrationV2ProjectedTurnItem): string { return `${row.sourceThreadId}:${row.sourceItemId}`; } function renumberVisible( rows: ReadonlyArray, ): OrchestrationV2ProjectedTurnItem[] { return rows.map((row, position) => (row.position === position ? row : { ...row, position })); } /** * Merge an older history page into the live projection. Dedupes by * sourceThreadId + sourceItemId, keeps chronological order (older first), and * preserves newer live rows already present in state. */ export function mergeOlderHistoryIntoProjection( projection: OrchestrationV2ThreadProjection, olderItems: ReadonlyArray, ): OrchestrationV2ThreadProjection { if (olderItems.length === 0) { return projection; } const existingKeys = new Set(projection.visibleTurnItems.map(projectedItemKey)); const turnItemById = new Map(); for (const item of projection.turnItems) { turnItemById.set(String(item.id), item); } const prepended: OrchestrationV2ProjectedTurnItem[] = []; for (const pageRow of olderItems) { let row = pageRow; const key = projectedItemKey(row); if (existingKeys.has(key)) { continue; } if (row.visibility === "local" || row.sourceThreadId === projection.thread.id) { const currentItem = turnItemById.get(String(row.sourceItemId)); if (currentItem !== undefined) { // A live event may have hidden this item while the page was in flight. // Do not resurrect the stale page copy. Retained interrupt requests are // the exception: they intentionally exist outside the bounded visible // window and should become visible when their history page arrives. if (currentItem.type !== "run_interrupt_request") { continue; } if ( !isOrchestrationV2TurnItemVisible({ item: currentItem, runs: projection.runs, attempts: projection.attempts, items: projection.turnItems, }) ) { continue; } row = { ...row, item: currentItem }; } } existingKeys.add(key); prepended.push(row); } if (prepended.length === 0) { return projection; } const visibleTurnItems = renumberVisible([...prepended, ...projection.visibleTurnItems]); for (const row of prepended) { if (row.visibility === "local" || row.sourceThreadId === projection.thread.id) { if (!turnItemById.has(String(row.sourceItemId))) { turnItemById.set(String(row.sourceItemId), row.item); } } } return { ...projection, turnItems: [...turnItemById.values()], visibleTurnItems, }; } export function applyHistoryPageMeta( current: ThreadHistoryMeta, page: { readonly nextCursor: string | null; readonly hasMoreHistory: boolean; }, ): ThreadHistoryMeta { return { historyCursor: page.nextCursor, hasMoreHistory: page.hasMoreHistory, loading: false, error: null, expanded: true, // Preserve the partial-timeline watermark across load-earlier pages. latestLocalTurnOrdinal: current.latestLocalTurnOrdinal, }; } /** Whether a client timeline should render its load-earlier control. */ export function shouldShowLoadEarlierControl(history: ThreadHistoryMeta): boolean { return history.hasMoreHistory || history.error !== null; }