export interface SessionArchiveTreeCandidate { id: string; path: string; archived: boolean; parentSessionPath?: string; } export interface SessionArchiveTreePlan { targets: T[]; unarchivedTargets: T[]; skippedAlreadyArchivedCount: number; } export function findArchiveCandidateByIdOrPrefix(candidates: readonly T[], sessionId: string): T | undefined { return candidates.find((candidate) => candidate.id === sessionId) ?? candidates.find((candidate) => candidate.id.startsWith(sessionId)); } export function planSessionArchiveTree(root: T, candidates: readonly T[]): SessionArchiveTreePlan { const targets = sessionArchiveSubtree(root, candidates); const unarchivedTargets = targets.filter((target) => !target.archived); return { targets, unarchivedTargets, skippedAlreadyArchivedCount: targets.length - unarchivedTargets.length, }; } function sessionArchiveSubtree(root: T, candidates: readonly T[]): T[] { const childrenByParentPath = new Map(); for (const candidate of candidates) { if (candidate.parentSessionPath === undefined) continue; const children = childrenByParentPath.get(candidate.parentSessionPath) ?? []; children.push(candidate); childrenByParentPath.set(candidate.parentSessionPath, children); } const result: T[] = []; const visit = (candidate: T, seenPaths: Set) => { if (seenPaths.has(candidate.path)) return; result.push(candidate); const nextSeenPaths = new Set(seenPaths); nextSeenPaths.add(candidate.path); for (const child of childrenByParentPath.get(candidate.path) ?? []) visit(child, nextSeenPaths); }; visit(root, new Set()); return result; }