@crestapps/ai-chat-ui 2.0.0-preview.170 → 2.0.0-preview.173

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.
@@ -762,6 +762,13 @@
762
762
  // network where WebRTC cannot work pays the full connection timeout — and a second microphone prompt —
763
763
  // at the start of every single conversation.
764
764
  var REALTIME_WEBRTC_BLOCKED_KEY = 'coreai.realtime.webrtcBlocked';
765
+ // The ICE servers resolved for the conversation currently starting. Scoped to one attempt, not to the page:
766
+ // TURN credentials are short-lived, so every conversation resolves its own set — but within an attempt the
767
+ // transport decision and the peer must see the same servers, and one hub round-trip is enough.
768
+ var realtimeAttemptIceServers = null;
769
+ // Identifies the conversation currently starting, so an ICE resolution that lands after the user gave up
770
+ // (or started again) cannot open a session for an attempt that no longer exists.
771
+ var realtimeAttemptToken = 0;
765
772
  var realtimeSawRelayCandidate = false;
766
773
  // WebRTC half-duplex echo guard: the shared microphone gate (see createMicGate) watches both the mic and
767
774
  // the assistant's remote audio and silences the outbound track unless the user is genuinely speaking, so
@@ -1378,10 +1385,12 @@
1378
1385
  console.info('The realtime voice session ended (' + reason + ').');
1379
1386
  }
1380
1387
 
1381
- // An idle close is deliberate and recoverable, so say so and offer to pick the conversation back up
1382
- // rather than leaving the user staring at a button that silently stopped working.
1388
+ // An idle or duration close is deliberate and recoverable, so say so and offer to pick the conversation
1389
+ // back up rather than leaving the user staring at a button that silently stopped working.
1383
1390
  if (reason === 'idle') {
1384
1391
  showRealtimeStatus(localize('endedIdle', 'Voice paused after a period of silence.'), true);
1392
+ } else if (reason === 'max_duration') {
1393
+ showRealtimeStatus(localize('endedMaxDuration', 'Voice paused — this session reached its time limit. You can start another.'), true);
1385
1394
  } else if (reason === 'disconnected') {
1386
1395
  showRealtimeStatus(localize('endedDisconnected', 'Voice session ended — the connection dropped.'), true);
1387
1396
  } else if (reason === 'error') {
@@ -1441,28 +1450,63 @@
1441
1450
  if (webRtcIceServers) {
1442
1451
  return Promise.resolve(webRtcIceServers);
1443
1452
  }
1453
+ if (realtimeAttemptIceServers) {
1454
+ return Promise.resolve(realtimeAttemptIceServers);
1455
+ }
1444
1456
  if (!connection || typeof connection.invoke !== 'function') {
1445
1457
  return Promise.resolve(DEFAULT_ICE_SERVERS);
1446
1458
  }
1447
1459
  return connection.invoke('GetRealtimeIceServers').then(function (servers) {
1448
- return Array.isArray(servers) && servers.length ? servers : DEFAULT_ICE_SERVERS;
1460
+ realtimeAttemptIceServers = Array.isArray(servers) && servers.length ? servers : DEFAULT_ICE_SERVERS;
1461
+ return realtimeAttemptIceServers;
1449
1462
  })["catch"](function (err) {
1450
1463
  if (window.console && console.warn) {
1451
1464
  console.warn('Could not resolve the realtime ICE servers; using the default STUN server.', err);
1452
1465
  }
1453
- return DEFAULT_ICE_SERVERS;
1466
+ realtimeAttemptIceServers = DEFAULT_ICE_SERVERS;
1467
+ return realtimeAttemptIceServers;
1454
1468
  });
1455
1469
  }
1456
- function isWebRtcKnownBlocked() {
1470
+
1471
+ // Identifies an ICE configuration by its server URLs, deliberately ignoring usernames and credentials:
1472
+ // ephemeral TURN credentials (Cloudflare, coturn HMAC) are reissued for every session, so including them
1473
+ // would change the fingerprint on each attempt and make the remembered failure worthless. Adding, removing
1474
+ // or repointing a STUN/TURN server does change it — which is the case that must invalidate the memory.
1475
+ function iceServersFingerprint(servers) {
1476
+ var urls = [];
1457
1477
  try {
1458
- return window.sessionStorage.getItem(REALTIME_WEBRTC_BLOCKED_KEY) === '1';
1478
+ (servers || []).forEach(function (server) {
1479
+ var list = server && server.urls;
1480
+ if (typeof list === 'string') {
1481
+ urls.push(list);
1482
+ } else if (Array.isArray(list)) {
1483
+ list.forEach(function (url) {
1484
+ if (url) {
1485
+ urls.push(url);
1486
+ }
1487
+ });
1488
+ }
1489
+ });
1490
+ } catch (err) {
1491
+ return 'none';
1492
+ }
1493
+ return urls.length ? urls.sort().join('|') : 'none';
1494
+ }
1495
+
1496
+ // A remembered failure only applies to the ICE configuration that produced it. Anything else strands a
1497
+ // deployment that has since been given a TURN server on the WebSocket transport until every open tab is
1498
+ // closed, with no way for the user to tell why. Values written by older builds ('1') match no fingerprint,
1499
+ // so upgrading clears the flag on its own.
1500
+ function isWebRtcKnownBlocked(servers) {
1501
+ try {
1502
+ return window.sessionStorage.getItem(REALTIME_WEBRTC_BLOCKED_KEY) === iceServersFingerprint(servers);
1459
1503
  } catch (err) {
1460
1504
  return false;
1461
1505
  }
1462
1506
  }
1463
1507
  function rememberWebRtcBlocked() {
1464
1508
  try {
1465
- window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY, '1');
1509
+ window.sessionStorage.setItem(REALTIME_WEBRTC_BLOCKED_KEY, iceServersFingerprint(realtimeAttemptIceServers || webRtcIceServers));
1466
1510
  } catch (err) {}
1467
1511
  }
1468
1512
 
@@ -1503,6 +1547,8 @@
1503
1547
  return;
1504
1548
  }
1505
1549
  applyRealtimeAudioPrefs(loadRealtimeAudioPrefs());
1550
+ realtimeAttemptIceServers = null;
1551
+ var attempt = ++realtimeAttemptToken;
1506
1552
  realtimeFellBack = false;
1507
1553
  realtimeSessionReady = false;
1508
1554
  realtimeEndedNotice = null;
@@ -1510,20 +1556,34 @@
1510
1556
  bindRealtimeLifecycleHandlers();
1511
1557
  setRealtimeState('requesting-mic');
1512
1558
 
1559
+ // Starting on WebSocket without even attempting WebRTC is a deployment fact worth stating once. The
1560
+ // console is otherwise indistinguishable from a healthy WebRTC session, so "no warning" gets read as
1561
+ // "WebRTC is working" when it can equally mean the server never offered the transport at all.
1562
+ if (!webRtcEnabled) {
1563
+ logWebSocketTransportReason('the server did not advertise the WebRTC transport');
1564
+ startRealtimeWebSocketConversation();
1565
+ return;
1566
+ }
1567
+
1513
1568
  // Prefer the WebRTC transport when the server advertises it: the browser's echo canceller references
1514
1569
  // the assistant's media track, so the model can ignore its own voice with the mic open (open rooms).
1515
1570
  // If the peer cannot connect (blocked UDP, no TURN, unsupported), we fall back to WebSocket at connect
1516
1571
  // time — see fallbackToWebSocket. The decision is made once, before the session starts.
1517
- if (webRtcEnabled && !isWebRtcKnownBlocked()) {
1572
+ //
1573
+ // Resolving the ICE servers before choosing a transport costs no extra round-trip — the peer reuses
1574
+ // realtimeAttemptIceServers — and it is what lets a remembered failure be scoped to the configuration
1575
+ // that actually caused it.
1576
+ resolveIceServers().then(function (servers) {
1577
+ if (attempt !== realtimeAttemptToken || !isRealtimeMode || isRealtimeActive) {
1578
+ return;
1579
+ }
1580
+ if (isWebRtcKnownBlocked(servers)) {
1581
+ logWebSocketTransportReason('a WebRTC attempt already failed earlier in this browser session with the same ICE configuration');
1582
+ startRealtimeWebSocketConversation();
1583
+ return;
1584
+ }
1518
1585
  startRealtimeWebRtcConversation();
1519
- return;
1520
- }
1521
-
1522
- // Starting on WebSocket without even attempting WebRTC is a deployment fact worth stating once. The
1523
- // console is otherwise indistinguishable from a healthy WebRTC session, so "no warning" gets read as
1524
- // "WebRTC is working" when it can equally mean the server never offered the transport at all.
1525
- logWebSocketTransportReason(webRtcEnabled ? 'a WebRTC attempt already failed earlier in this browser session' : 'the server did not advertise the WebRTC transport');
1526
- startRealtimeWebSocketConversation();
1586
+ });
1527
1587
  }
1528
1588
  function startRealtimeWebSocketConversation() {
1529
1589
  if (webRtcEnabled) {