@webex/plugin-meetings 3.12.0-next.65 → 3.12.0-next.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -136,6 +136,7 @@ import {
136
136
  STAGE_MANAGER_TYPE,
137
137
  LOCUSEVENT,
138
138
  LOCUS_LLM_EVENT,
139
+ LLM_DEFAULT_SESSION,
139
140
  LLM_PRACTICE_SESSION,
140
141
  } from '../constants';
141
142
  import BEHAVIORAL_METRICS from '../metrics/constants';
@@ -3507,8 +3508,6 @@ export default class Meeting extends StatelessWebexPlugin {
3507
3508
  this.recordingController.setLocusUrl(this.locusUrl);
3508
3509
  this.controlsOptionsManager.setLocusUrl(this.locusUrl, !!isMainLocus);
3509
3510
  this.webinar.locusUrlUpdate(url);
3510
- // @ts-ignore
3511
- this.webex.internal.llm.setRefreshHandler(() => this.refreshDataChannelToken());
3512
3511
 
3513
3512
  Trigger.trigger(
3514
3513
  this,
@@ -3777,7 +3776,7 @@ export default class Meeting extends StatelessWebexPlugin {
3777
3776
  });
3778
3777
  this.updateLLMConnection();
3779
3778
  });
3780
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3779
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
3781
3780
  this.stopKeepAlive();
3782
3781
 
3783
3782
  if (payload) {
@@ -3804,13 +3803,15 @@ export default class Meeting extends StatelessWebexPlugin {
3804
3803
  }
3805
3804
  this.rtcMetrics?.sendNextMetrics();
3806
3805
 
3807
- this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
3806
+ try {
3807
+ await this.ensureDefaultDatachannelTokenAfterAdmit();
3808
+ } catch (error) {
3808
3809
  LoggerProxy.logger.warn(
3809
3810
  `Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
3810
3811
  error?.message || String(error)
3811
3812
  }`
3812
3813
  );
3813
- });
3814
+ }
3814
3815
 
3815
3816
  this.updateLLMConnection();
3816
3817
  });
@@ -6499,16 +6500,23 @@ export default class Meeting extends StatelessWebexPlugin {
6499
6500
  throwOnError?: boolean;
6500
6501
  } = {}): Promise<void> => {
6501
6502
  // @ts-ignore - Fix type
6502
- const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
6503
- const isOwner = !currentOwner || currentOwner === this.id;
6503
+ // @ts-ignore - Fix type
6504
+ const {currentOwner, isOwner} = this.webex.internal.llm.resolveSessionOwnership(
6505
+ this.id,
6506
+ LLM_DEFAULT_SESSION
6507
+ );
6504
6508
 
6505
6509
  try {
6506
6510
  if (isOwner) {
6507
6511
  // @ts-ignore - Fix type
6508
- await this.webex.internal.llm.disconnectLLM({
6509
- code: 3050,
6510
- reason: 'done (permanent)',
6511
- });
6512
+ await this.webex.internal.llm.disconnectLLM(
6513
+ {
6514
+ code: 3050,
6515
+ reason: 'done (permanent)',
6516
+ },
6517
+ LLM_DEFAULT_SESSION,
6518
+ this.id
6519
+ );
6512
6520
  } else {
6513
6521
  LoggerProxy.logger.info(
6514
6522
  `Meeting:index#cleanupLLMConneciton --> skipping disconnect; LLM owned by meeting ${currentOwner}, not ${this.id}`
@@ -6530,27 +6538,31 @@ export default class Meeting extends StatelessWebexPlugin {
6530
6538
  }
6531
6539
  this.stopListeningForLLMEvents();
6532
6540
 
6533
- // If this meeting owned (or could have owned) the default LLM session,
6534
- // always release the owner tag here regardless of whether disconnectLLM
6535
- // resolved. `disconnectLLM` only clears the owner on its success path,
6536
- // so a failed disconnect would otherwise leave a stale owner pointing
6537
- // at a torn-down meeting and permanently block other meetings'
6538
- // `updateLLMConnection` calls via the ownership guard.
6541
+ // Re-check ownership after awaiting disconnectLLM. If ownership changed
6542
+ // while cleanup was in flight, do not clear another meeting's owner tag.
6539
6543
  if (isOwner) {
6540
- // @ts-ignore - Fix type
6541
- this.webex.internal.llm.setOwnerMeetingId?.(undefined);
6544
+ const {currentOwner: currentOwnerAfterCleanup} =
6545
+ // @ts-ignore - Fix type
6546
+ this.webex.internal.llm.resolveSessionOwnership(this.id, LLM_DEFAULT_SESSION);
6547
+
6548
+ if (currentOwnerAfterCleanup === this.id) {
6549
+ // @ts-ignore - Fix type
6550
+ this.webex.internal.llm.setOwnerMeetingId?.(undefined);
6551
+ }
6542
6552
  }
6543
6553
  }
6544
6554
  };
6545
6555
 
6546
6556
  /**
6547
- * Clears all data channel tokens stored in LLM.
6548
- * Called during meeting cleanup to ensure stale tokens are not reused.
6557
+ * Clears data channel tokens associated with this meeting ownership.
6558
+ * Ownership checks are enforced in internal-plugin-llm.
6549
6559
  * @returns {void}
6550
6560
  */
6551
6561
  clearDataChannelToken(): void {
6552
6562
  // @ts-ignore
6553
- this.webex.internal.llm.resetDatachannelTokens();
6563
+ this.webex.internal.llm.clearDatachannelToken(LLM_DEFAULT_SESSION, this.id);
6564
+ // @ts-ignore
6565
+ this.webex.internal.llm.clearDatachannelToken(LLM_PRACTICE_SESSION, this.id);
6554
6566
  }
6555
6567
 
6556
6568
  /**
@@ -6565,14 +6577,15 @@ export default class Meeting extends StatelessWebexPlugin {
6565
6577
 
6566
6578
  if (datachannelToken) {
6567
6579
  // @ts-ignore
6568
- this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
6580
+ this.webex.internal.llm.setDatachannelToken(datachannelToken, LLM_DEFAULT_SESSION, this.id);
6569
6581
  }
6570
6582
 
6571
6583
  if (practiceSessionDatachannelToken) {
6572
6584
  // @ts-ignore
6573
6585
  this.webex.internal.llm.setDatachannelToken(
6574
6586
  practiceSessionDatachannelToken,
6575
- DataChannelTokenType.PracticeSession
6587
+ LLM_PRACTICE_SESSION,
6588
+ this.id
6576
6589
  );
6577
6590
  }
6578
6591
  }
@@ -6585,7 +6598,10 @@ export default class Meeting extends StatelessWebexPlugin {
6585
6598
  private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
6586
6599
  try {
6587
6600
  // @ts-ignore
6588
- const datachannelToken = this.webex.internal.llm.getDatachannelToken();
6601
+ const datachannelToken = this.webex.internal.llm.getDatachannelToken(
6602
+ LLM_DEFAULT_SESSION,
6603
+ this.id
6604
+ );
6589
6605
  // @ts-ignore
6590
6606
  const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
6591
6607
 
@@ -6607,7 +6623,8 @@ export default class Meeting extends StatelessWebexPlugin {
6607
6623
  // @ts-ignore
6608
6624
  this.webex.internal.llm.setDatachannelToken(
6609
6625
  fetchedDatachannelToken,
6610
- DataChannelTokenType.Default
6626
+ LLM_DEFAULT_SESSION,
6627
+ this.id
6611
6628
  );
6612
6629
 
6613
6630
  return true;
@@ -6634,11 +6651,6 @@ export default class Meeting extends StatelessWebexPlugin {
6634
6651
 
6635
6652
  const isJoined = this.isJoined();
6636
6653
 
6637
- // @ts-ignore
6638
- const datachannelToken = this.webex.internal.llm.getDatachannelToken(
6639
- DataChannelTokenType.Default
6640
- );
6641
-
6642
6654
  const dataChannelUrl = datachannelUrl;
6643
6655
 
6644
6656
  // Ownership guard: when the default LLM session is already connected and
@@ -6648,10 +6660,33 @@ export default class Meeting extends StatelessWebexPlugin {
6648
6660
  // connection when this meeting is the current owner, or when no owner is
6649
6661
  // set yet (first claim).
6650
6662
  // @ts-ignore - Fix type
6651
- const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
6663
+ const {currentOwner} = this.webex.internal.llm.resolveSessionOwnership(
6664
+ this.id,
6665
+ LLM_DEFAULT_SESSION
6666
+ );
6652
6667
 
6668
+ // Capture connectivity before any reconnect attempt. If LLM was already
6669
+ // connected, we must respect current ownership. If it was disconnected,
6670
+ // this flow may reclaim stale owner tags after a fresh connect.
6653
6671
  // @ts-ignore - Fix type
6654
- if (this.webex.internal.llm.isConnected()) {
6672
+ const wasConnected = this.webex.internal.llm.isConnected();
6673
+
6674
+ // Prefer ownership-scoped token read. For disconnected stale-owner reclaim
6675
+ // flows, fallback to ownerless read so initial register can still carry a
6676
+ // token and recover from stale ownership without 401/403 dead-end.
6677
+ // @ts-ignore - Fix type
6678
+ let datachannelToken = this.webex.internal.llm.getDatachannelToken(
6679
+ LLM_DEFAULT_SESSION,
6680
+ this.id
6681
+ );
6682
+
6683
+ if (!datachannelToken && !wasConnected && currentOwner && currentOwner !== this.id) {
6684
+ // @ts-ignore - Fix type
6685
+ datachannelToken = this.webex.internal.llm.getDatachannelToken(LLM_DEFAULT_SESSION);
6686
+ }
6687
+
6688
+ // @ts-ignore - Fix type
6689
+ if (wasConnected) {
6655
6690
  if (currentOwner && currentOwner !== this.id) {
6656
6691
  // Another meeting owns the live LLM socket. We must not disconnect
6657
6692
  // or reconfigure it -- doing so would tear down a session the
@@ -6684,6 +6719,20 @@ export default class Meeting extends StatelessWebexPlugin {
6684
6719
  return undefined;
6685
6720
  }
6686
6721
 
6722
+ // Bind refresh handler before registration so interceptor-triggered token
6723
+ // refresh during register POST can resolve a valid handler.
6724
+ // Prefer this meeting as owner, but allow owner-less fallback when a stale
6725
+ // foreign owner tag is present on a disconnected session.
6726
+ const refreshHandlerOwnerMeetingId =
6727
+ currentOwner && currentOwner !== this.id ? undefined : this.id;
6728
+ const shouldAlignRefreshHandlerAfterOwnershipClaim = refreshHandlerOwnerMeetingId !== this.id;
6729
+ // @ts-ignore - Fix type
6730
+ this.webex.internal.llm.setRefreshHandler(
6731
+ () => this.refreshDataChannelToken(),
6732
+ LLM_DEFAULT_SESSION,
6733
+ refreshHandlerOwnerMeetingId
6734
+ );
6735
+
6687
6736
  // @ts-ignore - Fix type
6688
6737
  return this.webex.internal.llm
6689
6738
  .registerAndConnect(url, dataChannelUrl, datachannelToken)
@@ -6693,7 +6742,33 @@ export default class Meeting extends StatelessWebexPlugin {
6693
6742
  // subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
6694
6743
  // calls can detect and skip work that doesn't belong to them.
6695
6744
  // @ts-ignore - Fix type
6696
- this.webex.internal.llm.setOwnerMeetingId?.(this.id);
6745
+ const {isOwner} = this.webex.internal.llm.resolveSessionOwnership(
6746
+ this.id,
6747
+ LLM_DEFAULT_SESSION
6748
+ );
6749
+ const canReclaimAfterDisconnectedStart = !wasConnected;
6750
+
6751
+ // Refresh handler is pre-bound before registerAndConnect so token
6752
+ // refresh can work even during the registration request itself.
6753
+ if (isOwner || canReclaimAfterDisconnectedStart) {
6754
+ // Record ownership of the default LLM session for this meeting so
6755
+ // subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
6756
+ // calls can detect and skip work that doesn't belong to them.
6757
+ // @ts-ignore - Fix type
6758
+ this.webex.internal.llm.setOwnerMeetingId?.(this.id);
6759
+
6760
+ // If we pre-bound refresh ownerlessly (stale-owner reclaim path),
6761
+ // align the handler with the newly claimed owner immediately after
6762
+ // ownership is updated.
6763
+ if (shouldAlignRefreshHandlerAfterOwnershipClaim) {
6764
+ // @ts-ignore - Fix type
6765
+ this.webex.internal.llm.setRefreshHandler(
6766
+ () => this.refreshDataChannelToken(),
6767
+ LLM_DEFAULT_SESSION,
6768
+ this.id
6769
+ );
6770
+ }
6771
+ }
6697
6772
  // @ts-ignore - Fix type
6698
6773
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6699
6774
  // @ts-ignore - Fix type
@@ -9991,21 +10066,8 @@ export default class Meeting extends StatelessWebexPlugin {
9991
10066
  // again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
9992
10067
  // because stopTranscription() always fires its trigger.
9993
10068
  //
9994
- // Ownership-aware token clear: only clear the shared LLM data channel
9995
- // tokens when this meeting owns (or no meeting owns) the default LLM
9996
- // session. Otherwise we would wipe tokens still in use by another
9997
- // meeting's active LLM connection.
9998
- // @ts-ignore - Fix type
9999
- const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
10000
- const isOwner = !currentOwner || currentOwner === this.id;
10001
-
10002
- if (isOwner) {
10003
- this.clearDataChannelToken();
10004
- } else {
10005
- LoggerProxy.logger.info(
10006
- `Meeting:index#clearMeetingData --> skipping clearDataChannelToken; LLM owned by meeting ${currentOwner}, not ${this.id}`
10007
- );
10008
- }
10069
+ // Ownership-aware token clear is encapsulated inside clearDataChannelToken().
10070
+ this.clearDataChannelToken();
10009
10071
 
10010
10072
  await this.cleanupLLMConneciton({throwOnError: false});
10011
10073
  };
@@ -4,7 +4,6 @@
4
4
  import {WebexPlugin, config} from '@webex/webex-core';
5
5
  import uuid from 'uuid';
6
6
  import {get} from 'lodash';
7
- import {DataChannelTokenType} from '@webex/internal-plugin-llm';
8
7
  import {
9
8
  _ID_,
10
9
  HEADERS,
@@ -185,6 +184,10 @@ const Webinar = WebexPlugin.extend({
185
184
  * @returns {Promise<void>}
186
185
  */
187
186
  async cleanupPSDataChannel() {
187
+ const {isOwner} = this.webex.internal.llm.resolveSessionOwnership(
188
+ this.meetingId,
189
+ LLM_PRACTICE_SESSION
190
+ );
188
191
  this.llmListeners = this.llmListeners || {};
189
192
 
190
193
  if (this._pendingOnlineListener) {
@@ -193,20 +196,47 @@ const Webinar = WebexPlugin.extend({
193
196
  this._pendingOnlineListener = null;
194
197
  }
195
198
 
196
- // @ts-ignore - Fix type
197
- await this.webex.internal.llm.disconnectLLM(
198
- {
199
- code: 3050,
200
- reason: 'done (permanent)',
201
- },
202
- LLM_PRACTICE_SESSION
203
- );
199
+ try {
200
+ // @ts-ignore - Fix type
201
+ const disconnected = await this.webex.internal.llm.disconnectLLM(
202
+ {
203
+ code: 3050,
204
+ reason: 'done (permanent)',
205
+ },
206
+ LLM_PRACTICE_SESSION,
207
+ this.meetingId
208
+ );
204
209
 
205
- for (const {event, listenerKey} of PS_LLM_EVENTS) {
206
- if (this.llmListeners[listenerKey]) {
210
+ if (!disconnected) {
211
+ LoggerProxy.logger.info(
212
+ `Webinar:index#cleanupPSDataChannel --> skipping disconnect; practice-session LLM is not owned by meeting ${this.meetingId}`
213
+ );
214
+ }
215
+ } catch (error) {
216
+ // disconnectLLM clears ownership only on success; release a stale owner
217
+ // tag here so other meeting instances can reclaim practice-session LLM.
218
+ if (isOwner) {
207
219
  // @ts-ignore - Fix type
208
- this.webex.internal.llm.off(event, this.llmListeners[listenerKey]);
209
- this.llmListeners[listenerKey] = null;
220
+ this.webex.internal.llm.setOwnerMeetingId?.(undefined, LLM_PRACTICE_SESSION);
221
+ }
222
+
223
+ throw error;
224
+ } finally {
225
+ if (this._practiceSessionRelayListener) {
226
+ // @ts-ignore - Fix type
227
+ this.webex.internal.llm.off(
228
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
229
+ this._practiceSessionRelayListener
230
+ );
231
+ }
232
+ this._practiceSessionRelayListener = null;
233
+
234
+ for (const {event, listenerKey} of PS_LLM_EVENTS) {
235
+ if (this.llmListeners[listenerKey]) {
236
+ // @ts-ignore - Fix type
237
+ this.webex.internal.llm.off(event, this.llmListeners[listenerKey]);
238
+ this.llmListeners[listenerKey] = null;
239
+ }
210
240
  }
211
241
  }
212
242
  },
@@ -228,7 +258,8 @@ const Webinar = WebexPlugin.extend({
228
258
 
229
259
  // @ts-ignore
230
260
  const cachedToken = this.webex.internal.llm.getDatachannelToken(
231
- DataChannelTokenType.PracticeSession
261
+ LLM_PRACTICE_SESSION,
262
+ this.meetingId
232
263
  );
233
264
 
234
265
  if (cachedToken) {
@@ -246,7 +277,8 @@ const Webinar = WebexPlugin.extend({
246
277
  // @ts-ignore
247
278
  this.webex.internal.llm.setDatachannelToken(
248
279
  datachannelToken,
249
- dataChannelTokenType || DataChannelTokenType.PracticeSession
280
+ dataChannelTokenType || LLM_PRACTICE_SESSION,
281
+ this.meetingId
250
282
  );
251
283
 
252
284
  return datachannelToken;
@@ -274,6 +306,10 @@ const Webinar = WebexPlugin.extend({
274
306
 
275
307
  const meeting = this.getValidatedWebinarMeeting();
276
308
  const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
309
+ const {currentOwner, isOwner} = this.webex.internal.llm.resolveSessionOwnership(
310
+ this.meetingId,
311
+ LLM_PRACTICE_SESSION
312
+ );
277
313
 
278
314
  if (!isPracticeSession) {
279
315
  await this.cleanupPSDataChannel();
@@ -281,13 +317,22 @@ const Webinar = WebexPlugin.extend({
281
317
  return undefined;
282
318
  }
283
319
 
320
+ if (!isOwner) {
321
+ LoggerProxy.logger.info(
322
+ `Webinar:index#updatePSDataChannel --> skipping; practice-session LLM owned by meeting ${currentOwner}, not ${this.meetingId}`
323
+ );
324
+
325
+ return undefined;
326
+ }
327
+
284
328
  // @ts-ignore - Fix type
285
329
  const {url = undefined, info: {practiceSessionDatachannelUrl = undefined} = {}} =
286
330
  meeting?.locusInfo || {};
287
331
 
288
332
  // @ts-ignore
289
333
  let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
290
- DataChannelTokenType.PracticeSession
334
+ LLM_PRACTICE_SESSION,
335
+ this.meetingId
291
336
  );
292
337
 
293
338
  const isCaptionBoxOn = this.webex.internal.voicea.getIsCaptionBoxOn();
@@ -364,6 +409,30 @@ const Webinar = WebexPlugin.extend({
364
409
  practiceSessionDatachannelToken = refreshedPracticeSessionToken;
365
410
  }
366
411
 
412
+ const {currentOwner: currentOwnerBeforeConnect, isOwner: isOwnerBeforeConnect} =
413
+ this.webex.internal.llm.resolveSessionOwnership(this.meetingId, LLM_PRACTICE_SESSION);
414
+
415
+ if (!isOwnerBeforeConnect) {
416
+ LoggerProxy.logger.info(
417
+ `Webinar:index#updatePSDataChannel --> skipping pre-connect owner write; practice-session LLM owned by meeting ${currentOwnerBeforeConnect}, not ${this.meetingId}`
418
+ );
419
+
420
+ return undefined;
421
+ }
422
+
423
+ // Ensure refresh for practice datachannel requests is routed to this
424
+ // meeting only when we are actually about to connect the practice session.
425
+ // This avoids claiming ownership in flows that return early (e.g. missing
426
+ // practiceSessionDatachannelUrl or waiting for default session online).
427
+ // @ts-ignore - Fix type
428
+ this.webex.internal.llm.setRefreshHandler(
429
+ () => meeting.refreshDataChannelToken(),
430
+ LLM_PRACTICE_SESSION,
431
+ this.meetingId
432
+ );
433
+ // @ts-ignore - Fix type
434
+ this.webex.internal.llm.setOwnerMeetingId?.(this.meetingId, LLM_PRACTICE_SESSION);
435
+
367
436
  // @ts-ignore - Fix type
368
437
  return this.webex.internal.llm
369
438
  .registerAndConnect(
@@ -373,6 +442,18 @@ const Webinar = WebexPlugin.extend({
373
442
  LLM_PRACTICE_SESSION
374
443
  )
375
444
  .then((registerAndConnectResult) => {
445
+ const {currentOwner: currentOwnerAfterConnect, isOwner: isOwnerAfterConnect} =
446
+ this.webex.internal.llm.resolveSessionOwnership(this.meetingId, LLM_PRACTICE_SESSION);
447
+
448
+ if (this.meetingId && isOwnerAfterConnect) {
449
+ // @ts-ignore - Fix type
450
+ this.webex.internal.llm.setOwnerMeetingId?.(this.meetingId, LLM_PRACTICE_SESSION);
451
+ } else {
452
+ LoggerProxy.logger.info(
453
+ `Webinar:index#updatePSDataChannel --> skipping post-connect owner write; practice-session LLM owned by meeting ${currentOwnerAfterConnect}, not ${this.meetingId}`
454
+ );
455
+ }
456
+
376
457
  // Track the exact listener references so cleanupPSDataChannel can
377
458
  // unsubscribe deterministically, even if the meeting can no longer
378
459
  // be resolved at cleanup time.
@@ -395,6 +476,23 @@ const Webinar = WebexPlugin.extend({
395
476
  );
396
477
 
397
478
  return Promise.resolve(registerAndConnectResult);
479
+ })
480
+ .catch((error) => {
481
+ const {
482
+ currentOwner: currentOwnerAfterRegisterFailure,
483
+ isOwner: isOwnerAfterRegisterFailure,
484
+ } = this.webex.internal.llm.resolveSessionOwnership(this.meetingId, LLM_PRACTICE_SESSION);
485
+
486
+ if (isOwnerAfterRegisterFailure) {
487
+ // @ts-ignore - Fix type
488
+ this.webex.internal.llm.setOwnerMeetingId?.(undefined, LLM_PRACTICE_SESSION);
489
+ } else {
490
+ LoggerProxy.logger.info(
491
+ `Webinar:index#updatePSDataChannel --> skipping failure owner release; practice-session LLM owned by meeting ${currentOwnerAfterRegisterFailure}, not ${this.meetingId}`
492
+ );
493
+ }
494
+
495
+ throw error;
398
496
  });
399
497
  },
400
498