Voice: AVSpeechSynth setPendingContinuation overwrites without resuming; latent hang outside actor #38

Open
opened 2026-05-12 19:07:23 +00:00 by KofTwentyTwo · 0 comments
KofTwentyTwo commented 2026-05-12 19:07:23 +00:00 (Migrated from github.com)

Summary

SpeechSynthDelegate.setPendingContinuation overwrites without resuming. If two concurrent speak calls arrive, the second setPendingContinuation replaces the stored continuation without resuming the first. Lost continuation never resumes; first withCheckedContinuation block hangs forever.

Evidence

  • File: packages/Voice/Sources/Voice/TTS/AVSpeechSynth.swift:89-93

Impact

Today this can't fire because TTSEngineActor's serial executor serializes access. But the type itself is @unchecked Sendable and exposes the race. If anyone uses AVSpeechSynth outside the actor, the bug ships.

Recommended fix

On setPendingContinuation, if an existing continuation is present, resume it before replacing:

func setPendingContinuation(_ cont: CheckedContinuation<Void, Never>) {
    pendingContinuation?.resume()  // resume any orphan
    pendingContinuation = cont
}

Matches the didCancel semantics from stopSpeaking(at: .immediate).

Test

  • Unit: call setPendingContinuation twice → assert first continuation resumed

Source

  • Audit: .planning/audit-2026-05-12/voice-audit.md (2026-05-12) — finding P2-2
## Summary `SpeechSynthDelegate.setPendingContinuation` overwrites without resuming. If two concurrent `speak` calls arrive, the second `setPendingContinuation` replaces the stored continuation without resuming the first. Lost continuation never resumes; first `withCheckedContinuation` block hangs forever. ## Evidence - File: `packages/Voice/Sources/Voice/TTS/AVSpeechSynth.swift:89-93` ## Impact Today this can't fire because `TTSEngineActor`'s serial executor serializes access. But the type itself is `@unchecked Sendable` and exposes the race. If anyone uses `AVSpeechSynth` outside the actor, the bug ships. ## Recommended fix On `setPendingContinuation`, if an existing continuation is present, resume it before replacing: ```swift func setPendingContinuation(_ cont: CheckedContinuation<Void, Never>) { pendingContinuation?.resume() // resume any orphan pendingContinuation = cont } ``` Matches the `didCancel` semantics from `stopSpeaking(at: .immediate)`. ## Test - Unit: call `setPendingContinuation` twice → assert first continuation resumed ## Source - Audit: `.planning/audit-2026-05-12/voice-audit.md` (2026-05-12) — finding P2-2
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/Jarvis#38