Skip to content

fix(voice): handle multiple transitions simultaneously#11100

Merged
kodiakhq[bot] merged 5 commits intodiscordjs:mainfrom
Snazzah:fix/dave-transitions
Dec 12, 2025
Merged

fix(voice): handle multiple transitions simultaneously#11100
kodiakhq[bot] merged 5 commits intodiscordjs:mainfrom
Snazzah:fix/dave-transitions

Conversation

@Snazzah
Copy link
Copy Markdown
Contributor

@Snazzah Snazzah commented Sep 10, 2025

Please describe the changes this PR makes and why it should be merged:

This makes it so multiple transitions can be handled properly, according to the DAVESessionManager sample code. This changes the private property pendingTransition to pendingTransitions that hold a map of transitions. I don't think that constitutes a breaking change since its a private property.

Status and versioning classification:

  • Code changes have been tested against the Discord API, or there are no code changes

@vercel
Copy link
Copy Markdown

vercel bot commented Sep 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
discord-js Skipped Skipped Dec 12, 2025 3:46pm
discord-js-guide Skipped Skipped Dec 12, 2025 3:46pm

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 10, 2025

Codecov Report

❌ Patch coverage is 0% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.46%. Comparing base (5b4dbd5) to head (c72eea8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
packages/voice/src/networking/DAVESession.ts 0.00% 23 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11100      +/-   ##
==========================================
+ Coverage   32.42%   32.46%   +0.03%     
==========================================
  Files         369      369              
  Lines       13619    13601      -18     
  Branches     1069     1066       -3     
==========================================
- Hits         4416     4415       -1     
+ Misses       9068     9051      -17     
  Partials      135      135              
Flag Coverage Δ
voice 55.53% <0.00%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Snazzah Snazzah marked this pull request as ready for review September 11, 2025 17:55
@Snazzah Snazzah requested a review from a team as a code owner September 11, 2025 17:55
@github-project-automation github-project-automation bot moved this from Todo to Review Approved in discord.js Dec 12, 2025
@vercel vercel bot temporarily deployed to Preview – discord-js December 12, 2025 15:46 Inactive
@vercel vercel bot temporarily deployed to Preview – discord-js-guide December 12, 2025 15:46 Inactive
@kodiakhq kodiakhq bot merged commit c4fc79a into discordjs:main Dec 12, 2025
7 of 8 checks passed
@github-project-automation github-project-automation bot moved this from Review Approved to Done in discord.js Dec 12, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 12, 2025

📝 Walkthrough

Walkthrough

The DAVESession.ts file refactors transition tracking to support multiple pending transitions by replacing a single pendingTransition field with a pendingTransitions Map keyed by transition ID. Related methods including prepareTransition, executeTransition, and error handling are updated to work with the new Map-based structure while maintaining existing control flow logic.

Changes

Cohort / File(s) Summary
Transition tracking refactor
packages/voice/src/networking/DAVESession.ts
Replaced pendingTransition field with pendingTransitions Map<number, number> structure. Updated prepareTransition to record transitions in map and execute immediate transitions (id 0). Modified executeTransition to validate and retrieve target protocol_version from map, then clear specific transition after successful execution. Updated all MLS commit/welcome processing paths to store transitions in the new Map. Adjusted decrypt error handling to check map size instead of single object presence.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Verify all method implementations correctly use Map operations (.set(), .get(), .has(), .delete())
  • Confirm thread-safe handling of immediate reinitialization when protocol_version is 0
  • Validate that all code paths previously using pendingTransition now properly interact with the Map structure
  • Check decrypt error handling logic correctly interprets map size (0 vs. >0)

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately and specifically describes the main change: implementing support for handling multiple transitions simultaneously rather than just a single transition.
Description check ✅ Passed The pull request description is related to the changeset, explaining the motivation (following sample code), the implementation detail (replacing pendingTransition with pendingTransitions Map), and clarifying it's not a breaking change since it's private.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/voice/src/networking/DAVESession.ts (5)

211-224: prepareTransition() likely needs a guard against overwriting “better” state from commit/welcome (or vice‑versa).
Right now it unconditionally set()s, but processCommit() / processWelcome() also set() the same key with a different value; whichever arrives last wins.

Suggested direction: make prepareTransition() the only place that sets the target protocol version, and have commit/welcome only “mark pending” without clobbering it (see comment on processCommit / processWelcome below).


278-285: Delete invalidated transition(s) from pendingTransitions to avoid later false-success executes.
This prevents “stale pending” from being executed after you’ve already asked the voice server to invalidate it.

 public recoverFromInvalidTransition(transitionId: number) {
   if (this.reinitializing) return;
   this.emit('debug', `Invalidating transition ${transitionId}`);
   this.reinitializing = true;
   this.consecutiveFailures = 0;
+  this.pendingTransitions.delete(transitionId);
   this.emit('invalidateTransition', transitionId);
   this.reinit();
 }

313-324: processCommit() should not overwrite the target protocol version recorded by prepareTransition().
this.pendingTransitions.set(transitionId, this.protocolVersion) will clobber prepareTransition(...protocol_version) if prepareTransition() arrived first, causing executeTransition() to apply the wrong version.

At minimum, only set if absent:

- this.pendingTransitions.set(transitionId, this.protocolVersion);
+ if (!this.pendingTransitions.has(transitionId)) {
+   this.pendingTransitions.set(transitionId, this.protocolVersion);
+ }

If you actually need both “pending commit/welcome seen” and “target protocol version”, consider changing the map value to an object (e.g. { targetProtocolVersion?: number; commitSeen: boolean; }) instead of a single number.


340-351: Same overwrite bug as processCommit() for processWelcome().
This can clobber prepareTransition()’s target version for the same transitionId.

- this.pendingTransitions.set(transitionId, this.protocolVersion);
+ if (!this.pendingTransitions.has(transitionId)) {
+   this.pendingTransitions.set(transitionId, this.protocolVersion);
+ }

386-397: Verify decrypt-failure policy when pendingTransitions.size > 0: you now suppress failure counting entirely.
If a transition gets “stuck” (pending forever), you may never recover because consecutiveFailures won’t advance.

If intentional, consider pruning expired pending transitions (timestamped entries) and/or falling back to failure counting after some deadline.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b4dbd5 and c72eea8.

📒 Files selected for processing (1)
  • packages/voice/src/networking/DAVESession.ts (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests

Comment on lines 107 to +110
/**
* The pending transition.
* The pending transitions, mapped by their ID and the protocol version.
*/
private pendingTransition?: VoiceDavePrepareTransitionData | undefined;
private pendingTransitions = new Map<number, number>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Make pendingTransitions reference readonly to prevent accidental reassignment.
Map contents remain mutable, but the field itself shouldn’t be reassigned.

- private pendingTransitions = new Map<number, number>();
+ private readonly pendingTransitions = new Map<number, number>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* The pending transition.
* The pending transitions, mapped by their ID and the protocol version.
*/
private pendingTransition?: VoiceDavePrepareTransitionData | undefined;
private pendingTransitions = new Map<number, number>();
/**
* The pending transitions, mapped by their ID and the protocol version.
*/
private readonly pendingTransitions = new Map<number, number>();
🤖 Prompt for AI Agents
In packages/voice/src/networking/DAVESession.ts around lines 107 to 110, the
class field pendingTransitions is declared mutable and should be marked readonly
to prevent accidental reassignment; change the field declaration to include the
readonly modifier (keeping the Map itself mutable) so only its contents can be
changed, not the reference.

Comment on lines 231 to 258
public executeTransition(transitionId: number) {
this.emit('debug', `Executing transition (${transitionId})`);
if (!this.pendingTransition) {
if (!this.pendingTransitions.has(transitionId)) {
this.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);
return;
return false;
}

let transitioned = false;
if (transitionId === this.pendingTransition.transition_id) {
const oldVersion = this.protocolVersion;
this.protocolVersion = this.pendingTransition.protocol_version;

// Handle upgrades & defer downgrades
if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
this.downgraded = true;
this.emit('debug', 'Session downgraded');
} else if (transitionId > 0 && this.downgraded) {
this.downgraded = false;
this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
this.emit('debug', 'Session upgraded');
}

// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time
transitioned = true;
this.reinitializing = false;
this.lastTransitionId = transitionId;
this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
} else {
this.emit(
'debug',
`Received execute transition for an unexpected transition id (expected: ${this.pendingTransition.transition_id}, actual: ${transitionId})`,
);
const oldVersion = this.protocolVersion;
this.protocolVersion = this.pendingTransitions.get(transitionId)!;

// Handle upgrades & defer downgrades
if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
this.downgraded = true;
this.emit('debug', 'Session downgraded');
} else if (transitionId > 0 && this.downgraded) {
this.downgraded = false;
this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
this.emit('debug', 'Session upgraded');
}

this.pendingTransition = undefined;
return transitioned;
// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time
this.reinitializing = false;
this.lastTransitionId = transitionId;
this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);

this.pendingTransitions.delete(transitionId);
return true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t allow executeTransition() to “complete” a transition while reinitializing is true; also type the return explicitly.
As-is, a late executeTransition() can flip this.reinitializing = false and advance lastTransitionId even if you already invalidated that transition.

- public executeTransition(transitionId: number) {
+ public executeTransition(transitionId: number): boolean {
   this.emit('debug', `Executing transition (${transitionId})`);
+  if (this.reinitializing) {
+    this.emit('debug', `Ignoring execute transition (${transitionId}) while reinitializing`);
+    return false;
+  }
   if (!this.pendingTransitions.has(transitionId)) {
     this.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);
     return false;
   }
   const oldVersion = this.protocolVersion;
   this.protocolVersion = this.pendingTransitions.get(transitionId)!;
   // ...
   this.pendingTransitions.delete(transitionId);
   return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public executeTransition(transitionId: number) {
this.emit('debug', `Executing transition (${transitionId})`);
if (!this.pendingTransition) {
if (!this.pendingTransitions.has(transitionId)) {
this.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);
return;
return false;
}
let transitioned = false;
if (transitionId === this.pendingTransition.transition_id) {
const oldVersion = this.protocolVersion;
this.protocolVersion = this.pendingTransition.protocol_version;
// Handle upgrades & defer downgrades
if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
this.downgraded = true;
this.emit('debug', 'Session downgraded');
} else if (transitionId > 0 && this.downgraded) {
this.downgraded = false;
this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
this.emit('debug', 'Session upgraded');
}
// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time
transitioned = true;
this.reinitializing = false;
this.lastTransitionId = transitionId;
this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
} else {
this.emit(
'debug',
`Received execute transition for an unexpected transition id (expected: ${this.pendingTransition.transition_id}, actual: ${transitionId})`,
);
const oldVersion = this.protocolVersion;
this.protocolVersion = this.pendingTransitions.get(transitionId)!;
// Handle upgrades & defer downgrades
if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
this.downgraded = true;
this.emit('debug', 'Session downgraded');
} else if (transitionId > 0 && this.downgraded) {
this.downgraded = false;
this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
this.emit('debug', 'Session upgraded');
}
this.pendingTransition = undefined;
return transitioned;
// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time
this.reinitializing = false;
this.lastTransitionId = transitionId;
this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
this.pendingTransitions.delete(transitionId);
return true;
}
public executeTransition(transitionId: number): boolean {
this.emit('debug', `Executing transition (${transitionId})`);
if (this.reinitializing) {
this.emit('debug', `Ignoring execute transition (${transitionId}) while reinitializing`);
return false;
}
if (!this.pendingTransitions.has(transitionId)) {
this.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);
return false;
}
const oldVersion = this.protocolVersion;
this.protocolVersion = this.pendingTransitions.get(transitionId)!;
// Handle upgrades & defer downgrades
if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
this.downgraded = true;
this.emit('debug', 'Session downgraded');
} else if (transitionId > 0 && this.downgraded) {
this.downgraded = false;
this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
this.emit('debug', 'Session upgraded');
}
// In the future we'd want to signal to the DAVESession to transition also, but it only supports v1 at this time
this.reinitializing = false;
this.lastTransitionId = transitionId;
this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
this.pendingTransitions.delete(transitionId);
return true;
}
🤖 Prompt for AI Agents
In packages/voice/src/networking/DAVESession.ts around lines 231 to 258,
executeTransition can still mark a transition complete even while
reinitializing; add an early guard that if this.reinitializing is true you emit
a debug message and return false without modifying lastTransitionId,
reinitializing, downgraded, session state, or deleting the pending transition;
also change the method signature to explicitly return boolean. Ensure the rest
of the method only runs when not reinitializing so late calls do not advance
state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

5 participants