@@ -127,23 +127,27 @@ export async function POST(req: Request) {
127127 typeof body ?. display_name === 'string' && body . display_name . trim ( )
128128 ? body . display_name . trim ( )
129129 : studentDefaultDisplayName ( student ) ;
130- const roomName = typeof body ?. room_name === 'string' ? body . room_name . trim ( ) : '' ;
131- if ( ! displayName || ! roomName ) {
130+ const requestedRoomName = typeof body ?. room_name === 'string' ? body . room_name . trim ( ) : '' ;
131+ if ( ! displayName || ! requestedRoomName ) {
132132 return NextResponse . json (
133133 { error : 'display_name and room_name are required.' } ,
134134 { status : 400 , headers : { 'Cache-Control' : 'no-store' } }
135135 ) ;
136136 }
137137 const participantIdentity = createStudentParticipantIdentity ( student ) ;
138138 const config = await readRuntimeConfig ( ) ;
139- const agentMode = inferAgentMode ( body ?. agent_mode , roomName , config . agentMode ) ;
139+ const agentMode = inferAgentMode ( body ?. agent_mode , requestedRoomName , config . agentMode ) ;
140140 const sessionActivity =
141141 agentMode === 'realtime'
142- ? await readSessionActivityContext ( body , roomName , config . sessionPurpose )
142+ ? await readSessionActivityContext ( body , requestedRoomName , config . sessionPurpose )
143143 : undefined ;
144+ const roomName =
145+ agentMode === 'realtime' && sessionActivity
146+ ? canonicalRealtimeRoomName ( requestedRoomName , displayName , student , sessionActivity )
147+ : requestedRoomName ;
144148 if ( agentMode === 'realtime' && config . realtimeResetting ) {
145149 logTokenEvent ( 'rejected realtime token during reset' , {
146- roomName,
150+ roomName : requestedRoomName ,
147151 studentNumber : student . studentNumber ,
148152 requestedAgentMode : body ?. agent_mode ?? null ,
149153 requestedActivityType : body ?. activity_type ?? null ,
@@ -178,6 +182,7 @@ export async function POST(req: Request) {
178182
179183 logTokenEvent ( 'issuing participant token' , {
180184 roomName,
185+ requestedRoomName,
181186 displayName,
182187 studentId : student . id ,
183188 studentNumber : student . studentNumber ,
@@ -279,6 +284,53 @@ function safeIdentityPart(value: string) {
279284 ) ;
280285}
281286
287+ function safeRoomNamePart ( value : string ) {
288+ return (
289+ value
290+ . trim ( )
291+ . toLowerCase ( )
292+ . replace ( / [ ^ a - z 0 - 9 ] + / g, '_' )
293+ . replace ( / ^ _ + | _ + $ / g, '' )
294+ . slice ( 0 , 40 ) || 'student'
295+ ) ;
296+ }
297+
298+ function isActivityRoomName ( roomName : string ) {
299+ return (
300+ roomName . startsWith ( 'eval-' ) ||
301+ roomName . startsWith ( 'eval_' ) ||
302+ roomName . startsWith ( 'task-' ) ||
303+ roomName . startsWith ( 'task_' )
304+ ) ;
305+ }
306+
307+ function roomSuffixFromRequested ( roomName : string ) {
308+ const match = roomName . match ( / [ _ - ] ( [ a - z A - Z 0 - 9 ] { 8 } ) $ / ) ;
309+ return match ?. [ 1 ] ?? String ( Date . now ( ) ) . slice ( - 8 ) ;
310+ }
311+
312+ function canonicalRealtimeRoomName (
313+ requestedRoomName : string ,
314+ displayName : string ,
315+ student : StudentSession ,
316+ sessionActivity : SessionActivityContext
317+ ) {
318+ if ( ! isActivityRoomName ( requestedRoomName ) ) return requestedRoomName ;
319+
320+ const prefix =
321+ sessionActivity . sessionPurpose === 'evaluation' ||
322+ sessionActivity . activityType === 'free_conversation'
323+ ? 'eval'
324+ : 'task' ;
325+ return [
326+ prefix ,
327+ student . classNumber ,
328+ student . rollNumber ,
329+ safeRoomNamePart ( displayName ) ,
330+ roomSuffixFromRequested ( requestedRoomName ) ,
331+ ] . join ( '_' ) ;
332+ }
333+
282334function createStudentParticipantIdentity ( student : StudentSession ) {
283335 return `student-${ safeIdentityPart ( student . studentNumber ) } -${ Math . floor ( Math . random ( ) * 10_000 ) } ` ;
284336}
@@ -295,7 +347,14 @@ async function readRuntimeConfig(): Promise<RuntimeConfig> {
295347}
296348
297349function inferAgentMode ( value : unknown , roomName : string , fallback : AgentMode ) : AgentMode {
298- if ( roomName . startsWith ( 'eval-' ) || roomName . startsWith ( 'task-' ) ) return 'realtime' ;
350+ if (
351+ roomName . startsWith ( 'eval-' ) ||
352+ roomName . startsWith ( 'eval_' ) ||
353+ roomName . startsWith ( 'task-' ) ||
354+ roomName . startsWith ( 'task_' )
355+ ) {
356+ return 'realtime' ;
357+ }
299358 if ( roomName . startsWith ( 'realtime-' ) ) return 'realtime' ;
300359 return normalizeAgentMode ( value ?? fallback ) ;
301360}
@@ -310,8 +369,8 @@ type SessionPurposeSignal = {
310369} ;
311370
312371function parseRoomSessionPurpose ( roomName : string ) : SessionPurpose | undefined {
313- if ( roomName . startsWith ( 'eval-' ) ) return 'evaluation' ;
314- if ( roomName . startsWith ( 'task-' ) ) return 'practice' ;
372+ if ( roomName . startsWith ( 'eval-' ) || roomName . startsWith ( 'eval_' ) ) return 'evaluation' ;
373+ if ( roomName . startsWith ( 'task-' ) || roomName . startsWith ( 'task_' ) ) return 'practice' ;
315374 return undefined ;
316375}
317376
0 commit comments