VAPI-3989: map Twilio Stream <Parameter> children to StreamParam - #28
Merged
mramasubramanian-bw merged 3 commits intoSep 22, 2026
Merged
Conversation
Twilio bots attach key/value context to a media stream with <Parameter>
children on <Stream>; that is how most bots receive callSid, tenant, and
similar values when the WebSocket opens. The translator read only the
<Stream> attributes and ignored its children, so a Stream with two
Parameters produced byte-identical BXML to one with none, no finding was
raised, and the bot connected with an empty customParameters map.
Translator:
- Each <Parameter name value/> becomes a nested <StreamParam name value/>
under the emitted <StartStream>, in order, for both Connect (bidirectional)
and Start (fork) streams. Attribute values are XML-escaped by the builder.
- Bandwidth allows at most 12 StreamParam per StartStream; extras are
dropped with a Stream warning naming the count. Twilio caps name+value at
500 chars combined, so Bandwidth's 256/2048 per-attribute limits cannot
be exceeded by valid TwiML and are not re-checked.
- A <Parameter> missing name or value, or any non-Parameter child, is
dropped with a warning instead of emitting BXML Bandwidth would reject.
Bridge:
- Add customParametersFromBwStart(), which maps Bandwidth's StartStream
"start" event (streamParams: flat name->value map) to the Twilio
customParameters map the bridge already forwards in its own "start"
message. Values are coerced to strings; malformed input yields {}.
Wiring a live Bandwidth source that calls it is VAPI-3991.
Docs: update the Stream matrix note and AGENTS.md. Tests cover ordering,
nesting inside StartStream, the fork case, escaping, the 12 cap, invalid
Parameters, unknown children, and the bridge mapper end to end.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Review follow-ups: - The earlier comment claimed Twilio's 500-char combined name+value cap made Bandwidth's per-attribute limits unreachable. That holds for value (2048) but not for name (256): a 300-char Parameter name is valid TwiML and made Bandwidth reject the whole BXML document. Drop a Parameter whose name exceeds 256 or value exceeds 2048 with a warning that names the length, same as the 12-element cap. - customParametersFromBwStart now builds a null-prototype object so a parameter literally named "__proto__" is stored as an own property instead of hitting the Object.prototype setter and vanishing. - The 12-cap test asserted /2 / which also matched "12"; it now checks the exact dropped-count phrase. Add tests for the 256/2048 limits and the __proto__ key.
…ings Review polish: - Duplicate <Parameter> names: Bandwidth delivers streamParams as a flat map (as does Twilio's customParameters), so a repeated name can carry one value and which wins is undocumented. Keep the first, drop the rest, and warn, matching the project's no-silent-degradation convention. - The 12-cap check now runs before the per-parameter validity checks, so every <Parameter> past the 12 accepted ones is tallied under the cap warning and its count is exact in mixed cases. - The missing-name/value warning truncated nothing; a huge name went whole into the finding. All finding paths now echo at most 32 chars of a name. - Drop the conditional around StartStream children: an empty children array already serializes as a self-closing tag. - customParametersFromBwStart forwards only string/number/boolean values. Nested objects and arrays are outside the documented flat shape and are skipped instead of becoming "[object Object]". Not changed: length checks count UTF-16 code units, as the docs say "characters". Whether Bandwidth counts bytes is to be confirmed against a live response during VAPI-3991 fixture capture.
cguthrie-bw
approved these changes
Sep 22, 2026
mramasubramanian-bw
deleted the
VAPI-3989/stream-parameter-to-streamparam
branch
September 22, 2026 19:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Twilio bots attach key/value context to a media stream with
<Parameter>children on<Stream>. That is how most bots receivecallSid, tenant, and similar values when the WebSocket opens. The translator read only the<Stream>attributes and ignored its children, so a Stream with two Parameters produced byte-identical BXML to one with none, no finding was raised, and the bot connected with an emptycustomParametersmap.Each
<Parameter name value/>now becomes a nested<StreamParam name value/>under the emitted<StartStream>, in order. Bandwidth echoes those in its WebSocketstartevent asstreamParams, and a new bridge helper maps that to the TwiliocustomParametersthe bot expects.Changes
Translator (
src/translator/translate.ts)<Parameter>children map to nested<StreamParam/>for both Connect (bidirectional) and Start (fork) streams. Values are XML-escaped by the builder.StreamParamperStartStream, withnameup to 256 characters andvalueup to 2048. Exceeding any of these makes Bandwidth reject the whole BXML document, so an over-limit<Parameter>is dropped with a Stream warning that names the count or length. Twilio's only limit is 500 characters for name plus value combined, which boundsvaluebut leavesnamefree to exceed 256 in valid TwiML.<Parameter>missingnameorvalue, or any non-Parameter child, is dropped with a warning instead of emitting BXML Bandwidth would reject.<Parameter>names:streamParamsis a flat map, so only one value can survive and which one is undocumented. The first is kept, later ones are dropped with a warning.Bridge (
src/streams/bridge.ts)customParametersFromBwStart()maps Bandwidth'sstartevent (streamParams, a flat name to value map) to the TwiliocustomParametersmap the bridge already forwards in its ownstartmessage. String, number, and boolean values are forwarded as strings. Nested objects and arrays are outside the documented flat shape and are skipped. Malformed input yields an empty object. The result is a null-prototype object so a parameter literally named__proto__is kept rather than swallowed by the prototype setter.Docs: Stream matrix note and
AGENTS.md.Before / after
Input:
Before:
After:
Verification
npm run typecheck && npx vitest run: 44 files, 321 passed, 6 skipped.test/translate-stream-conference.test.tscover ordering, nesting inside StartStream, the fork case, escaping, the 12 cap, the 256 / 2048 length limits, duplicate names, an exact cap tally in mixed cases, invalid Parameters, and unknown children.test/streams-wire.test.tspush a Bandwidth-shapedstartevent through the mapper and assert the bot receives it ascustomParameters, including a key named__proto__.Open question for VAPI-3991: the length checks count UTF-16 code units because the docs say "characters". If Bandwidth counts bytes, a multibyte value could pass here and still be rejected. Confirm against a live response during fixture capture.
Reference: StartStream docs, StreamParam.