[Bug] Calls.TrackCall never delivers events for API-originated calls (ref/channel.id key mismatch)
Summary
Calls.TrackCall never delivers any event to clients for API-originated calls, because the publisher and the subscriber key on two different identifiers, so every event is dropped silently.
Steps to Reproduce
- Originate a call via the SDK/API (
Calls.createCall), which internally callsTrackCalland starts iterating the returnedstatusStream. - Let Asterisk/ARI dispatch
Dialevents for the call throughVoiceDispatcher.handleDial. - Observe the apiserver logs and the client's
statusStream.
Expected Behavior
The statusStream returned by Calls.createCall() (mods/sdk/src/Calls.ts:127-203, stream construction at 151-154 / 181-203) should receive TRYING/RINGING/ANSWER/BUSY/NOANSWER/FAILED events as they occur, letting the caller observe call progress instead of polling GetCall.
Actual Behavior
No event is ever delivered. Root cause: the publisher and subscriber key on two different identifiers.
mods/apiserver/src/calls/createTrackCall.ts:71-77—trackCall()registers the caller's stream intrackingCallsMapkeyed by the call ref returned byCreateCall:const trackCall = (call: { request: BaseApiObject }) => { const stream = call as unknown as CallStream; const { ref } = call.request; logger.verbose("call to trackCall", { ref }); trackingCallsMap.set(ref, stream); };mods/apiserver/src/voice/VoiceDispatcher.ts:145-146—handleDial()callscreateHandleDialEventsWithNats(this.nc)(channel.id, event), publishing to thecalls.trackNATS subject keyed bychannel.id:async handleDial(event: Dial, channel: Channel) { createHandleDialEventsWithNats(this.nc)(channel.id, event); }createTrackCall.ts:53-58then doestrackingCallsMap.get(ref)and returns early on the miss:const stream = trackingCallsMap.get(ref); if (!stream) { // There is not request to track this call return; }
Every Dial event published under channel.id is dropped because it never matches a key registered under the call ref.
Production evidence
From a production incident (2026-08-30, 16 API-originated calls, all VOICE_PRERECORDED):
18:08:51.098 call to trackCall {"ref":"c21ff1ab-5b46-4d99-8879-fad1e1d02d0a"}
18:08:56.253 added channel to bridge {"mediaSessionRef":"1788113331.622",
"channelId":"df561aee-dcc1-44f4-a3c0-6caa09f2be0a"}
18:08:56.259 call to subscription.callback {"ref":"df561aee-…","status":"TRYING"}
18:08:56.259 call to subscription.callback {"ref":"df561aee-…","status":"ANSWER"}Across all 16 calls: 16 call to trackCall registrations, 7 published events, zero matches. c21ff1ab-… is the call ref (it also appears as the X-Call-Ref SIP header in a packet capture of the same call); df561aee-… is a channel id.
Impact
Callers cannot observe BUSY/NOANSWER/FAILED at all via statusStream and must fall back to polling GetCall for the CDR. The stream hangs until the call is torn down.
Suggested direction (a suggestion, not a prescription)
The dialplan already sets the call ref as a channel variable — asterisk/config/extensions.conf:10 does Set(CALL_REF=${PJSIP_HEADER(read,X-Call-Ref)}) — the enum member exists at mods/apiserver/src/voice/types/ari.ts:40 (ChannelVar.CALL_REF), and createGetChannelVarWithoutThrow is already used nearby in VoiceDispatcher.isHandledElsewhere. So handleDial could resolve the real call ref rather than using channel.id.
Two open caveats
- Which leg these Dial events belong to is unconfirmed. In the logs above,
df561aee-…is the id logged at "added channel to bridge," i.e. the external-media channel — not the PSTN leg. If the ARI Dial events are firing on the external-media leg, re-keying alone would not make them meaningful, and the right fix may be a different event source entirely (or removing this path). Flagging this as an open question for maintainers to confirm, not a settled fact. - SIP-originated calls carry no
X-Call-Ref, soCALL_REFwould be empty for them. Whatever fix is chosen needs to decide between skipping the publish for that case or minting a synthetic ref the waymods/apiserver/src/events/transformEvent.ts:73-80already does for SIP-originated calls without a ref.
Priority
P1 – High
Source: fonoster/fonoster