Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 84 additions & 3 deletions packages/@webex/plugin-meetings/src/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3734,7 +3734,7 @@ export default class Meeting extends StatelessWebexPlugin {
});
this.updateLLMConnection();
});
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
this.stopKeepAlive();

if (payload) {
Expand All @@ -3760,6 +3760,15 @@ export default class Meeting extends StatelessWebexPlugin {
});
}
this.rtcMetrics?.sendNextMetrics();

this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
LoggerProxy.logger.warn(
`Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
error?.message || String(error)
}`
);
});

this.updateLLMConnection();
});

Expand Down Expand Up @@ -5960,6 +5969,30 @@ export default class Meeting extends StatelessWebexPlugin {
);
}

/**
* Restores LLM subchannel subscriptions after reconnect when captions are active.
* @returns {void}
*/
private restoreLLMSubscriptionsIfNeeded(): void {
try {
// @ts-ignore
const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();

if (!isCaptionBoxOn) {
return;
}

// @ts-ignore
this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Await transcription resubscribe call in online handler

restoreLLMSubscriptionsIfNeeded() wraps updateSubchannelSubscriptions() in a synchronous try/catch, but updateSubchannelSubscriptions is async and its promise is not awaited or .catched here. If that promise rejects (for example, during reconnect races), the rejection bypasses this catch and becomes unhandled, so the intended warning log and graceful recovery path do not run.

Useful? React with 👍 / 👎.

} catch (error) {
const msg = error?.message || String(error);

LoggerProxy.logger.warn(
`Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
);
}
}

/**
* This is a callback for the LLM event that is triggered when it comes online
* This method in turn will trigger an event to the developers that the LLM is connected
Expand All @@ -5968,8 +6001,8 @@ export default class Meeting extends StatelessWebexPlugin {
* @returns {null}
*/
private handleLLMOnline = (): void => {
// @ts-ignore
this.webex.internal.llm.off('online', this.handleLLMOnline);
this.restoreLLMSubscriptionsIfNeeded();

Comment on lines 6003 to +6005
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove persistent LLM online listener on meeting cleanup

After this change, handleLLMOnline no longer unregisters itself, which makes the listener effectively persistent for the Meeting instance. Teardown still calls cleanupLLMConneciton() without removeOnlineListener, so ended/disposed meetings can keep online callbacks attached and receive future online events from later connections, causing duplicate MEETING_TRANSCRIPTION_CONNECTED triggers and stale-instance callbacks over time.

Useful? React with 👍 / 👎.

Trigger.trigger(
this,
{
Expand Down Expand Up @@ -6200,6 +6233,8 @@ export default class Meeting extends StatelessWebexPlugin {
this.saveDataChannelToken(join);
// @ts-ignore - config coming from registerPlugin
if (this.config.enableAutomaticLLM) {
// @ts-ignore
this.webex.internal.llm.off('online', this.handleLLMOnline);
// @ts-ignore
this.webex.internal.llm.on('online', this.handleLLMOnline);
this.updateLLMConnection()
Expand Down Expand Up @@ -6343,6 +6378,52 @@ export default class Meeting extends StatelessWebexPlugin {
}
}

/**
* Ensures default-session data channel token exists after lobby admission.
* Some lobby users do not receive a token until they are admitted.
* @returns {Promise<boolean>} true when a new token is fetched and cached
*/
private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
try {
// @ts-ignore
const datachannelToken = this.webex.internal.llm.getDatachannelToken();
// @ts-ignore
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();

if (!isDataChannelTokenEnabled || datachannelToken) {
return false;
}

const response = await this.meetingRequest.fetchDatachannelToken({
locusUrl: this.locusUrl,
requestingParticipantId: this.members.selfId,
isPracticeSession: false,
});
const fetchedDatachannelToken = response?.body?.datachannelToken;

if (!fetchedDatachannelToken) {
return false;
}

// @ts-ignore
this.webex.internal.llm.setDatachannelToken(
fetchedDatachannelToken,
DataChannelTokenType.Default
);
Comment on lines +6409 to +6412
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revalidate meeting identity before caching fetched lobby token

ensureDefaultDatachannelTokenAfterAdmit() writes the fetched token into the shared LLM token cache after an awaited network call, but it never verifies that the meeting/locus is still the same one that initiated the fetch. If the user leaves (or switches meetings) while fetchDatachannelToken() is in flight, this stale token can overwrite the cache after teardown; the next admitted-guest flow then sees a token present and skips fetching the correct one, so updateLLMConnection() can reconnect with invalid credentials.

Useful? React with 👍 / 👎.


return true;
} catch (error) {
const msg = error?.message || String(error);

LoggerProxy.logger.warn(
`Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
{statusCode: error?.statusCode}
);

return false;
}
}

/**
* Connects to low latency mercury and reconnects if the address has changed
* It will also disconnect if called when the meeting has ended
Expand Down
76 changes: 75 additions & 1 deletion packages/@webex/plugin-meetings/src/webinar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,63 @@ const Webinar = WebexPlugin.extend({
);
},

/**
* Ensures practice-session token exists before registering the practice LLM channel.
* @param {object} meeting
* @returns {Promise<string|undefined>}
*/
async ensurePracticeSessionDatachannelToken(meeting) {
// @ts-ignore
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();

if (!isDataChannelTokenEnabled) {
return undefined;
}

// @ts-ignore
const cachedToken = this.webex.internal.llm.getDatachannelToken(
DataChannelTokenType.PracticeSession
);

if (cachedToken) {
return cachedToken;
}

try {
const refreshResponse = await meeting.refreshDataChannelToken();
const {datachannelToken, dataChannelTokenType} = refreshResponse?.body ?? {};

if (!datachannelToken) {
return undefined;
}

// @ts-ignore
this.webex.internal.llm.setDatachannelToken(
datachannelToken,
dataChannelTokenType || DataChannelTokenType.PracticeSession
);
Comment on lines +188 to +191
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revalidate state before caching refreshed practice token

ensurePracticeSessionDatachannelToken() stores the refreshed token in the shared LLM cache before updatePSDataChannel() performs its stale-invocation checks. If practice eligibility or meeting lifecycle changes while refreshDataChannelToken() is in flight (e.g., user leaves and cleanup clears tokens), this late write can repopulate cache with a stale token; subsequent updates then read a cached token and skip refresh, leading to practice-session register attempts with invalid credentials. Guard this cache write with a fresh state/sequence check tied to the current meeting context.

Useful? React with 👍 / 👎.


return datachannelToken;
} catch (error) {
LoggerProxy.logger.warn(
`Webinar:index#ensurePracticeSessionDatachannelToken --> failed to proactively refresh practice-session token: ${
error?.message || String(error)
}`
);

return undefined;
}
},

/**
* Connects to low latency mercury and reconnects if the address has changed
* It will also disconnect if called when the meeting has ended
* @returns {Promise}
*/
async updatePSDataChannel() {
this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
const invocationSequence = this._updatePSDataChannelSequence;

const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();

Expand All @@ -174,7 +225,7 @@ const Webinar = WebexPlugin.extend({
meeting?.locusInfo || {};

// @ts-ignore
const practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
DataChannelTokenType.PracticeSession
);

Expand Down Expand Up @@ -229,6 +280,29 @@ const Webinar = WebexPlugin.extend({
this._pendingOnlineListener = null;
}

const refreshedPracticeSessionToken = await this.ensurePracticeSessionDatachannelToken(meeting);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revalidate default LLM connectivity after token refresh await

In updatePSDataChannel(), default-session connectivity is checked and _pendingOnlineListener is removed before await ensurePracticeSessionDatachannelToken(meeting), but there is no second default isConnected() check afterward. If the default LLM session drops while the token refresh is in flight, this stale invocation can still run registerAndConnect(..., LLM_PRACTICE_SESSION) even though the prerequisite connection is gone, and because the pending online listener was already cleared there is no automatic retry when default comes back online.

Useful? React with 👍 / 👎.


const latestPracticeSessionDatachannelUrl = get(
meeting,
'locusInfo.info.practiceSessionDatachannelUrl'
);
const isStillPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();

// Skip stale invocations after async refresh to avoid reconnecting a session
// that was already updated/cleaned by a newer state transition.
if (
invocationSequence !== this._updatePSDataChannelSequence ||
!isStillPracticeSession ||
!latestPracticeSessionDatachannelUrl ||
latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl
) {
return undefined;
Comment on lines +297 to +299
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reconnect with latest practice URL after token refresh

The post-refresh stale check returns when latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl, but this branch does not schedule a follow-up reconnect. If the practice-session URL rotates while ensurePracticeSessionDatachannelToken() is in flight (and no role/practice-status transition happens), this invocation exits and the practice LLM channel can stay disconnected until an unrelated later trigger. Instead of returning here, continue with the latest URL or immediately re-invoke updatePSDataChannel().

Useful? React with 👍 / 👎.

}

if (refreshedPracticeSessionToken) {
practiceSessionDatachannelToken = refreshedPracticeSessionToken;
}

// @ts-ignore - Fix type
return this.webex.internal.llm
Comment on lines +283 to 307
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-check practice-session state after async token refresh

updatePSDataChannel() now awaits ensurePracticeSessionDatachannelToken() and then unconditionally calls registerAndConnect(...) using the earlier session state, but there is no second isJoinPracticeSessionDataChannel() guard after the await. If practice mode/panelist status flips while the refresh request is in flight (for example, host ends practice session), a stale invocation can reconnect the practice LLM channel after a newer invocation already cleaned it up, leaving captions/subscriptions attached to the wrong session.

Useful? React with 👍 / 👎.

.registerAndConnect(
Expand Down
Loading
Loading