@oxvo/ai-live-assist 7.4.23 → 7.4.36

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.
Files changed (53) hide show
  1. package/cjs/AiLiveAssist.d.ts +26 -2
  2. package/cjs/AiLiveAssist.js +640 -141
  3. package/cjs/avatarUrl.d.ts +2 -0
  4. package/cjs/avatarUrl.js +64 -0
  5. package/cjs/client.d.ts +1 -1
  6. package/cjs/client.js +14 -1
  7. package/cjs/index.js +2 -2
  8. package/cjs/locales/index.d.ts +10 -0
  9. package/cjs/locales/index.js +49 -0
  10. package/cjs/locales/resources.d.ts +3 -0
  11. package/cjs/locales/resources.js +4807 -0
  12. package/cjs/locales/supported.d.ts +2 -0
  13. package/cjs/locales/supported.js +47 -0
  14. package/cjs/media.d.ts +6 -3
  15. package/cjs/media.js +60 -45
  16. package/cjs/messages.js +9 -4
  17. package/cjs/preview.d.ts +4 -3
  18. package/cjs/preview.js +76 -20
  19. package/cjs/types.d.ts +13 -1
  20. package/cjs/types.js +121 -0
  21. package/cjs/ui.d.ts +40 -0
  22. package/cjs/ui.js +1091 -290
  23. package/cjs/version.d.ts +1 -1
  24. package/cjs/version.js +1 -1
  25. package/cjs/voicePresenceUi.d.ts +2 -1
  26. package/cjs/voicePresenceUi.js +115 -96
  27. package/lib/AiLiveAssist.d.ts +26 -2
  28. package/lib/AiLiveAssist.js +638 -140
  29. package/lib/avatarUrl.d.ts +2 -0
  30. package/lib/avatarUrl.js +60 -0
  31. package/lib/client.d.ts +1 -1
  32. package/lib/client.js +14 -1
  33. package/lib/index.js +2 -2
  34. package/lib/locales/index.d.ts +10 -0
  35. package/lib/locales/index.js +42 -0
  36. package/lib/locales/resources.d.ts +3 -0
  37. package/lib/locales/resources.js +4804 -0
  38. package/lib/locales/supported.d.ts +2 -0
  39. package/lib/locales/supported.js +44 -0
  40. package/lib/media.d.ts +6 -3
  41. package/lib/media.js +60 -45
  42. package/lib/messages.js +9 -4
  43. package/lib/preview.d.ts +4 -3
  44. package/lib/preview.js +76 -20
  45. package/lib/types.d.ts +13 -1
  46. package/lib/types.js +120 -1
  47. package/lib/ui.d.ts +40 -0
  48. package/lib/ui.js +1091 -290
  49. package/lib/version.d.ts +1 -1
  50. package/lib/version.js +1 -1
  51. package/lib/voicePresenceUi.d.ts +2 -1
  52. package/lib/voicePresenceUi.js +116 -97
  53. package/package.json +5 -3
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveAiGuideProtocolVersion = exports.normalizeAiGuideOpenInput = exports.confirmationReplyIntent = exports.ReplaySessionReadiness = exports.REPLAY_SESSION_READY_TIMEOUT_MS = void 0;
3
+ exports.resolveAiGuideProtocolVersion = exports.normalizeAiGuideOpenInput = exports.resolveAiGuideLocale = exports.confirmationReplyIntent = exports.ReplaySessionReadiness = exports.REPLAY_SESSION_READY_TIMEOUT_MS = void 0;
4
4
  const client_js_1 = require("./client.js");
5
5
  const control_js_1 = require("./control.js");
6
6
  const context_js_1 = require("./context.js");
@@ -78,6 +78,7 @@ const MESSENGER_RETURN_RELEASE_REQUEST_TIMEOUT_MS = 2500;
78
78
  const MESSENGER_RETURN_END_AUTHORITY_MAX_FUTURE_MS = 60 * 60 * 1000;
79
79
  const MESSENGER_BINDING_POLL_MS = 50;
80
80
  const HANDOFF_REQUEST_REPLAY_TIMEOUT_MS = 15000;
81
+ const MANUAL_HANDOFF_RECONNECT_REPLAY_LIMIT = 1;
81
82
  const MESSENGER_RETURN_EVENT = "oxvo:ai-guide-return-requested";
82
83
  const MESSENGER_SUPPORT_STATE_EVENT = "oxvo:support-state";
83
84
  const MESSENGER_SWITCH_ID_PATTERN = /^[0-9a-f]{32}$/;
@@ -461,12 +462,41 @@ const confirmationReplyIntent = (value) => {
461
462
  return null;
462
463
  };
463
464
  exports.confirmationReplyIntent = confirmationReplyIntent;
464
- const boundedLocale = (value) => {
465
- const normalized = value.replace("_", "-").slice(0, 16);
466
- return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/.test(normalized)
467
- ? normalized
468
- : "en";
465
+ const canonicalLocale = (value) => {
466
+ const normalized = value.trim().replace(/_/g, "-");
467
+ if (!normalized || normalized.length > 64)
468
+ return null;
469
+ try {
470
+ return Intl.getCanonicalLocales(normalized)[0] ?? null;
471
+ }
472
+ catch {
473
+ return null;
474
+ }
469
475
  };
476
+ const boundedLocale = (value) => canonicalLocale(value) ?? "en";
477
+ const resolveAiGuideLocale = (configured, requested) => {
478
+ const fallback = boundedLocale(configured?.default ?? "en");
479
+ if (!requested?.trim())
480
+ return fallback;
481
+ const candidate = canonicalLocale(requested);
482
+ if (!candidate)
483
+ return fallback;
484
+ const allowedByKey = new Map();
485
+ for (const locale of configured?.allowed ?? [fallback]) {
486
+ const canonical = canonicalLocale(locale);
487
+ if (canonical) {
488
+ allowedByKey.set(canonical.toLocaleLowerCase("en"), canonical);
489
+ }
490
+ }
491
+ const exact = allowedByKey.get(candidate.toLocaleLowerCase("en"));
492
+ if (exact)
493
+ return exact;
494
+ const baseLanguage = candidate.split("-")[0];
495
+ if (!baseLanguage || baseLanguage === candidate)
496
+ return fallback;
497
+ return allowedByKey.get(baseLanguage.toLocaleLowerCase("en")) ?? fallback;
498
+ };
499
+ exports.resolveAiGuideLocale = resolveAiGuideLocale;
470
500
  const path = () => window.location.pathname || "/";
471
501
  const randomId = (prefix) => `${prefix}_${crypto.randomUUID()}`;
472
502
  const normalizeAiGuideOpenInput = (input = {}) => {
@@ -631,7 +661,7 @@ const parseResumeRecord = (value) => {
631
661
  try {
632
662
  const record = JSON.parse(value);
633
663
  const scopes = Array.isArray(record.scopes) ? record.scopes : [];
634
- const locale = typeof record.locale === "string" ? record.locale.replace("_", "-") : "";
664
+ const locale = typeof record.locale === "string" ? canonicalLocale(record.locale) : null;
635
665
  if (!PUBLIC_REQUEST_ID_PATTERN.test(record.sessionId ?? "") ||
636
666
  !(0, client_js_1.isValidAssistSessionGrant)(record.resumeCredential) ||
637
667
  !["text", "voice"].includes(record.mode ?? "") ||
@@ -640,8 +670,7 @@ const parseResumeRecord = (value) => {
640
670
  scopes.some((scope) => typeof scope !== "string" ||
641
671
  !CONSENT_SCOPES.has(scope)) ||
642
672
  new Set(scopes).size !== scopes.length ||
643
- locale.length > 16 ||
644
- !/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/.test(locale) ||
673
+ !locale ||
645
674
  typeof record.expiresAt !== "number" ||
646
675
  !Number.isFinite(record.expiresAt) ||
647
676
  record.expiresAt <= Date.now()) {
@@ -1162,6 +1191,7 @@ class AiLiveAssist {
1162
1191
  this.messengerLoadPromise = null;
1163
1192
  this.messengerHandoffRequestInFlight = false;
1164
1193
  this.handoffRequestLifecycleOwner = null;
1194
+ this.pendingManualHandoffIntent = null;
1165
1195
  this.handoffRequestProgressTimer = null;
1166
1196
  this.messengerBindingChallengesInFlight = new Set();
1167
1197
  this.messengerBindingChallengesDelivered = new Set();
@@ -1281,6 +1311,8 @@ class AiLiveAssist {
1281
1311
  this.pageAssistanceRecoverySubmissionDigest = null;
1282
1312
  this.pageAssistanceRecoveryEffectTail = Promise.resolve();
1283
1313
  this.unsubscribePageAssistancePreference = null;
1314
+ this.pageAssistanceTrackerIdentity = null;
1315
+ this.pageAssistanceTrackerRotationPending = false;
1284
1316
  this.pageAssistanceAuthorityGeneration = 0;
1285
1317
  this.pageAssistanceBindingGeneration = "";
1286
1318
  this.minimumPageAssistanceAuthorityEpoch = 0;
@@ -1355,12 +1387,22 @@ class AiLiveAssist {
1355
1387
  this.handleMessengerReturnRequested(event);
1356
1388
  };
1357
1389
  this.onPageHide = (event) => {
1390
+ const recoveryWasBusy = !event.persisted && (this.reconnecting || this.reconnectTimer !== null);
1391
+ const resumeAtHide = recoveryWasBusy ? this.readResume() : null;
1392
+ const ordinaryRecoveryPending = recoveryWasBusy &&
1393
+ !resumeAtHide?.modeUpgrade &&
1394
+ this.recoveryBusy(resumeAtHide);
1358
1395
  this.clearReconnectTimer();
1359
1396
  if (this.terminalRequestPending || this.readTerminalEndIntent()) {
1360
1397
  this.parkTerminalRetry();
1361
1398
  this.cleanupTransport({ preserveConnectionRequest: true });
1362
- if (!event.persisted)
1363
- this.lock.release();
1399
+ return;
1400
+ }
1401
+ if (!event.persisted && ordinaryRecoveryPending) {
1402
+ // A real navigation cannot leave an outgoing resume authorization or
1403
+ // socket alive after giving another page the activity lock. BFCache
1404
+ // keeps the existing suspended-page behavior and resumes on pageshow.
1405
+ this.cleanupTransport();
1364
1406
  return;
1365
1407
  }
1366
1408
  if ((!this.result && !this.readResume()?.modeUpgrade) || this.ended)
@@ -1373,8 +1415,6 @@ class AiLiveAssist {
1373
1415
  this.messengerTerminalDeliveryObserved) {
1374
1416
  this.reconnecting = false;
1375
1417
  }
1376
- if (!event.persisted)
1377
- this.lock.release();
1378
1418
  };
1379
1419
  this.onPageShow = (event) => {
1380
1420
  if (this.restoreTerminalFromStorage()) {
@@ -1489,8 +1529,11 @@ class AiLiveAssist {
1489
1529
  this.terminalEndStorageKey = `__oxvo_ai_live_assist_terminal_end_${this.siteKey}`;
1490
1530
  this.messengerReturnEndStorageKey = `__oxvo_ai_live_assist_handoff_terminal_${this.siteKey}`;
1491
1531
  this.freshAiReturnProvisionalStorageKey = `__oxvo_ai_live_assist_fresh_return_${this.siteKey}`;
1532
+ this.pageAssistanceTrackerIdentityStorageKey = `__oxvo_ai_live_assist_page_assistance_tracker_${this.siteKey}`;
1492
1533
  this.transcript = this.readTranscript();
1493
1534
  this.visitorIdentity = this.resolveVisitorId();
1535
+ this.pageAssistanceTrackerIdentity =
1536
+ this.readPageAssistanceTrackerIdentity();
1494
1537
  const resume = this.readResume();
1495
1538
  this.tabId = resume?.tabId || app.session.getTabId() || randomId("tab");
1496
1539
  this.pageAssistanceBindingGeneration =
@@ -1503,10 +1546,12 @@ class AiLiveAssist {
1503
1546
  }
1504
1547
  this.windowId = randomId("window");
1505
1548
  this.terminalOffline = navigator.onLine === false;
1506
- this.locale = boundedLocale(options.locale ||
1507
- document.documentElement.lang ||
1508
- navigator.language ||
1509
- "en");
1549
+ this.locale =
1550
+ resume?.locale ??
1551
+ boundedLocale(options.locale ||
1552
+ navigator.language ||
1553
+ document.documentElement.lang ||
1554
+ "en");
1510
1555
  const endpoint = app.options.endpoint;
1511
1556
  const runtimeUrl = options.runtimeUrl ||
1512
1557
  (endpoint
@@ -1525,9 +1570,17 @@ class AiLiveAssist {
1525
1570
  this.bindWindowLifecycle();
1526
1571
  void this.initialize();
1527
1572
  }
1573
+ recoveryBusy(record = this.readResume()) {
1574
+ return (!this.ended &&
1575
+ !this.destroyed &&
1576
+ !this.terminalRequestPending &&
1577
+ (this.reconnecting || this.reconnectTimer !== null) &&
1578
+ Boolean(record || this.result || this.modeUpgradeTerminalResult));
1579
+ }
1528
1580
  open(input = {}) {
1529
- if (this.terminalRequestPending)
1581
+ if (this.terminalRequestPending || this.recoveryBusy()) {
1530
1582
  return this.getState();
1583
+ }
1531
1584
  const normalized = (0, exports.normalizeAiGuideOpenInput)(input);
1532
1585
  const requestedMode = this.result && !this.ended
1533
1586
  ? (this.lastLaunch?.mode ?? this.mode)
@@ -1581,6 +1634,8 @@ class AiLiveAssist {
1581
1634
  return this.getState();
1582
1635
  }
1583
1636
  close() {
1637
+ if (this.recoveryBusy())
1638
+ return;
1584
1639
  this.clearPendingIntentBeforeAuthorization();
1585
1640
  this.view?.close();
1586
1641
  if (!this.result &&
@@ -1591,18 +1646,23 @@ class AiLiveAssist {
1591
1646
  this.emitStateEvent();
1592
1647
  }
1593
1648
  getState() {
1649
+ const resume = this.readResume();
1650
+ const recovering = this.recoveryBusy(resume);
1594
1651
  const active = !this.ended &&
1595
1652
  Boolean(this.result ||
1596
1653
  this.modeUpgradeTerminalResult ||
1597
1654
  this.pendingSessionAuthorization ||
1598
- this.launchInFlight);
1655
+ this.launchInFlight ||
1656
+ recovering);
1599
1657
  return {
1600
1658
  ready: this.initialized,
1601
1659
  available: Boolean(this.bootstrap && this.view),
1602
1660
  open: this.view?.isOpen ?? false,
1603
1661
  active,
1604
1662
  mode: active
1605
- ? (this.lastLaunch?.mode ?? this.mode)
1663
+ ? (this.lastLaunch?.mode ??
1664
+ (recovering && !this.result ? resume?.mode : null) ??
1665
+ this.mode)
1606
1666
  : (this.pendingOpenIntent?.mode ?? null),
1607
1667
  phase: this.lifecyclePhase,
1608
1668
  companion: this.messengerIntegrationEnabled,
@@ -1738,7 +1798,7 @@ class AiLiveAssist {
1738
1798
  this.replayReadiness?.destroy();
1739
1799
  this.clearPendingIntent();
1740
1800
  this.discardPendingConfirmationDecisions();
1741
- window.removeEventListener("pagehide", this.onPageHide);
1801
+ window.removeEventListener("pagehide", this.onPageHide, true);
1742
1802
  window.removeEventListener("pageshow", this.onPageShow);
1743
1803
  window.removeEventListener("online", this.onOnline);
1744
1804
  window.removeEventListener("offline", this.onOffline);
@@ -1793,9 +1853,10 @@ class AiLiveAssist {
1793
1853
  this.bootstrapAbort = controller;
1794
1854
  const timeout = setTimeout(() => controller.abort(), Math.max(1000, Math.min(15000, this.options.bootstrapTimeoutMs ?? 15000)));
1795
1855
  try {
1796
- const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), controller.signal);
1856
+ const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), this.locale, controller.signal);
1797
1857
  if (this.destroyed)
1798
1858
  return;
1859
+ this.locale = (0, exports.resolveAiGuideLocale)(bootstrap.locale, this.locale);
1799
1860
  this.syncAgentDiscovery(bootstrap.agentAccess?.discoveryUrl ?? null);
1800
1861
  if (bootstrap.replayRecordingEnabled === false) {
1801
1862
  this.bootstrap = bootstrap;
@@ -1876,7 +1937,7 @@ class AiLiveAssist {
1876
1937
  (this.renderedTranscriptIds ?? (this.renderedTranscriptIds = new Set())).clear();
1877
1938
  this.transcriptRestoredFromCache = false;
1878
1939
  this.view = voicePresenceView
1879
- ? new voicePresenceUi_js_1.VoicePresenceView(bootstrap, this.options.messages ?? {}, callbacks, this.tabId, (fallback) => void this.activateFallback(fallback), this.options.visualTestMode === true)
1940
+ ? new voicePresenceUi_js_1.VoicePresenceView(effectiveBootstrap, this.options.messages ?? {}, callbacks, this.tabId, (fallback) => void this.activateFallback(fallback), this.options.visualTestMode === true)
1880
1941
  : new ui_js_1.WidgetView(effectiveBootstrap, this.options.messages ?? {}, callbacks);
1881
1942
  this.syncCompanionView();
1882
1943
  this.initializePageAssistancePreference(effectiveBootstrap);
@@ -2028,6 +2089,10 @@ class AiLiveAssist {
2028
2089
  if (this.queuedQuestions?.some((question) => question.submissionId === submissionId && question.shown)) {
2029
2090
  return true;
2030
2091
  }
2092
+ if ((this.renderedTranscriptIds ?? (this.renderedTranscriptIds = new Set())).has(submissionId) ||
2093
+ this.transcript?.some((entry) => entry.role === "user" && entry.id === submissionId)) {
2094
+ return true;
2095
+ }
2031
2096
  return (this.acknowledgedQuestionIds?.has(submissionId) === true ||
2032
2097
  this.failedQuestionIds?.has(submissionId) === true);
2033
2098
  }
@@ -2086,7 +2151,11 @@ class AiLiveAssist {
2086
2151
  performPendingOpen() {
2087
2152
  const intent = this.pendingOpenIntent;
2088
2153
  const view = this.view;
2089
- if (!intent || !view || this.destroyed || this.terminalRequestPending)
2154
+ if (!intent ||
2155
+ !view ||
2156
+ this.destroyed ||
2157
+ this.terminalRequestPending ||
2158
+ this.recoveryBusy())
2090
2159
  return;
2091
2160
  const unavailable = this.publicModeUnavailable(intent.mode);
2092
2161
  if (unavailable) {
@@ -2841,6 +2910,8 @@ class AiLiveAssist {
2841
2910
  if (!challengeId)
2842
2911
  return null;
2843
2912
  this.clearHandoffRequestProgressTimer();
2913
+ if (current.kind === "manual")
2914
+ this.pendingManualHandoffIntent = null;
2844
2915
  }
2845
2916
  const token = {};
2846
2917
  this.handoffRequestLifecycleOwner = challengeId
@@ -2857,6 +2928,9 @@ class AiLiveAssist {
2857
2928
  return false;
2858
2929
  const owner = this.handoffRequestLifecycleOwner;
2859
2930
  this.clearHandoffRequestProgressTimer();
2931
+ if (this.pendingManualHandoffIntent?.token === owner?.token) {
2932
+ this.pendingManualHandoffIntent = null;
2933
+ }
2860
2934
  this.handoffRequestLifecycleOwner = null;
2861
2935
  this.messengerHandoffRequestInFlight = false;
2862
2936
  if (owner?.kind === "challenge") {
@@ -2866,6 +2940,149 @@ class AiLiveAssist {
2866
2940
  this.view?.dismissHandoffRequestPreparing?.();
2867
2941
  return true;
2868
2942
  }
2943
+ manualHandoffLaunchGeneration() {
2944
+ return Number.isSafeInteger(this.launchGeneration)
2945
+ ? this.launchGeneration
2946
+ : 0;
2947
+ }
2948
+ manualHandoffIntentIsCurrent(intent) {
2949
+ const owner = this.handoffRequestLifecycleOwner;
2950
+ if (owner?.kind !== "manual" ||
2951
+ owner.token !== intent.token ||
2952
+ this.pendingManualHandoffIntent !== intent ||
2953
+ intent.launchGeneration !== this.manualHandoffLaunchGeneration() ||
2954
+ this.terminalRequestPending ||
2955
+ this.ended ||
2956
+ this.destroyed) {
2957
+ return false;
2958
+ }
2959
+ const currentSessionId = this.result?.sessionId ?? this.readResume()?.sessionId ?? null;
2960
+ return !intent.sessionId || !currentSessionId
2961
+ ? true
2962
+ : intent.sessionId === currentSessionId;
2963
+ }
2964
+ manualHandoffAuthorityReady(intent) {
2965
+ return (this.manualHandoffIntentIsCurrent(intent) &&
2966
+ this.result !== null &&
2967
+ this.channel?.isOpen === true &&
2968
+ this.channel.hasFencedAuthority &&
2969
+ (this.lifecyclePhase === "active_text" ||
2970
+ this.lifecyclePhase === "active_voice" ||
2971
+ this.lifecyclePhase === "paused"));
2972
+ }
2973
+ queueManualHandoff(reason) {
2974
+ const current = this.pendingManualHandoffIntent;
2975
+ if (current && this.manualHandoffIntentIsCurrent(current))
2976
+ return;
2977
+ const lifecycleToken = this.beginHandoffRequestLifecycle();
2978
+ if (!lifecycleToken)
2979
+ return;
2980
+ const requestId = randomId("handoff_request");
2981
+ this.pendingManualHandoffIntent = {
2982
+ reason,
2983
+ requestId,
2984
+ token: lifecycleToken,
2985
+ launchGeneration: this.manualHandoffLaunchGeneration(),
2986
+ sessionId: this.result?.sessionId ?? null,
2987
+ reconnectReplaysRemaining: MANUAL_HANDOFF_RECONNECT_REPLAY_LIMIT,
2988
+ awaitingReconnect: false,
2989
+ deliveryAttempt: 0,
2990
+ deliveryInFlight: false,
2991
+ };
2992
+ const owner = this.handoffRequestLifecycleOwner;
2993
+ if (owner?.kind !== "manual" || owner.token !== lifecycleToken)
2994
+ return;
2995
+ owner.requestId = requestId;
2996
+ if (this.handoffContextPromptExpected()) {
2997
+ this.view?.showHandoffRequestPreparing?.(HANDOFF_REQUEST_PREPARING_TITLE, HANDOFF_REQUEST_PREPARING_DESCRIPTION);
2998
+ }
2999
+ this.view?.setStatus(this.view.message("humanConnecting"));
3000
+ this.drainPendingManualHandoff();
3001
+ }
3002
+ drainPendingManualHandoff() {
3003
+ const intent = this.pendingManualHandoffIntent;
3004
+ if (!intent)
3005
+ return;
3006
+ if (!this.manualHandoffIntentIsCurrent(intent)) {
3007
+ this.finishHandoffRequestLifecycle(intent.token);
3008
+ return;
3009
+ }
3010
+ if (intent.awaitingReconnect &&
3011
+ (this.reconnecting || this.reconnectTimer !== null)) {
3012
+ return;
3013
+ }
3014
+ if (!this.manualHandoffAuthorityReady(intent))
3015
+ return;
3016
+ if (intent.deliveryInFlight)
3017
+ return;
3018
+ intent.sessionId ?? (intent.sessionId = this.result?.sessionId ?? null);
3019
+ intent.awaitingReconnect = false;
3020
+ intent.deliveryInFlight = true;
3021
+ intent.deliveryAttempt += 1;
3022
+ const deliveryAttempt = intent.deliveryAttempt;
3023
+ const delivery = this.sendControlOutcome("handoff.request", { reason: intent.reason }, intent.requestId, 5000);
3024
+ this.armHandoffRequestProgressTimer(intent.token);
3025
+ void this.observeManualHandoffDelivery(intent, deliveryAttempt, delivery);
3026
+ }
3027
+ async observeManualHandoffDelivery(intent, deliveryAttempt, delivery) {
3028
+ let outcome = null;
3029
+ try {
3030
+ outcome = await delivery;
3031
+ }
3032
+ catch {
3033
+ // The stable request identity makes an ambiguous transport outcome safe
3034
+ // to replay once after a newly fenced reconnect.
3035
+ }
3036
+ if (!this.manualHandoffIntentIsCurrent(intent) ||
3037
+ intent.deliveryAttempt !== deliveryAttempt) {
3038
+ return;
3039
+ }
3040
+ intent.deliveryInFlight = false;
3041
+ if (outcome?.status === "ack")
3042
+ return;
3043
+ if (outcome?.status === "explicit_error") {
3044
+ if (outcome.code === "LEASE_CONFLICT" && outcome.retryable) {
3045
+ this.requeueManualHandoffAfterReconnect(intent, outcome.message);
3046
+ }
3047
+ else {
3048
+ this.showRecoverableHandoffFailure(outcome.message, intent.token);
3049
+ }
3050
+ return;
3051
+ }
3052
+ this.requeueManualHandoffAfterReconnect(intent, this.view?.message("humanUnavailable") ?? "Human support is unavailable");
3053
+ }
3054
+ requeueManualHandoffAfterReconnect(intent, exhaustedMessage) {
3055
+ if (!this.manualHandoffIntentIsCurrent(intent))
3056
+ return;
3057
+ const owner = this.handoffRequestLifecycleOwner;
3058
+ if (owner?.kind !== "manual" || owner.token !== intent.token)
3059
+ return;
3060
+ if (intent.awaitingReconnect)
3061
+ return;
3062
+ if (intent.reconnectReplaysRemaining <= 0) {
3063
+ this.showRecoverableHandoffFailure(exhaustedMessage, intent.token);
3064
+ return;
3065
+ }
3066
+ intent.reconnectReplaysRemaining -= 1;
3067
+ intent.awaitingReconnect = true;
3068
+ intent.deliveryInFlight = false;
3069
+ this.clearHandoffRequestProgressTimer();
3070
+ this.view?.setStatus(this.view.message("humanConnecting"));
3071
+ this.scheduleReconnect(true);
3072
+ }
3073
+ handleManualHandoffLeaseConflict(requestId, message) {
3074
+ const intent = this.pendingManualHandoffIntent;
3075
+ const owner = this.handoffRequestLifecycleOwner;
3076
+ if (!intent ||
3077
+ owner?.kind !== "manual" ||
3078
+ owner.token !== intent.token ||
3079
+ owner.requestId !== requestId ||
3080
+ !this.manualHandoffIntentIsCurrent(intent)) {
3081
+ return false;
3082
+ }
3083
+ this.requeueManualHandoffAfterReconnect(intent, message);
3084
+ return true;
3085
+ }
2869
3086
  handoffContextPromptExpected() {
2870
3087
  const offered = new Set([
2871
3088
  ...(this.bootstrap?.consent?.requiredScopes ?? []),
@@ -2892,6 +3109,14 @@ class AiLiveAssist {
2892
3109
  return;
2893
3110
  }
2894
3111
  this.handoffRequestProgressTimer = null;
3112
+ if (owner.kind === "manual") {
3113
+ const intent = this.pendingManualHandoffIntent;
3114
+ if (intent?.token === ownerToken) {
3115
+ this.requeueManualHandoffAfterReconnect(intent, this.view?.message("humanUnavailable") ??
3116
+ "Human support is unavailable");
3117
+ }
3118
+ return;
3119
+ }
2895
3120
  // This is a replay checkpoint, not a terminal handoff deadline. Console
2896
3121
  // creation and its durable outbox can legitimately outlive this browser
2897
3122
  // interval. Once Messenger has an exact prepared-open proof, that
@@ -2900,11 +3125,59 @@ class AiLiveAssist {
2900
3125
  return;
2901
3126
  this.view?.setStatus(this.view.message("humanConnecting"));
2902
3127
  this.armHandoffRequestProgressTimer(ownerToken);
2903
- if (!this.channel?.isOpen || !this.channel.hasFencedAuthority) {
3128
+ if (owner.kind === "challenge" ||
3129
+ !this.channel?.isOpen ||
3130
+ !this.channel.hasFencedAuthority) {
2904
3131
  this.scheduleReconnect(true);
2905
3132
  }
2906
3133
  }, HANDOFF_REQUEST_REPLAY_TIMEOUT_MS);
2907
3134
  }
3135
+ rememberDeliveredMessengerBindingChallenge(challengeId) {
3136
+ this.messengerBindingChallengesDelivered.add(challengeId);
3137
+ if (this.messengerBindingChallengesDelivered.size <= 32)
3138
+ return;
3139
+ const oldest = this.messengerBindingChallengesDelivered
3140
+ .values()
3141
+ .next().value;
3142
+ if (oldest)
3143
+ this.messengerBindingChallengesDelivered.delete(oldest);
3144
+ }
3145
+ async observeMessengerBindingChallengeDelivery(challengeId, lifecycleToken, delivery) {
3146
+ let outcome = null;
3147
+ try {
3148
+ outcome = await delivery;
3149
+ }
3150
+ catch {
3151
+ // A thrown transport outcome is the same ambiguous delivery class as a
3152
+ // timeout: the persisted stable request may already be committed.
3153
+ }
3154
+ this.messengerBindingChallengesInFlight.delete(challengeId);
3155
+ if (!this.handoffRequestLifecycleIsCurrent(lifecycleToken))
3156
+ return;
3157
+ if (outcome?.status === "ack") {
3158
+ this.rememberDeliveredMessengerBindingChallenge(challengeId);
3159
+ return;
3160
+ }
3161
+ if (this.messengerTransferPending ||
3162
+ this.messengerTransferMaterialized() ||
3163
+ this.messengerTerminalDeliveryObserved) {
3164
+ this.rememberDeliveredMessengerBindingChallenge(challengeId);
3165
+ return;
3166
+ }
3167
+ if (outcome?.status === "explicit_error") {
3168
+ this.clearMessengerBindingChallenge(challengeId);
3169
+ this.showRecoverableHandoffFailure(outcome.message ||
3170
+ this.view?.message("humanUnavailable") ||
3171
+ "Human support is unavailable", lifecycleToken);
3172
+ return;
3173
+ }
3174
+ // The server may have committed this stable request without returning its
3175
+ // ACK. Reconnect even when the old socket still looks healthy so a newly
3176
+ // fenced owner can replay the exact durable challenge promptly.
3177
+ this.view?.setStatus(this.view.message("humanConnecting"));
3178
+ this.armHandoffRequestProgressTimer(lifecycleToken);
3179
+ this.scheduleReconnect(true);
3180
+ }
2908
3181
  async requestHumanHandoff(reason, messengerBindingChallengeId, challengeExpiresAt, isCurrent) {
2909
3182
  if (!isCurrent() ||
2910
3183
  this.terminalRequestPending ||
@@ -2912,32 +3185,29 @@ class AiLiveAssist {
2912
3185
  this.destroyed) {
2913
3186
  return;
2914
3187
  }
2915
- if (messengerBindingChallengeId) {
2916
- if (this.messengerBindingChallengesDelivered.has(messengerBindingChallengeId) ||
2917
- this.messengerBindingChallengesInFlight.has(messengerBindingChallengeId)) {
2918
- return;
2919
- }
3188
+ if (!messengerBindingChallengeId) {
3189
+ this.queueManualHandoff(reason);
3190
+ return;
3191
+ }
3192
+ if (this.messengerBindingChallengesDelivered.has(messengerBindingChallengeId) ||
3193
+ this.messengerBindingChallengesInFlight.has(messengerBindingChallengeId)) {
3194
+ return;
2920
3195
  }
2921
3196
  const lifecycleToken = this.beginHandoffRequestLifecycle(messengerBindingChallengeId);
2922
3197
  if (!lifecycleToken)
2923
3198
  return;
2924
- if (messengerBindingChallengeId) {
2925
- this.messengerBindingChallengesInFlight.add(messengerBindingChallengeId);
2926
- this.view?.dismissHandoffRequestPreparing?.();
2927
- }
2928
- else if (this.handoffContextPromptExpected()) {
2929
- this.view?.showHandoffRequestPreparing?.(HANDOFF_REQUEST_PREPARING_TITLE, HANDOFF_REQUEST_PREPARING_DESCRIPTION);
2930
- }
3199
+ this.messengerBindingChallengesInFlight.add(messengerBindingChallengeId);
3200
+ this.view?.dismissHandoffRequestPreparing?.();
2931
3201
  this.view?.setStatus(this.view.message("humanConnecting"));
2932
3202
  const payload = { reason };
2933
3203
  let requestDelivered = false;
2934
- let reconnectAfterRequest = false;
3204
+ let bindingDeliveryObserved = false;
2935
3205
  try {
2936
3206
  // The first request intentionally carries no provider receipt. The
2937
3207
  // server must issue a one-time binding challenge before it can accept
2938
3208
  // proof from this browser. Only the challenge response is allowed to
2939
3209
  // fetch and return the Messenger binding receipt.
2940
- if (this.messengerIntegrationEnabled && messengerBindingChallengeId) {
3210
+ if (this.messengerIntegrationEnabled) {
2941
3211
  const messengerBindingReceipt = await this.ensureMessengerBindingReceipt(challengeExpiresAt, isCurrent);
2942
3212
  if (!isCurrent() ||
2943
3213
  !this.handoffRequestLifecycleIsCurrent(lifecycleToken) ||
@@ -2947,8 +3217,8 @@ class AiLiveAssist {
2947
3217
  return;
2948
3218
  }
2949
3219
  if (!messengerBindingReceipt ||
2950
- (messengerBindingChallengeId &&
2951
- (!challengeExpiresAt || challengeExpiresAt <= Date.now()))) {
3220
+ !challengeExpiresAt ||
3221
+ challengeExpiresAt <= Date.now()) {
2952
3222
  const unavailableMessage = this.view?.message("humanUnavailable") ??
2953
3223
  "Human support is unavailable";
2954
3224
  this.showRecoverableHandoffFailure(!messengerBindingReceipt && this.messengerHandoffRequiresRefresh()
@@ -2963,44 +3233,10 @@ class AiLiveAssist {
2963
3233
  !this.handoffRequestLifecycleIsCurrent(lifecycleToken)) {
2964
3234
  return;
2965
3235
  }
2966
- let messageId;
2967
- if (messengerBindingChallengeId) {
2968
- // Persist the server correlation before sending. The server may accept
2969
- // the request even when the response is lost, so ACK-first persistence
2970
- // would make a reload unable to recognize its terminal replay.
2971
- if (!this.rememberMessengerBindingChallenge(messengerBindingChallengeId)) {
2972
- this.showRecoverableHandoffFailure(this.view?.message("humanUnavailable") ??
2973
- "Human support is unavailable", lifecycleToken);
2974
- return;
2975
- }
2976
- const lifecycleOwner = this.handoffRequestLifecycleOwner;
2977
- if (!lifecycleOwner || lifecycleOwner.token !== lifecycleToken)
2978
- return;
2979
- // Challenge responses use the challenge as their stable message ID.
2980
- // Persist the correlation before delivery because a timeout is
2981
- // ambiguous: the server may already own the durable handoff.
2982
- lifecycleOwner.requestId = messengerBindingChallengeId;
2983
- const delivery = await this.sendControlOutcome("handoff.request", payload, messengerBindingChallengeId, 10000);
2984
- if (delivery?.status !== "ack") {
2985
- if (delivery?.status === "explicit_error") {
2986
- this.clearMessengerBindingChallenge(messengerBindingChallengeId);
2987
- this.showRecoverableHandoffFailure(delivery.message ||
2988
- this.view?.message("humanUnavailable") ||
2989
- "Human support is unavailable", lifecycleToken);
2990
- }
2991
- else if (this.handoffRequestLifecycleIsCurrent(lifecycleToken)) {
2992
- requestDelivered = true;
2993
- reconnectAfterRequest = true;
2994
- this.armHandoffRequestProgressTimer(lifecycleToken);
2995
- }
2996
- return;
2997
- }
2998
- messageId = delivery.messageId;
2999
- }
3000
- else {
3001
- messageId = this.send("handoff.request", payload);
3002
- }
3003
- if (!messageId) {
3236
+ // Persist the server correlation before sending. The server may accept
3237
+ // the request even when the response is lost, so ACK-first persistence
3238
+ // would make a reload unable to recognize its terminal replay.
3239
+ if (!this.rememberMessengerBindingChallenge(messengerBindingChallengeId)) {
3004
3240
  this.showRecoverableHandoffFailure(this.view?.message("humanUnavailable") ??
3005
3241
  "Human support is unavailable", lifecycleToken);
3006
3242
  return;
@@ -3008,34 +3244,28 @@ class AiLiveAssist {
3008
3244
  const lifecycleOwner = this.handoffRequestLifecycleOwner;
3009
3245
  if (!lifecycleOwner || lifecycleOwner.token !== lifecycleToken)
3010
3246
  return;
3011
- lifecycleOwner.requestId = messageId;
3247
+ // Challenge responses use the challenge as their stable message ID.
3248
+ // Persist the correlation before delivery because a timeout is
3249
+ // ambiguous: the server may already own the durable handoff.
3250
+ lifecycleOwner.requestId = messengerBindingChallengeId;
3251
+ const delivery = this.sendControlOutcome("handoff.request", payload, messengerBindingChallengeId, 5000);
3252
+ // Do not serialize the prepared/open server event behind an ACK that
3253
+ // is emitted only after the durable Console handoff finishes. The
3254
+ // stable challenge ID and persisted correlation make this observation
3255
+ // safely replayable if the outcome is lost.
3012
3256
  requestDelivered = true;
3257
+ bindingDeliveryObserved = true;
3013
3258
  this.armHandoffRequestProgressTimer(lifecycleToken);
3014
- if (messageId && messengerBindingChallengeId) {
3015
- this.messengerBindingChallengesDelivered.add(messengerBindingChallengeId);
3016
- if (this.messengerBindingChallengesDelivered.size > 32) {
3017
- const oldest = this.messengerBindingChallengesDelivered
3018
- .values()
3019
- .next().value;
3020
- if (oldest)
3021
- this.messengerBindingChallengesDelivered.delete(oldest);
3022
- }
3023
- }
3259
+ void this.observeMessengerBindingChallengeDelivery(messengerBindingChallengeId, lifecycleToken, delivery);
3260
+ return;
3024
3261
  }
3025
3262
  finally {
3026
- if (messengerBindingChallengeId) {
3027
- if (isCurrent()) {
3028
- this.messengerBindingChallengesInFlight.delete(messengerBindingChallengeId);
3029
- }
3263
+ if (isCurrent() && !bindingDeliveryObserved) {
3264
+ this.messengerBindingChallengesInFlight.delete(messengerBindingChallengeId);
3030
3265
  }
3031
3266
  if (!requestDelivered) {
3032
3267
  this.finishHandoffRequestLifecycle(lifecycleToken);
3033
3268
  }
3034
- if (reconnectAfterRequest &&
3035
- this.handoffRequestLifecycleIsCurrent(lifecycleToken) &&
3036
- (!this.channel?.isOpen || !this.channel.hasFencedAuthority)) {
3037
- this.scheduleReconnect(true);
3038
- }
3039
3269
  }
3040
3270
  }
3041
3271
  showRecoverableHandoffFailure(message, lifecycleToken) {
@@ -3083,7 +3313,22 @@ class AiLiveAssist {
3083
3313
  try {
3084
3314
  const minimizing = this.view.minimizeForCompanionTransfer();
3085
3315
  if (minimizing) {
3086
- await minimizing;
3316
+ const morphBudgetMs = this.view.companionTransferMorphBudgetMs();
3317
+ await new Promise((resolve) => {
3318
+ let settled = false;
3319
+ const finish = () => {
3320
+ if (settled)
3321
+ return;
3322
+ settled = true;
3323
+ window.clearTimeout(timeout);
3324
+ resolve();
3325
+ };
3326
+ const timeout = window.setTimeout(finish, Math.max(1, morphBudgetMs));
3327
+ // A visual transition must never become protocol authority. If its
3328
+ // completion callback is lost or throws, the bounded deadline lets
3329
+ // the current transfer hide the AI surface and continue safely.
3330
+ minimizing.then(finish, finish);
3331
+ });
3087
3332
  if (!isCurrent())
3088
3333
  return false;
3089
3334
  }
@@ -3558,7 +3803,7 @@ class AiLiveAssist {
3558
3803
  }
3559
3804
  }
3560
3805
  const expectedMode = provisional?.mode ?? this.preferredCompanionMode();
3561
- if (mode !== expectedMode)
3806
+ if (provisional?.phase !== "released" && mode !== expectedMode)
3562
3807
  return;
3563
3808
  this.startMessengerReturn(mode, freshSession, switchId);
3564
3809
  }
@@ -3638,6 +3883,23 @@ class AiLiveAssist {
3638
3883
  expiresAt,
3639
3884
  };
3640
3885
  }
3886
+ releaseFreshAiReturnAfterPreRuntimeFailure(authority) {
3887
+ if (!this.messengerReturnLaunchIsCurrent(authority, authority.mode, authority.launchIdempotencyKey)) {
3888
+ return;
3889
+ }
3890
+ const provisional = this.freshAiReturnProvisionalFor(authority.conversationDisplayId);
3891
+ if (provisional?.phase !== "launching" ||
3892
+ provisional.sessionId !== null ||
3893
+ provisional.mode !== authority.mode ||
3894
+ provisional.launchIdempotencyKey !== authority.launchIdempotencyKey) {
3895
+ return;
3896
+ }
3897
+ this.persistFreshAiReturnProvisional({
3898
+ ...provisional,
3899
+ phase: "released",
3900
+ launchIdempotencyKey: null,
3901
+ });
3902
+ }
3641
3903
  quiesceProvisionalFreshAiReturn() {
3642
3904
  this.launchGeneration += 1;
3643
3905
  this.clearReconnectTimer();
@@ -3654,9 +3916,7 @@ class AiLiveAssist {
3654
3916
  return true;
3655
3917
  const resume = this.readResume();
3656
3918
  const result = this.result;
3657
- const sessionId = provisional.sessionId ??
3658
- (resume?.sessionId ?? null) ??
3659
- (result?.sessionId ?? null);
3919
+ const sessionId = provisional.sessionId ?? resume?.sessionId ?? result?.sessionId ?? null;
3660
3920
  if (!sessionId)
3661
3921
  return false;
3662
3922
  const authority = resume?.sessionId === sessionId
@@ -3739,8 +3999,9 @@ class AiLiveAssist {
3739
3999
  return "launch";
3740
4000
  const resume = this.readResume();
3741
4001
  const sessionId = provisional.sessionId ??
3742
- (resume?.sessionId ?? null) ??
3743
- (this.result?.sessionId ?? null);
4002
+ resume?.sessionId ??
4003
+ this.result?.sessionId ??
4004
+ null;
3744
4005
  if (!sessionId) {
3745
4006
  // Keep and reuse the exact launch identity. A launch whose response was
3746
4007
  // interrupted must never be replaced by a second fresh operation.
@@ -3961,10 +4222,30 @@ class AiLiveAssist {
3961
4222
  };
3962
4223
  if (!this.persistFreshAiReturnProvisional(launching))
3963
4224
  return false;
4225
+ const messengerDescriptor = this.currentMessengerDescriptor();
4226
+ const messengerReturnAuthority = this.messengerTransferredSurface &&
4227
+ this.companion &&
4228
+ messengerDescriptor &&
4229
+ this.resolveMessenger() === this.messengerTransferredSurface &&
4230
+ this.messengerMatchesDescriptor(this.messengerTransferredSurface, messengerDescriptor) &&
4231
+ this.messengerConversation?.inboxId === messengerDescriptor.inboxId
4232
+ ? {
4233
+ conversationDisplayId: conversationId,
4234
+ inboxId: messengerDescriptor.inboxId,
4235
+ linkId: this.companion.linkId,
4236
+ launchIdempotencyKey,
4237
+ mode,
4238
+ previousAiSessionId: this.companion.currentAiSessionId,
4239
+ surface: this.messengerTransferredSurface,
4240
+ }
4241
+ : null;
4242
+ if (!messengerReturnAuthority)
4243
+ return false;
3964
4244
  this.messengerReturnRetryRemaining = 1;
3965
4245
  await this.launch(mode, scopes, launchIdempotencyKey, {
3966
4246
  startMuted: mode === "voice",
3967
4247
  associateCompanion: false,
4248
+ messengerReturnAuthority,
3968
4249
  });
3969
4250
  this.messengerReturnRetryRemaining = null;
3970
4251
  const sessionId = this.result?.sessionId;
@@ -3988,7 +4269,7 @@ class AiLiveAssist {
3988
4269
  const controller = new AbortController();
3989
4270
  const timeout = setTimeout(() => controller.abort(), 5000);
3990
4271
  try {
3991
- const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), controller.signal);
4272
+ const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), this.locale, controller.signal);
3992
4273
  if (this.destroyed)
3993
4274
  return false;
3994
4275
  this.bootstrap = this.effectivePageAssistanceBootstrap(bootstrap);
@@ -5531,7 +5812,8 @@ class AiLiveAssist {
5531
5812
  return false;
5532
5813
  if (this.destroyed ||
5533
5814
  this.launchInFlight ||
5534
- this.terminalRequestPending) {
5815
+ this.terminalRequestPending ||
5816
+ this.recoveryBusy()) {
5535
5817
  return false;
5536
5818
  }
5537
5819
  if (initialQuestion) {
@@ -5560,7 +5842,10 @@ class AiLiveAssist {
5560
5842
  if (this.queuedLaunchToken !== queuedLaunchToken)
5561
5843
  return;
5562
5844
  this.queuedLaunchToken = null;
5563
- if (this.destroyed || this.ended || this.terminalRequestPending)
5845
+ if (this.destroyed ||
5846
+ this.ended ||
5847
+ this.terminalRequestPending ||
5848
+ this.recoveryBusy())
5564
5849
  return;
5565
5850
  void this.launch(mode, scopes, launchKey);
5566
5851
  });
@@ -5888,26 +6173,52 @@ class AiLiveAssist {
5888
6173
  }
5889
6174
  }
5890
6175
  async launch(mode, scopes, launchIdempotencyKey = (0, client_js_1.idempotencyKey)("launch"), options = {}) {
6176
+ let runtimeLaunchStarted = false;
6177
+ const releaseMessengerReturnIfPreRuntime = () => {
6178
+ if (options.messengerReturnAuthority && !runtimeLaunchStarted) {
6179
+ this.releaseFreshAiReturnAfterPreRuntimeFailure(options.messengerReturnAuthority);
6180
+ }
6181
+ };
5891
6182
  if (this.launchInFlight ||
5892
6183
  this.terminalRequestPending ||
5893
6184
  this.ended ||
5894
- this.destroyed)
6185
+ this.destroyed ||
6186
+ this.recoveryBusy()) {
6187
+ releaseMessengerReturnIfPreRuntime();
5895
6188
  return;
6189
+ }
5896
6190
  const launchGeneration = ++this.launchGeneration;
5897
6191
  const launchIsCurrent = () => launchGeneration === this.launchGeneration &&
5898
6192
  !this.terminalRequestPending &&
5899
6193
  !this.ended &&
5900
6194
  !this.destroyed;
5901
6195
  const bootstrap = this.bootstrap;
5902
- if (!bootstrap?.configRevision || !bootstrap.consent || !this.view)
6196
+ if (!bootstrap?.configRevision || !bootstrap.consent || !this.view) {
6197
+ releaseMessengerReturnIfPreRuntime();
5903
6198
  return;
6199
+ }
5904
6200
  const experienceMode = bootstrap.experienceMode ?? "standard";
5905
6201
  const voicePresence = experienceMode === "voice_presence";
5906
6202
  const protocolVersion = (0, exports.resolveAiGuideProtocolVersion)(bootstrap);
5907
6203
  if (voicePresence && mode !== "voice") {
5908
6204
  this.view.showError("Voice assist requires microphone access.", false, false);
6205
+ releaseMessengerReturnIfPreRuntime();
5909
6206
  return;
5910
6207
  }
6208
+ if (this.visitorActiveTakeoverOnly &&
6209
+ options.messengerReturnAuthority !== undefined) {
6210
+ if (!this.messengerReturnLaunchIsCurrent(options.messengerReturnAuthority, mode, launchIdempotencyKey)) {
6211
+ releaseMessengerReturnIfPreRuntime();
6212
+ return;
6213
+ }
6214
+ // A page reload can recover Messenger while bootstrap still reports the
6215
+ // AI session that was handed off as visitor-active. The authenticated
6216
+ // terminal release and exact return authority above make that marker
6217
+ // stale. Cancel its generic eligibility retry so it cannot later launch
6218
+ // a second session without the Messenger return authority.
6219
+ this.cancelVisitorActiveEligibilityRecheck(true);
6220
+ this.visitorActiveTakeoverOnly = false;
6221
+ }
5911
6222
  if (this.visitorActiveTakeoverOnly) {
5912
6223
  const pendingLaunch = {
5913
6224
  mode,
@@ -5922,6 +6233,7 @@ class AiLiveAssist {
5922
6233
  if (handoffRequested === false) {
5923
6234
  this.beginVisitorActiveEligibilityRecheck(pendingLaunch, launchGeneration);
5924
6235
  }
6236
+ releaseMessengerReturnIfPreRuntime();
5925
6237
  return;
5926
6238
  }
5927
6239
  this.pageAssistanceAuthorityGeneration += 1;
@@ -5936,6 +6248,7 @@ class AiLiveAssist {
5936
6248
  this.view.showActiveElsewhere(false);
5937
6249
  this.launchInFlight = false;
5938
6250
  this.requestFocusedHandoff();
6251
+ releaseMessengerReturnIfPreRuntime();
5939
6252
  return;
5940
6253
  }
5941
6254
  let activeMode = mode;
@@ -5961,7 +6274,7 @@ class AiLiveAssist {
5961
6274
  this.replayReadiness.assertCurrent(replay);
5962
6275
  const controller = new AbortController();
5963
6276
  const connectionGeneration = this.beginConnectionAttempt(controller);
5964
- const assertConnectionCurrent = () => this.assertConnectionCurrent(controller, connectionGeneration, replay);
6277
+ const assertConnectionCurrent = () => this.assertConnectionCurrent(controller, connectionGeneration, replay, options.messengerReturnAuthority, activeMode, launchIdempotencyKey);
5965
6278
  media = this.createMedia(() => {
5966
6279
  try {
5967
6280
  assertConnectionCurrent();
@@ -5986,7 +6299,8 @@ class AiLiveAssist {
5986
6299
  assertConnectionCurrent();
5987
6300
  if (voicePresence ||
5988
6301
  mode !== "voice" ||
5989
- !bootstrap.capabilities?.text) {
6302
+ !bootstrap.capabilities?.text ||
6303
+ options.messengerReturnAuthority !== undefined) {
5990
6304
  throw error;
5991
6305
  }
5992
6306
  this.releaseMedia(media);
@@ -6039,6 +6353,7 @@ class AiLiveAssist {
6039
6353
  return;
6040
6354
  }
6041
6355
  const requestedLaunchPath = path();
6356
+ runtimeLaunchStarted = true;
6042
6357
  const authorization = this.runtime.launch({
6043
6358
  siteKey: this.siteKey,
6044
6359
  configRevision: bootstrap.configRevision,
@@ -6141,6 +6456,7 @@ class AiLiveAssist {
6141
6456
  this.state("error");
6142
6457
  }
6143
6458
  finally {
6459
+ releaseMessengerReturnIfPreRuntime();
6144
6460
  this.launchInFlight = false;
6145
6461
  this.drainPendingFocusedHandoff();
6146
6462
  }
@@ -6156,7 +6472,7 @@ class AiLiveAssist {
6156
6472
  this.visitorLimitAbort = controller;
6157
6473
  const timeout = setTimeout(() => controller.abort(), 5000);
6158
6474
  try {
6159
- const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), controller.signal);
6475
+ const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), this.locale, controller.signal);
6160
6476
  if (!isCurrent() || controller.signal.aborted)
6161
6477
  return false;
6162
6478
  const effectiveBootstrap = this.effectivePageAssistanceBootstrap(bootstrap);
@@ -6277,7 +6593,7 @@ class AiLiveAssist {
6277
6593
  operation = (async () => {
6278
6594
  try {
6279
6595
  const bootstrap = await Promise.race([
6280
- this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), controller.signal),
6596
+ this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), this.locale, controller.signal),
6281
6597
  aborted,
6282
6598
  ]);
6283
6599
  if (!bootstrap) {
@@ -6620,9 +6936,20 @@ class AiLiveAssist {
6620
6936
  : modeUpgrade
6621
6937
  ? this.retryableModeUpgradeFailure(error, transportStarted)
6622
6938
  : this.retryableResumeFailure(error, transportStarted);
6939
+ const unrecoverableResume = this.unrecoverableResumeFailure(error);
6940
+ const durableResumeAtFailure = !retryableFailure && !modeUpgrade && unrecoverableResume
6941
+ ? this.readResume()
6942
+ : null;
6943
+ const supersededByNewerSessionCredential = Boolean(durableResumeAtFailure?.sessionId === record.sessionId &&
6944
+ !attemptedCredentials.has(durableResumeAtFailure.resumeCredential));
6623
6945
  const preparedMediaWasAttached = preparedModeUpgradeMedia !== null &&
6624
6946
  this.media === preparedModeUpgradeMedia;
6625
- this.cleanupTransport();
6947
+ const pendingManualHandoff = this.pendingManualHandoffIntent;
6948
+ this.cleanupTransport({
6949
+ preserveHandoffRequestLifecycle: Boolean((retryableFailure || supersededByNewerSessionCredential) &&
6950
+ pendingManualHandoff &&
6951
+ this.manualHandoffIntentIsCurrent(pendingManualHandoff)),
6952
+ });
6626
6953
  if (preparedModeUpgradeMedia && !preparedMediaWasAttached) {
6627
6954
  preparedModeUpgradeMedia.close();
6628
6955
  }
@@ -6650,7 +6977,6 @@ class AiLiveAssist {
6650
6977
  await this.endFailedModeUpgrade();
6651
6978
  return;
6652
6979
  }
6653
- const unrecoverableResume = this.unrecoverableResumeFailure(error);
6654
6980
  const retainedModeUpgradeAuthority = this.modeUpgradeTerminalResult;
6655
6981
  if (modeUpgrade &&
6656
6982
  !retryableFailure &&
@@ -6712,6 +7038,12 @@ class AiLiveAssist {
6712
7038
  if (ownsResume) {
6713
7039
  this.resumeAttemptOwner = null;
6714
7040
  this.reconnecting = false;
7041
+ // connectTransport reaches the authoritative active state while the
7042
+ // enclosing resume still owns the recovery flag. Drain only after
7043
+ // that flag is cleared so a deferred handoff is replayed exactly once
7044
+ // on the newly fenced channel instead of remaining stranded.
7045
+ if (connected)
7046
+ this.drainPendingManualHandoff();
6715
7047
  }
6716
7048
  if (ownsResume &&
6717
7049
  retryableFailure &&
@@ -7019,6 +7351,7 @@ class AiLiveAssist {
7019
7351
  this.reconcilePendingHandoffContext();
7020
7352
  if (this.lifecyclePhase !== "paused")
7021
7353
  this.state("paused");
7354
+ this.drainPendingManualHandoff();
7022
7355
  return;
7023
7356
  }
7024
7357
  const welcomeMessage = this.pendingHumanFallback
@@ -7050,6 +7383,7 @@ class AiLiveAssist {
7050
7383
  const activeState = mode === "voice" ? "active_voice" : "active_text";
7051
7384
  if (this.lifecyclePhase !== activeState)
7052
7385
  this.state(activeState);
7386
+ this.drainPendingManualHandoff();
7053
7387
  void this.flushPendingConfirmationDecision();
7054
7388
  void this.flushPendingQuestion();
7055
7389
  if (this.pendingHumanFallback) {
@@ -7720,10 +8054,7 @@ class AiLiveAssist {
7720
8054
  }
7721
8055
  parsePageAssistanceRequest(payload, sentAt = new Date().toISOString()) {
7722
8056
  const bootstrap = this.bootstrap;
7723
- if (this.resolveVisitorId() !== this.visitorIdentity) {
7724
- this.disposePageAssistancePreference();
7725
- this.view?.setPageAssistancePersistentAllowed(false);
7726
- }
8057
+ this.reconcilePageAssistanceTrackerIdentity(this.pageAssistancePreferenceStore, this.pageAssistancePreferenceStore !== null);
7727
8058
  if (!this.result ||
7728
8059
  !this.channel?.isOpen ||
7729
8060
  !versionAtLeast(version_js_1.VERSION, "7.4.0") ||
@@ -7907,7 +8238,8 @@ class AiLiveAssist {
7907
8238
  !this.pageAssistanceDecisionWindowOpen(pending.decisionDeadline)) {
7908
8239
  return;
7909
8240
  }
7910
- const saved = request.persistenceEligible &&
8241
+ const saved = !this.pageAssistanceTrackerRotationPending &&
8242
+ request.persistenceEligible &&
7911
8243
  PERSISTENT_PAGE_ASSISTANCE_CATEGORIES.has(request.actionCategory)
7912
8244
  ? (store?.read() ?? null)
7913
8245
  : null;
@@ -8803,6 +9135,9 @@ class AiLiveAssist {
8803
9135
  };
8804
9136
  }
8805
9137
  handleHandoffContextRequest(request) {
9138
+ // The authoritative prompt proves the stable manual request was accepted;
9139
+ // visitor decision expiry now owns the only remaining deadline.
9140
+ this.clearHandoffRequestProgressTimer();
8806
9141
  const current = this.pendingHandoffContext;
8807
9142
  if (current?.request.requestId === request.requestId &&
8808
9143
  current.request.requestChecksum === request.requestChecksum) {
@@ -9158,10 +9493,18 @@ class AiLiveAssist {
9158
9493
  !this.messengerTransferred &&
9159
9494
  !(this.messengerTransferPending && this.messengerTerminalDeliveryObserved)) {
9160
9495
  // Microphone authority loss must not wait behind a slower ordered
9161
- // presentation or page-action handler. The normal FIFO handler observes
9162
- // the already-committed mode and remains idempotent.
9496
+ // presentation or page-action handler. When this state belongs to our
9497
+ // pending mode command, stop capture immediately but wait for the
9498
+ // correlated ACK before sending another control frame or retiring the
9499
+ // transport. Otherwise the text-mode media update can collide with the
9500
+ // still-claimed mode command and force a revision-conflict reconnect.
9163
9501
  this.revokeActiveActionAtIngress();
9164
- this.commitTextModeActivation();
9502
+ if (this.pendingTextModeTransition) {
9503
+ this.revokePendingTextModeCapture();
9504
+ }
9505
+ else {
9506
+ this.commitTextModeActivation();
9507
+ }
9165
9508
  return;
9166
9509
  }
9167
9510
  if (sessionState === "ending") {
@@ -9842,6 +10185,10 @@ class AiLiveAssist {
9842
10185
  if (!lifecycleOwner || lifecycleOwner.requestId !== requestId)
9843
10186
  return;
9844
10187
  this.pendingAction = false;
10188
+ if (code === "LEASE_CONFLICT" &&
10189
+ this.handleManualHandoffLeaseConflict(requestId, message)) {
10190
+ return;
10191
+ }
9845
10192
  this.showRecoverableHandoffFailure(message, lifecycleOwner.token);
9846
10193
  return;
9847
10194
  }
@@ -9876,10 +10223,16 @@ class AiLiveAssist {
9876
10223
  if (!currentPauseObservation)
9877
10224
  this.pauseCommandGeneration += 1;
9878
10225
  this.applyAuthoritativePauseState(state);
10226
+ this.drainPendingManualHandoff();
9879
10227
  }
9880
10228
  if (state === "active_text" &&
9881
10229
  (this.mode === "voice" || this.pendingTextModeTransition)) {
9882
- this.commitTextModeActivation();
10230
+ if (this.pendingTextModeTransition) {
10231
+ this.revokePendingTextModeCapture();
10232
+ }
10233
+ else {
10234
+ this.commitTextModeActivation();
10235
+ }
9883
10236
  }
9884
10237
  if (TERMINAL_STATES.has(state)) {
9885
10238
  this.finishFromServer(this.string(payload.reasonCode), message.sessionId);
@@ -10014,6 +10367,12 @@ class AiLiveAssist {
10014
10367
  }
10015
10368
  const retryable = payload.retryable === true;
10016
10369
  if (code === "LEASE_CONFLICT" && retryable) {
10370
+ if (operation?.type === "handoff.request" &&
10371
+ this.handleManualHandoffLeaseConflict(operation.messageId, this.string(payload.message) ||
10372
+ this.view?.message("humanUnavailable") ||
10373
+ "Human support is unavailable")) {
10374
+ return;
10375
+ }
10017
10376
  // The backend only marks a missing lease or a newer fence owned by
10018
10377
  // this same tab retryable. Retire this transport and reconcile via
10019
10378
  // /resume without flashing a false cross-tab takeover error.
@@ -12663,7 +13022,7 @@ class AiLiveAssist {
12663
13022
  this.scheduleReconnect(true);
12664
13023
  return;
12665
13024
  }
12666
- const transition = { channel, messageId };
13025
+ const transition = { channel, messageId, captureRevoked: false };
12667
13026
  this.pendingTextModeTransition = transition;
12668
13027
  if (this.view instanceof ui_js_1.WidgetView)
12669
13028
  this.view.setActivity("connecting");
@@ -12690,6 +13049,13 @@ class AiLiveAssist {
12690
13049
  }
12691
13050
  this.commitTextModeActivation();
12692
13051
  }
13052
+ revokePendingTextModeCapture() {
13053
+ const transition = this.pendingTextModeTransition;
13054
+ if (!transition || transition.captureRevoked)
13055
+ return;
13056
+ transition.captureRevoked = true;
13057
+ this.media?.revokeLocalAudioCapture();
13058
+ }
12693
13059
  commitTextModeActivation() {
12694
13060
  this.pendingTextModeTransition = null;
12695
13061
  if (this.mode === "text" ||
@@ -13133,12 +13499,12 @@ class AiLiveAssist {
13133
13499
  : 0) + 1;
13134
13500
  return this.connectionGeneration;
13135
13501
  }
13136
- assertConnectionCurrent(controller, generation, replay) {
13502
+ assertConnectionCurrent(controller, generation, replay, messengerReturnAuthority, mode, launchIdempotencyKey) {
13137
13503
  if (controller.signal.aborted ||
13138
13504
  this.destroyed ||
13139
13505
  this.ended ||
13140
13506
  this.terminalRequestPending ||
13141
- this.messengerTransferred ||
13507
+ !this.messengerReturnLaunchIsCurrent(messengerReturnAuthority, mode, launchIdempotencyKey) ||
13142
13508
  (this.messengerTransferPending &&
13143
13509
  this.messengerTerminalDeliveryObserved) ||
13144
13510
  generation !== this.connectionGeneration) {
@@ -13146,6 +13512,48 @@ class AiLiveAssist {
13146
13512
  }
13147
13513
  this.replayReadiness.assertCurrent(replay);
13148
13514
  }
13515
+ messengerReturnLaunchIsCurrent(authority, mode, launchIdempotencyKey) {
13516
+ if (!this.messengerTransferred)
13517
+ return true;
13518
+ if (!authority)
13519
+ return false;
13520
+ const messenger = this.resolveMessenger();
13521
+ const descriptor = this.currentMessengerDescriptor();
13522
+ const provisional = this.freshAiReturnProvisionalFor(authority.conversationDisplayId);
13523
+ const returningSessionId = this.result?.sessionId ?? null;
13524
+ const provisionalIsCurrent = provisional?.phase === "launching"
13525
+ ? provisional.sessionId === null
13526
+ : provisional?.phase === "launched" &&
13527
+ provisional.sessionId !== null &&
13528
+ provisional.sessionId === returningSessionId;
13529
+ const companionIsCurrent = (this.companion?.activeSurface === "messenger" &&
13530
+ (this.companion.currentAiSessionId === authority.previousAiSessionId ||
13531
+ this.companion.currentAiSessionId === null)) ||
13532
+ (this.companion?.activeSurface === "ai" &&
13533
+ returningSessionId !== null &&
13534
+ this.companion.currentAiSessionId === returningSessionId);
13535
+ return Boolean(this.messengerReturnInFlight !== null &&
13536
+ messenger === authority.surface &&
13537
+ descriptor !== null &&
13538
+ descriptor.inboxId === authority.inboxId &&
13539
+ this.messengerMatchesDescriptor(authority.surface, descriptor) &&
13540
+ this.messengerTransferredConversationId ===
13541
+ authority.conversationDisplayId &&
13542
+ this.messengerTransferredSurface === authority.surface &&
13543
+ this.messengerReleasedReturnConversationId ===
13544
+ authority.conversationDisplayId &&
13545
+ companionIsCurrent &&
13546
+ this.companion?.linkId === authority.linkId &&
13547
+ this.messengerConversation?.conversationDisplayId ===
13548
+ authority.conversationDisplayId &&
13549
+ this.messengerConversation?.inboxId === authority.inboxId &&
13550
+ authority.mode === mode &&
13551
+ authority.launchIdempotencyKey === launchIdempotencyKey &&
13552
+ provisionalIsCurrent &&
13553
+ provisional?.mode === mode &&
13554
+ provisional?.launchIdempotencyKey === authority.launchIdempotencyKey &&
13555
+ provisional?.linkId === authority.linkId);
13556
+ }
13149
13557
  modeUpgradePathIsCurrent(boundPath) {
13150
13558
  const authoritativePath = this.authoritativeSessionPath;
13151
13559
  return (path() === boundPath &&
@@ -13716,10 +14124,11 @@ class AiLiveAssist {
13716
14124
  const current = this.bootstrap;
13717
14125
  if (current?.configRevision && current.consent)
13718
14126
  return current;
13719
- const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), signal);
14127
+ const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), this.locale, signal);
13720
14128
  if (!bootstrap.configRevision || !bootstrap.consent) {
13721
14129
  throw new client_js_1.RuntimeClientError("INTERNAL_UNAVAILABLE", "Terminal launch recovery metadata is temporarily unavailable.", true, null, 503);
13722
14130
  }
14131
+ this.locale = (0, exports.resolveAiGuideLocale)(bootstrap.locale, this.locale);
13723
14132
  this.bootstrap = bootstrap;
13724
14133
  return bootstrap;
13725
14134
  }
@@ -14077,12 +14486,16 @@ class AiLiveAssist {
14077
14486
  }
14078
14487
  this.pageAssistancePreferenceStore = store;
14079
14488
  if (!store) {
14489
+ this.reconcilePageAssistanceTrackerIdentity(null, true);
14080
14490
  this.view?.setPageAssistancePersistentAllowed(false);
14081
14491
  return;
14082
14492
  }
14083
14493
  this.unsubscribePageAssistancePreference = store.subscribe((receipt, previous) => this.handlePageAssistancePreferenceChange(receipt, previous));
14494
+ this.reconcilePageAssistanceTrackerIdentity(store, true);
14084
14495
  const pendingRevocation = this.restorePendingPageAssistanceRevocation();
14085
- this.view?.setPageAssistancePersistentAllowed(Boolean(store.read()) && !pendingRevocation);
14496
+ this.view?.setPageAssistancePersistentAllowed(Boolean(store.read()) &&
14497
+ !pendingRevocation &&
14498
+ !this.pageAssistanceTrackerRotationPending);
14086
14499
  if (pendingRevocation) {
14087
14500
  this.invalidatePageAssistanceAuthority(false);
14088
14501
  void this.flushPendingPageAssistanceRevocation();
@@ -14121,22 +14534,105 @@ class AiLiveAssist {
14121
14534
  visitorId() {
14122
14535
  return this.visitorIdentity;
14123
14536
  }
14537
+ currentTrackerVisitorIdentity() {
14538
+ try {
14539
+ const meta = this.app.getSessionMeta();
14540
+ const trackerId = typeof meta?.userUUID === "string" ? meta.userUUID : "";
14541
+ return PUBLIC_REQUEST_ID_PATTERN.test(trackerId) ? trackerId : null;
14542
+ }
14543
+ catch {
14544
+ return null;
14545
+ }
14546
+ }
14547
+ readPageAssistanceTrackerIdentity() {
14548
+ try {
14549
+ const stored = localStorage.getItem(this.pageAssistanceTrackerIdentityStorageKey) ??
14550
+ "";
14551
+ return PUBLIC_REQUEST_ID_PATTERN.test(stored) ? stored : null;
14552
+ }
14553
+ catch {
14554
+ return null;
14555
+ }
14556
+ }
14557
+ persistPageAssistanceTrackerIdentity(identity) {
14558
+ try {
14559
+ localStorage.setItem(this.pageAssistanceTrackerIdentityStorageKey, identity);
14560
+ return (localStorage.getItem(this.pageAssistanceTrackerIdentityStorageKey) ===
14561
+ identity);
14562
+ }
14563
+ catch {
14564
+ return false;
14565
+ }
14566
+ }
14567
+ clearPageAssistancePreferenceForTrackerRotation(store) {
14568
+ const previous = store.read();
14569
+ if (previous)
14570
+ this.rememberPendingPageAssistanceRevocation(previous);
14571
+ this.suppressPageAssistancePreferenceChange = true;
14572
+ try {
14573
+ store.clear();
14574
+ }
14575
+ finally {
14576
+ this.suppressPageAssistancePreferenceChange = false;
14577
+ }
14578
+ this.invalidatePageAssistanceAuthority(true);
14579
+ this.view?.setPageAssistancePersistentAllowed(false);
14580
+ if (previous)
14581
+ void this.flushPendingPageAssistanceRevocation();
14582
+ return store.read() === null;
14583
+ }
14584
+ reconcilePageAssistanceTrackerIdentity(store, permissionStateKnown) {
14585
+ const current = this.currentTrackerVisitorIdentity();
14586
+ if (!current)
14587
+ return;
14588
+ if (current === this.pageAssistanceTrackerIdentity) {
14589
+ this.pageAssistanceTrackerRotationPending = false;
14590
+ return;
14591
+ }
14592
+ const previous = this.pageAssistanceTrackerIdentity;
14593
+ if (!permissionStateKnown) {
14594
+ if (previous) {
14595
+ this.pageAssistanceTrackerRotationPending = true;
14596
+ this.view?.setPageAssistancePersistentAllowed(false);
14597
+ }
14598
+ return;
14599
+ }
14600
+ const saved = store?.read() ?? null;
14601
+ const requiresRevocation = previous !== null || saved !== null;
14602
+ if (requiresRevocation) {
14603
+ this.pageAssistanceTrackerRotationPending = true;
14604
+ const cleared = store
14605
+ ? this.clearPageAssistancePreferenceForTrackerRotation(store)
14606
+ : true;
14607
+ if (!store) {
14608
+ this.invalidatePageAssistanceAuthority(true);
14609
+ this.view?.setPageAssistancePersistentAllowed(false);
14610
+ }
14611
+ if (!cleared)
14612
+ return;
14613
+ }
14614
+ this.pageAssistanceTrackerIdentity = current;
14615
+ this.persistPageAssistanceTrackerIdentity(current);
14616
+ this.pageAssistanceTrackerRotationPending = false;
14617
+ }
14124
14618
  resolveVisitorId() {
14125
14619
  const key = `__oxvo_ai_live_assist_visitor_${this.siteKey}`;
14126
- const meta = this.app.getSessionMeta();
14127
- const trackerId = typeof meta.userUUID === "string" ? meta.userUUID : "";
14128
- if (/^[A-Za-z0-9_.:-]{8,128}$/.test(trackerId))
14129
- return trackerId;
14130
14620
  try {
14131
14621
  const stored = localStorage.getItem(key) ?? "";
14132
- if (/^[A-Za-z0-9_.:-]{8,128}$/.test(stored))
14622
+ if (PUBLIC_REQUEST_ID_PATTERN.test(stored))
14133
14623
  return stored;
14134
- const created = randomId("visitor");
14135
- localStorage.setItem(key, created);
14136
- return created;
14137
14624
  }
14138
14625
  catch {
14139
- return randomId("visitor");
14626
+ /* Fall through to the tracker identity or an in-memory fallback. */
14627
+ }
14628
+ const resolved = this.currentTrackerVisitorIdentity() ?? randomId("visitor");
14629
+ try {
14630
+ localStorage.setItem(key, resolved);
14631
+ const stored = localStorage.getItem(key) ?? "";
14632
+ return PUBLIC_REQUEST_ID_PATTERN.test(stored) ? stored : resolved;
14633
+ }
14634
+ catch {
14635
+ return resolved;
14140
14636
  }
14141
14637
  }
14142
14638
  readTranscript() {
@@ -14966,7 +15462,10 @@ class AiLiveAssist {
14966
15462
  return connection?.saveData === true || connection?.effectiveType === "2g";
14967
15463
  }
14968
15464
  bindWindowLifecycle() {
14969
- window.addEventListener("pagehide", this.onPageHide);
15465
+ // Run transport cleanup before TabActivityLock's own pagehide listener
15466
+ // releases cross-tab authority. Pagehide targets Window, where capture
15467
+ // listeners are invoked before the lock's non-capture listener.
15468
+ window.addEventListener("pagehide", this.onPageHide, true);
14970
15469
  window.addEventListener("pageshow", this.onPageShow);
14971
15470
  window.addEventListener("online", this.onOnline);
14972
15471
  window.addEventListener("offline", this.onOffline);