#883·fonoster

[Bug] Calls.TrackCall never delivers events for API-originated calls (ref/channel.id key mismatch)

Author: psandersCreated Sep 7, 2026Updated Sep 7, 2026
Labelsbugvoice

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

  1. Originate a call via the SDK/API (Calls.createCall), which internally calls TrackCall and starts iterating the returned statusStream.
  2. Let Asterisk/ARI dispatch Dial events for the call through VoiceDispatcher.handleDial.
  3. 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-77trackCall() registers the caller's stream in trackingCallsMap keyed by the call ref returned by CreateCall:
    typescript
    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-146handleDial() calls createHandleDialEventsWithNats(this.nc)(channel.id, event), publishing to the calls.track NATS subject keyed by channel.id:
    typescript
    async handleDial(event: Dial, channel: Channel) {
      createHandleDialEventsWithNats(this.nc)(channel.id, event);
    }
  • createTrackCall.ts:53-58 then does trackingCallsMap.get(ref) and returns early on the miss:
    typescript
    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

  1. 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.
  2. SIP-originated calls carry no X-Call-Ref, so CALL_REF would be empty for them. Whatever fix is chosen needs to decide between skipping the publish for that case or minting a synthetic ref the way mods/apiserver/src/events/transformEvent.ts:73-80 already does for SIP-originated calls without a ref.

Priority

P1 – High