@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.
- package/cjs/AiLiveAssist.d.ts +26 -2
- package/cjs/AiLiveAssist.js +640 -141
- package/cjs/avatarUrl.d.ts +2 -0
- package/cjs/avatarUrl.js +64 -0
- package/cjs/client.d.ts +1 -1
- package/cjs/client.js +14 -1
- package/cjs/index.js +2 -2
- package/cjs/locales/index.d.ts +10 -0
- package/cjs/locales/index.js +49 -0
- package/cjs/locales/resources.d.ts +3 -0
- package/cjs/locales/resources.js +4807 -0
- package/cjs/locales/supported.d.ts +2 -0
- package/cjs/locales/supported.js +47 -0
- package/cjs/media.d.ts +6 -3
- package/cjs/media.js +60 -45
- package/cjs/messages.js +9 -4
- package/cjs/preview.d.ts +4 -3
- package/cjs/preview.js +76 -20
- package/cjs/types.d.ts +13 -1
- package/cjs/types.js +121 -0
- package/cjs/ui.d.ts +40 -0
- package/cjs/ui.js +1091 -290
- package/cjs/version.d.ts +1 -1
- package/cjs/version.js +1 -1
- package/cjs/voicePresenceUi.d.ts +2 -1
- package/cjs/voicePresenceUi.js +115 -96
- package/lib/AiLiveAssist.d.ts +26 -2
- package/lib/AiLiveAssist.js +638 -140
- package/lib/avatarUrl.d.ts +2 -0
- package/lib/avatarUrl.js +60 -0
- package/lib/client.d.ts +1 -1
- package/lib/client.js +14 -1
- package/lib/index.js +2 -2
- package/lib/locales/index.d.ts +10 -0
- package/lib/locales/index.js +42 -0
- package/lib/locales/resources.d.ts +3 -0
- package/lib/locales/resources.js +4804 -0
- package/lib/locales/supported.d.ts +2 -0
- package/lib/locales/supported.js +44 -0
- package/lib/media.d.ts +6 -3
- package/lib/media.js +60 -45
- package/lib/messages.js +9 -4
- package/lib/preview.d.ts +4 -3
- package/lib/preview.js +76 -20
- package/lib/types.d.ts +13 -1
- package/lib/types.js +120 -1
- package/lib/ui.d.ts +40 -0
- package/lib/ui.js +1091 -290
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/voicePresenceUi.d.ts +2 -1
- package/lib/voicePresenceUi.js +116 -97
- package/package.json +5 -3
package/cjs/AiLiveAssist.js
CHANGED
|
@@ -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
|
|
465
|
-
const normalized = value.replace(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1363
|
-
|
|
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 =
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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 ??
|
|
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(
|
|
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 ||
|
|
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 (
|
|
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
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
-
|
|
2925
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2951
|
-
|
|
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
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3015
|
-
|
|
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 (
|
|
3027
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3743
|
-
|
|
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 ||
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
9162
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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()) &&
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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);
|