Three issues caused the port to remain bound after partial failures:
1. VoiceCallWebhookServer.start() had no idempotency guard — calling it
while the server was already listening would create a second server on
the same port.
2. createVoiceCallRuntime() did not clean up the webhook server if a step
after webhookServer.start() failed (e.g. manager.initialize). The
server kept the port bound while the runtime promise rejected.
3. ensureRuntime() cached the rejected promise forever, so subsequent
calls would re-throw the same error without ever retrying. Combined
with (2), the port stayed orphaned until gateway restart.
Fixes#32387
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: log a warning when endCall fails on stream
disconnect instead of silently discarding the error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Twilio media stream disconnects (e.g., caller hangs up or
network drops), the call object was left in an active state indefinitely.
This caused "stuck calls" that consumed resources and blocked new calls.
Now calls are automatically ended when their media stream closes,
matching the expected lifecycle behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: log a warning when the stale call reaper
fails to end a call instead of silently discarding the error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a periodic reaper that automatically ends calls older than a
configurable threshold. This catches calls stuck in unexpected states,
such as notify-mode calls that never receive a terminal webhook from
the provider.
New config option:
staleCallReaperSeconds: number (default: 0 = disabled)
When enabled, checks every 30 seconds and ends calls exceeding the
max age. Recommended value: 120-300 for production deployments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address review feedback: the in-memory deletion of initialMessage is
not persisted to disk, which is acceptable because a gateway restart
would also sever the media stream, making replay impossible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-generates TTS audio for the configured inboundGreeting at startup
and serves it instantly when an inbound call connects, eliminating the
500ms+ TTS synthesis delay on the first ring.
Changes:
- twilio.ts: Add cachedGreetingAudio storage with getter/setter
- runtime.ts: Pre-synthesize greeting TTS after provider initialization
- webhook.ts: Play cached audio directly via media stream on inbound
connect, falling back to the original TTS path for outbound calls
or when no cached audio is available
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(bluebubbles): auto-strip markdown from outbound messages (#7402)
* fix(security): add timeout to webhook body reading (#6762)
Adds 30-second timeout to readBody() in voice-call, bluebubbles, and nostr
webhook handlers. Prevents Slow-Loris DoS (CWE-400, CVSS 7.5).
Merged with existing maxBytes protection in voice-call.
* fix(security): unify Error objects and lint fixes in webhook timeouts (#6762)
* fix: prevent plugins from auto-enabling without user consent (#3961)
Changes default plugin enabled state from true to false in enablePluginEntry().
Preserves existing enabled:true values. Fixes#3932.
* fix: apply hierarchical mediaMaxMb config to all channels (#8749)
Generalizes resolveAttachmentMaxBytes() to use account → channel → global
config resolution for all channels, not just BlueBubbles. Fixes#7847.
* fix(bluebubbles): sanitize attachment filenames against header injection (#10333)
Strip ", \r, \n, and \\ from filenames after path.basename() to prevent
multipart Content-Disposition header injection (CWE-93, CVSS 5.4).
Also adds sanitization to setGroupIconBlueBubbles which had zero filename
sanitization.
* fix(lint): exclude extensions/ from Oxlint preflight check (#9313)
Extensions use PluginRuntime|null patterns that trigger
no-redundant-type-constituents because PluginRuntime resolves to any.
Excluding extensions/ from Oxlint unblocks user upgrades.
Re-applies the approach from closed PR #10087.
* fix(bluebubbles): add tempGuid to createNewChatWithMessage payload (#7745)
Non-Private-API mode (AppleScript) requires tempGuid in send payloads.
The main sendMessageBlueBubbles already had it, but createNewChatWithMessage
was missing it, causing 400 errors for new chat creation without Private API.
* fix: send stop-typing signal when run ends with NO_REPLY (#8785)
Adds onCleanup callback to the typing controller that fires when the
controller is cleaned up while typing was active (e.g., after NO_REPLY).
Channels using createTypingCallbacks automatically get stop-typing on
cleanup. This prevents the typing indicator from lingering in group chats
when the agent decides not to reply.
* fix(telegram): deduplicate skill commands in multi-agent setup (#5717)
Two fixes:
1. Skip duplicate workspace dirs when listing skill commands across agents.
Multiple agents sharing the same workspace would produce duplicate commands
with _2, _3 suffixes.
2. Clear stale commands via deleteMyCommands before registering new ones.
Commands from deleted skills now get cleaned up on restart.
* fix: add size limits to unbounded in-memory caches (#4948)
Adds max-size caps with oldest-entry eviction to prevent OOM in
long-running deployments:
- BlueBubbles serverInfoCache: 64 entries (already has TTL)
- Google Chat authCache: 32 entries
- Matrix directRoomCache: 1024 entries
- Discord presenceCache: 5000 entries per account
* fix: address review concerns (#11093)
- Chain deleteMyCommands → setMyCommands to prevent race condition (#5717)
- Rename enablePluginEntry to registerPluginEntry (now sets enabled: false)
- Add Slow-Loris timeout test for readJsonBody (#6023)
* fix(voice-call): prevent audio overlap with TTS queue
Add a TTS queue to serialize audio playback and prevent overlapping
speech during voice calls. Previously, concurrent speak() calls could
send audio chunks simultaneously, causing garbled/choppy output.
Changes:
- Add queueTts() to MediaStreamHandler for sequential TTS playback
- Wrap playTtsViaStream() audio sending in the queue
- Clear queue on barge-in (when user starts speaking)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(voice-call): use iterative queue processing to prevent heap exhaustion
The recursive processQueue() pattern accumulated stack frames, causing
JavaScript heap out of memory errors on macOS CI. Convert to while loop
for constant stack usage regardless of queue depth.
* fix: prevent voice-call TTS overlap (#1713) (thanks @dguido)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>