@@ -133,7 +133,7 @@ final class WebRTCConnectionManager: WebRTCConnectionManaging {
133133 return StartupResult ( agentId: auth. agentId, metrics: metrics)
134134 }
135135
136- /// Race the delegate's "agent audio track subscribed " signal against `timeout`.
136+ /// Race the delegate's "first remote participant joined " signal against `timeout`.
137137 /// A `.timedOut` result makes `connect` fail with `StartupFailure.agentTimeout`.
138138 private func waitForAgentReady( timeout: TimeInterval ) async -> AgentReadyWaitResult {
139139 guard let delegate = readinessDelegate else {
@@ -143,7 +143,7 @@ final class WebRTCConnectionManager: WebRTCConnectionManaging {
143143 return await withTaskGroup ( of: AgentReadyWaitResult . self) { group in
144144 group. addTask {
145145 do {
146- try await delegate. awaitSubscription ( )
146+ try await delegate. awaitRemoteParticipant ( )
147147 return . success( elapsed: Date ( ) . timeIntervalSince ( start) )
148148 } catch {
149149 return . timedOut( elapsed: Date ( ) . timeIntervalSince ( start) )
@@ -321,6 +321,11 @@ final class WebRTCConnectionManager: WebRTCConnectionManaging {
321321 }
322322}
323323
324+ /// The agent joins with an identity prefixed `agent`; other participants don't.
325+ private func isAgentParticipant( _ participant: Participant ) -> Bool {
326+ ( participant. identity. map { String ( describing: $0) } ?? " " ) . hasPrefix ( " agent " )
327+ }
328+
324329// MARK: – Room event delegate
325330
326331/// `RoomDelegate` that forwards data, remote speaking, and remote-disconnect events
@@ -353,8 +358,7 @@ final class LiveKitRoomEventDelegate: RoomDelegate {
353358 }
354359
355360 nonisolated func room( _ room: Room , participantDidDisconnect participant: RemoteParticipant ) {
356- let identityString = participant. identity. map { String ( describing: $0) } ?? " "
357- guard identityString. hasPrefix ( " agent " ) || room. remoteParticipants. isEmpty else { return }
361+ guard isAgentParticipant ( participant) || room. remoteParticipants. isEmpty else { return }
358362 Task { [ onRemoteDisconnect] in
359363 await onRemoteDisconnect ( )
360364 }
@@ -370,101 +374,70 @@ final class LiveKitRoomEventDelegate: RoomDelegate {
370374
371375// MARK: – Readiness delegate
372376
373- /// Observes LiveKit and signals exactly one event: the agent's audio track is
374- /// subscribed ( the real "safe to send" signal). Holds no timing policy; callers
375- /// race `awaitSubscription ()` against their own timeout.
377+ /// Observes LiveKit and signals exactly one event: the first remote participant
378+ /// has joined the room (the "safe to send" signal). Holds no timing policy; callers
379+ /// race `awaitRemoteParticipant ()` against their own timeout.
376380@MainActor
377- private final class LiveKitReadinessDelegate : RoomDelegate {
378- private enum Stage { case waiting, ready, released }
379-
381+ final class LiveKitReadinessDelegate : RoomDelegate {
380382 private let logger : any Logging
381- private var stage : Stage = . waiting
382- private var awaiter : CheckedContinuation < Void , Error > ?
383+ private var continuation : CheckedContinuation < Void , Error > ?
384+ private var outcome : Result < Void , Error > ?
383385
384386 init ( logger: any Logging ) {
385387 self . logger = logger
386388 }
387389
388- /// Suspend until the agent's audio track is subscribed . Throws `CancellationError`
390+ /// Suspend until the first remote participant joins . Throws `CancellationError`
389391 /// if the awaiting task is cancelled or `release()` is called (e.g. on disconnect).
390392 /// Single-caller — there's exactly one `waitForAgentReady` per connection lifetime.
391- func awaitSubscription( ) async throws {
392- switch stage {
393- case . ready: return
394- case . released: throw CancellationError ( )
395- case . waiting: break
396- }
393+ func awaitRemoteParticipant( ) async throws {
394+ if let outcome { return try outcome. get ( ) }
397395 try await withTaskCancellationHandler {
398396 try await withCheckedThrowingContinuation { ( cont: CheckedContinuation < Void , Error > ) in
399- if Task . isCancelled {
397+ if let outcome {
398+ cont. resume ( with: outcome)
399+ } else if Task . isCancelled {
400400 cont. resume ( throwing: CancellationError ( ) )
401- return
402- }
403- switch stage {
404- case . ready: cont. resume ( )
405- case . released: cont. resume ( throwing: CancellationError ( ) )
406- case . waiting: awaiter = cont
401+ } else {
402+ continuation = cont
407403 }
408404 }
409405 } onCancel: {
410- Task { @MainActor [ weak self] in
411- if let cont = self ? . awaiter {
412- self ? . awaiter = nil
413- cont. resume ( throwing: CancellationError ( ) )
414- }
415- }
406+ Task { @MainActor [ weak self] in self ? . finish ( . failure( CancellationError ( ) ) ) }
416407 }
417408 }
418409
419410 /// Resolve the in-flight awaiter (if any) with `CancellationError`. Called by
420411 /// the manager on disconnect (or before swapping in a fresh delegate on reconnect).
421412 func release( ) {
422- guard stage != . released else { return }
423- stage = . released
424- if let cont = awaiter {
425- awaiter = nil
426- cont. resume ( throwing: CancellationError ( ) )
427- }
413+ finish ( . failure( CancellationError ( ) ) )
428414 }
429415
430416 // MARK: - RoomDelegate
431417
432418 nonisolated func roomDidConnect( _ room: Room ) {
433- Task { @MainActor in self . checkForSubscribedAudio ( in: room) }
434- }
435-
436- nonisolated func room( _ room: Room , participantDidConnect _: RemoteParticipant ) {
437- Task { @MainActor in self . checkForSubscribedAudio ( in: room) }
419+ Task { @MainActor in
420+ if room. remoteParticipants. values. contains ( where: isAgentParticipant) { self . markReady ( ) }
421+ }
438422 }
439423
440- nonisolated func room(
441- _: Room ,
442- participant _: RemoteParticipant ,
443- didSubscribeTrack publication: RemoteTrackPublication
444- ) {
424+ nonisolated func room( _: Room , participantDidConnect participant: RemoteParticipant ) {
445425 Task { @MainActor in
446- guard publication. kind == . audio else { return }
447- self . markReady ( )
426+ if isAgentParticipant ( participant) { self . markReady ( ) }
448427 }
449428 }
450429
451430 // MARK: - Private
452431
453- private func checkForSubscribedAudio( in room: Room ) {
454- guard stage == . waiting else { return }
455- let hasSubscribed = room. remoteParticipants. values. contains { participant in
456- participant. audioTracks. contains { $0. isSubscribed && $0. track != nil }
457- }
458- if hasSubscribed { markReady ( ) }
432+ private func markReady( ) {
433+ logger. debug ( " Agent joined " )
434+ finish ( . success( ( ) ) )
459435 }
460436
461- private func markReady( ) {
462- guard stage == . waiting else { return }
463- stage = . ready
464- logger. debug ( " Agent audio track subscribed " )
465- if let cont = awaiter {
466- awaiter = nil
467- cont. resume ( )
468- }
437+ private func finish( _ result: Result < Void , Error > ) {
438+ guard outcome == nil else { return }
439+ outcome = result
440+ continuation? . resume ( with: result)
441+ continuation = nil
469442 }
470443}
0 commit comments