fix: stop websocket reconnects from re-enabling noise cancellation against template policy - #3736
Open
cyril-k-031225 wants to merge 1 commit into
Open
fix: stop websocket reconnects from re-enabling noise cancellation against template policy#3736cyril-k-031225 wants to merge 1 commit into
cyril-k-031225 wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
room.isNoiseCancellationEnabled had two writers. HMSTransport wrote the
raw /init feature flag on every connect, and Store ANDed the template
policy decision against the field's own prior value rather than against
the init flag:
room.isNoiseCancellationEnabled =
!!plugin?.enabled && !!room.isNoiseCancellationEnabled;
That made the policy writer destructive — once the init writer had
overwritten the field, the correct value was unrecoverable. Since
retrySignalDisconnectTask calls internalConnect on every websocket
reconnect, each reconnect reset the flag to the raw init value and
reopened the Krisp gate for 158-483ms until policy re-arrived. A peer
whose audio track already existed could attach Krisp inside that window,
which is how a template with noise cancellation disabled kept billing on
0.4% of joins.
Each writer now owns its own field and isNoiseCancellationEnabled is a
get-only accessor over their AND. Both writes are idempotent, both
sources default to false so the pre-policy state denies rather than
allows, and the accessor makes a third destructive writer a compile
error.
Also resolve the policy flag unconditionally from setKnownRoles instead
of from the key-driven switch in addPluginsToRoles. Dispatching on key
presence meant a template whose policy omitted noiseCancellation never
ran the writer at all, so such rooms kept the raw init flag and a policy
that later dropped the key could not revoke an earlier enable.
Whiteboard and transcription stay in the switch, where absence correctly
means no-op.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cyril-k-031225
force-pushed
the
fix-noise-cancellation-ra
branch
from
August 21, 2026 08:34
30f1115 to
6d8d0f5
Compare
raviteja83
approved these changes
Aug 21, 2026
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.
Problem
room.isNoiseCancellationEnabledhad two writers, and one of them was destructive.Writer 1 —
transport/index.ts:1114, ininternalConnect, writes the rawnoiseCancellation/initfeature flag.Writer 2 —
sdk/store/Store.ts, inhandleNoiseCancellationPlugin, ANDed the template policy decision against its own prior value rather than against the init flag:Once writer 1 overwrote the field, writer 2 had lost the information it needed and could not recover the correct value.
retrySignalDisconnectTaskcallsinternalConnecton every websocket reconnect, so each reconnect reset the field to the raw init flag. Policy re-arrived and corrected it, but not instantly — a measured 158–483 ms window (4 runs) in which the Krisp gate was open.Impact
A customer disabled noise cancellation in their template on June 8. It took effect for 99.6% of joins. The remaining 0.4% kept Krisp running and kept billing 10–50 min/day for 10 weeks.
Only 0.4% because attaching Krisp needs a local audio track to already exist when the gate reopens. On first connect the track is created after policy, so the window is unreachable; once preview completes the track persists and every reconnect becomes a live opportunity. That combination is common on flaky mobile — all 44 leaking peers were mobile.
Two amplifiers made a 300 ms race expensive: the gate is checked only at add time, so Krisp runs for the life of the track once attached (
krisp.stop≈ 0 daily); and billing charges the whole session, turning one peer × 19.5 min of exposure into 4 peers × 53.6 min billed (~2.8×).Reproduced on a second account, session
6a8620b8d7913cfe895a014f— Krisp running withisNoiseCancellationEnabled: false.Second leak, no race required
addPluginsToRolesdispatched on the keys present inparams.plugins. A template whose policy omittednoiseCancellationnever ran the policy writer at all, so the room kept the raw init flag indefinitely — and a policy that later dropped the key could not revoke an earlier enable.Fix
The single mutable field becomes two independent source fields plus a get-only accessor:
Three properties follow:
trueunless both sources genuinely saytrue.false, so the pre-policy state denies instead of allows. The 158–483 ms interval still exists in time; it no longer grants anything.Separately,
handleNoiseCancellationPluginis now called unconditionally fromsetKnownRolesinstead of from the key-drivenswitch, so an absentnoiseCancellationkey resolves to disabled. Whiteboard and transcription stay in the switch, where absence correctly means no-op — they grant permissions additively.Behavior change worth flagging
Templates that have the init flag enabled and no
noiseCancellationkey in policy currently get working noise cancellation. After this change they do not. That is the intended semantics — the template is the authority and an absent key means "not configured" — but it is a real behavior change. Worth counting how many live templates are in that state before this ships.Public API
Not affected. The two source fields live on the internal
Roomclass, which is not re-exported from the package barrel; consumers getHMSRoomfrom./schema, which is unchanged.interfaces/room.tsgains areadonlymarker, and that file is not part of the public export surface either. Verified against the builtdist/index.d.ts.Tests
Two tests, one per defect. Both were verified to be load-bearing by mutating the source to reintroduce each bug:
stays disabled when a reconnect re-writes the init flag after policy said no— the regression test for the race. Fails if the derived AND is weakened.revokes the policy source when a later policy drops the noiseCancellation key— fails if the writer goes back to key-driven dispatch.Restoring the key-driven
switchfails only the second test, so the two discriminate cleanly rather than overlapping.packages/hms-video-store: 32 suites, 235 tests, all passing. Lint andtscclean. Full monorepo build succeeds across all 10 projects.Deliberately out of scope
AudioVideoToggle.tsx:335re-adds Krisp on audio-track churn whenever the room gate and layout appdata both allow. Unchanged — it is the mechanism that converted an open gate into an attach, but the gate is now closed.noise_cancellation.enabled_by_defaultstaystrue, and the dashboard switch still renders disabled-and-unchecked once the template flag is off — invisible and unclearable. Separate repos.noiseCancellationUnleash flag for the affected account is an ops action independent of this branch.🤖 Generated with Claude Code