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 sendNotification → generateSpeech → playAudio (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.
Bug
When two
/notifyrequests 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 callssendNotification→generateSpeech→playAudio(afplay), resulting in two voices speaking over each other.Root Cause
handleVoiceRequestinPulse/VoiceServer/voice.tsisasyncand Bun.serve processes incoming requests in parallel. There is no queue or mutex around audio playback, so concurrent/notifyrequests each independently invokeafplay.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:
Then wrap the three
sendNotificationcalls inhandleVoiceRequest: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
/notifyrequests within a short window — e.g., an Algorithm phase transition during a morning cron run. Both voices will speak simultaneously.