( header: SessionHeader, sessionId: string = header.id, )
| 995 | |
| 996 | /** Validate and normalize a current SessionHeader before canonical persistence. */ |
| 997 | export function normalizeSessionHeader( |
| 998 | header: SessionHeader, |
| 999 | sessionId: string = header.id, |
| 1000 | ): SessionHeader { |
| 1001 | const valid = |
| 1002 | header.id === sessionId && |
| 1003 | typeof header.workspaceRoot === 'string' && |
| 1004 | typeof header.cwd === 'string' && |
| 1005 | (header.projectId === undefined || |
| 1006 | header.projectId === null || |
| 1007 | (typeof header.projectId === 'string' && header.projectId.length > 0)) && |
| 1008 | isFiniteNumber(header.createdAt) && |
| 1009 | isFiniteNumber(header.lastUsedAt) && |
| 1010 | (header.lastMessageAt === undefined || isFiniteNumber(header.lastMessageAt)) && |
| 1011 | typeof header.name === 'string' && |
| 1012 | typeof header.titleIsManual === 'boolean' && |
| 1013 | typeof header.isFlagged === 'boolean' && |
| 1014 | Array.isArray(header.labels) && |
| 1015 | header.labels.every((label) => typeof label === 'string') && |
| 1016 | typeof header.isArchived === 'boolean' && |
| 1017 | !Object.prototype.hasOwnProperty.call(header, 'archivedAt') && |
| 1018 | isSessionStatus(header.status) && |
| 1019 | (header.blockedReason === undefined || isSessionBlockedReason(header.blockedReason)) && |
| 1020 | (header.statusUpdatedAt === undefined || isFiniteNumber(header.statusUpdatedAt)) && |
| 1021 | (header.parentSessionId === undefined || typeof header.parentSessionId === 'string') && |
| 1022 | (header.branchOfTurnId === undefined || typeof header.branchOfTurnId === 'string') && |
| 1023 | isValidConversationCopyLineage(header) && |
| 1024 | isValidRevisionLineage(header) && |
| 1025 | isValidSubagentSessionLineage(header) && |
| 1026 | (header.lastReadMessageId === undefined || typeof header.lastReadMessageId === 'string') && |
| 1027 | typeof header.hasUnread === 'boolean' && |
| 1028 | isBackendKind(header.backend) && |
| 1029 | typeof header.llmConnectionSlug === 'string' && |
| 1030 | typeof header.connectionLocked === 'boolean' && |
| 1031 | typeof header.model === 'string' && |
| 1032 | (header.toolProfile === undefined || isSessionToolProfile(header.toolProfile)) && |
| 1033 | isPermissionMode(header.permissionMode) && |
| 1034 | isCollaborationMode(header.collaborationMode) && |
| 1035 | isOrchestrationMode(header.orchestrationMode) && |
| 1036 | (header.transcriptLedgerVersion === undefined || |
| 1037 | header.transcriptLedgerVersion === 0 || |
| 1038 | header.transcriptLedgerVersion === 1) && |
| 1039 | header.schemaVersion === 1; |
| 1040 | if (!valid) { |
| 1041 | throw new Error(`Invalid session header for session ${sessionId}: malformed fields`); |
| 1042 | } |
| 1043 | const normalizedName = normalizeSessionName(header.name); |
| 1044 | if (header.blockedReason === undefined) { |
| 1045 | const { blockedReason: _blockedReason, ...withoutBlockedReason } = header; |
| 1046 | return { ...withoutBlockedReason, name: normalizedName }; |
| 1047 | } |
| 1048 | return { ...header, name: normalizedName }; |
| 1049 | } |
| 1050 | |
| 1051 | function isValidRevisionLineage(header: SessionHeader): boolean { |
| 1052 | const values = [ |
no test coverage detected