Skip to content

voice.ts: concurrent /notify requests cause simultaneous audio playback (no queue) #1361

@packetsherpa

Description

@packetsherpa

Bug

When two /notify requests arrive at Pulse simultaneously — e.g., an Algorithm phase-transition curl fires while a cron job also dispatches a voice notification — both requests are handled concurrently by Bun.serve. Each independently calls sendNotificationgenerateSpeechplayAudio (afplay), resulting in two voices speaking over each other.

Root Cause

handleVoiceRequest in Pulse/VoiceServer/voice.ts is async and Bun.serve processes incoming requests in parallel. There is no queue or mutex around audio playback, so concurrent /notify requests each independently invoke afplay.

Affected lines (v5.0.0)

Lines 627, 665, 683 — three bare await sendNotification(...) calls with nothing serializing them.

Fix

Add a module-level promise chain that serializes all voice playback:

// After the Rate Limiting section, before sendNotification:
let voiceQueue: Promise<void> = Promise.resolve()

function enqueueVoice<T>(fn: () => Promise<T>): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    voiceQueue = voiceQueue.then(async () => {
      try { resolve(await fn()) } catch (err) { reject(err) }
    })
  })
}

Then wrap the three sendNotification calls in handleVoiceRequest:

// Line 627
const result = await enqueueVoice(() => sendNotification(title, message, voiceEnabled, voiceId, voiceSettings, volume))

// Line 665
await enqueueVoice(() => sendNotification("PAI Notification", message, true, voiceId))

// Line 683
await enqueueVoice(() => sendNotification(title, message, true, null))

This ensures concurrent requests queue behind each other and audio plays sequentially. The HTTP response is still held until the queued notification completes (preserving the existing sequential-curl behavior for Algorithm phase transitions).

Reproduction

Trigger two /notify requests within a short window — e.g., an Algorithm phase transition during a morning cron run. Both voices will speak simultaneously.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions