| 1060 | * within a 30-minute window. |
| 1061 | */ |
| 1062 | export function detectMultiClauding( |
| 1063 | sessions: Array<{ |
| 1064 | session_id: string |
| 1065 | user_message_timestamps: string[] |
| 1066 | }>, |
| 1067 | ): { |
| 1068 | overlap_events: number |
| 1069 | sessions_involved: number |
| 1070 | user_messages_during: number |
| 1071 | } { |
| 1072 | const OVERLAP_WINDOW_MS = 30 * 60000 |
| 1073 | const allSessionMessages: Array<{ ts: number; sessionId: string }> = [] |
| 1074 | |
| 1075 | for (const session of sessions) { |
| 1076 | for (const timestamp of session.user_message_timestamps) { |
| 1077 | try { |
| 1078 | const ts = new Date(timestamp).getTime() |
| 1079 | allSessionMessages.push({ ts, sessionId: session.session_id }) |
| 1080 | } catch { |
| 1081 | // Skip invalid timestamps |
| 1082 | } |
| 1083 | } |
| 1084 | } |
| 1085 | |
| 1086 | allSessionMessages.sort((a, b) => a.ts - b.ts) |
| 1087 | |
| 1088 | const multiClaudeSessionPairs = new Set<string>() |
| 1089 | const messagesDuringMulticlaude = new Set<string>() |
| 1090 | |
| 1091 | // Sliding window: sessionLastIndex tracks the most recent index for each session |
| 1092 | let windowStart = 0 |
| 1093 | const sessionLastIndex = new Map<string, number>() |
| 1094 | |
| 1095 | for (let i = 0; i < allSessionMessages.length; i++) { |
| 1096 | const msg = allSessionMessages[i]! |
| 1097 | |
| 1098 | // Shrink window from the left |
| 1099 | while ( |
| 1100 | windowStart < i && |
| 1101 | msg.ts - allSessionMessages[windowStart]!.ts > OVERLAP_WINDOW_MS |
| 1102 | ) { |
| 1103 | const expiring = allSessionMessages[windowStart]! |
| 1104 | if (sessionLastIndex.get(expiring.sessionId) === windowStart) { |
| 1105 | sessionLastIndex.delete(expiring.sessionId) |
| 1106 | } |
| 1107 | windowStart++ |
| 1108 | } |
| 1109 | |
| 1110 | // Check if this session appeared earlier in the window (pattern: s1 -> s2 -> s1) |
| 1111 | const prevIndex = sessionLastIndex.get(msg.sessionId) |
| 1112 | if (prevIndex !== undefined) { |
| 1113 | for (let j = prevIndex + 1; j < i; j++) { |
| 1114 | const between = allSessionMessages[j]! |
| 1115 | if (between.sessionId !== msg.sessionId) { |
| 1116 | const pair = [msg.sessionId, between.sessionId].sort().join(':') |
| 1117 | multiClaudeSessionPairs.add(pair) |
| 1118 | messagesDuringMulticlaude.add( |
| 1119 | `${allSessionMessages[prevIndex]!.ts}:${msg.sessionId}`, |