@oxvo/ai-live-assist 7.4.37 → 7.4.49

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.
@@ -558,10 +558,72 @@ const versionAtLeast = (current, minimum) => {
558
558
  };
559
559
  const resolveAiGuideProtocolVersion = (bootstrap) => bootstrap.experienceMode === "voice_presence" ||
560
560
  (bootstrap.minimumProtocolVersion ?? 1) >= 2 ||
561
- bootstrap.protocol.min >= 2
561
+ bootstrap.protocol.min >= 2 ||
562
+ bootstrap.protocol.max >= 2
562
563
  ? 2
563
564
  : 1;
564
565
  exports.resolveAiGuideProtocolVersion = resolveAiGuideProtocolVersion;
566
+ const resumeProtocolIsSupported = (record, bootstrap) => {
567
+ const experienceMode = bootstrap.experienceMode ?? "standard";
568
+ if (record.experienceMode !== experienceMode)
569
+ return false;
570
+ if (record.protocolVersion === (0, exports.resolveAiGuideProtocolVersion)(bootstrap)) {
571
+ return true;
572
+ }
573
+ // Prefer V2 for fresh standard launches without abandoning a V1 session
574
+ // while the published configuration and runtime still admit V1.
575
+ return (record.experienceMode === "standard" &&
576
+ record.protocolVersion === 1 &&
577
+ (bootstrap.minimumProtocolVersion ?? 1) <= 1 &&
578
+ bootstrap.protocol.min <= 1 &&
579
+ bootstrap.protocol.max >= 1);
580
+ };
581
+ const protocolVersionIsAdmitted = (protocolVersion, bootstrap) => protocolVersion >= (bootstrap.minimumProtocolVersion ?? 1) &&
582
+ protocolVersion >= bootstrap.protocol.min &&
583
+ protocolVersion <= bootstrap.protocol.max;
584
+ const pendingLaunchIsBound = (pendingLaunch) => (pendingLaunch.protocolVersion === 1 ||
585
+ pendingLaunch.protocolVersion === 2) &&
586
+ (pendingLaunch.experienceMode === "standard" ||
587
+ pendingLaunch.experienceMode === "voice_presence");
588
+ const bindPendingLaunchIntent = (pendingLaunch, bootstrap) => {
589
+ const effectiveExperienceMode = bootstrap.experienceMode ?? "standard";
590
+ let protocolVersion = pendingLaunch.protocolVersion;
591
+ let experienceMode = pendingLaunch.experienceMode;
592
+ const hasProtocolVersion = protocolVersion === 1 || protocolVersion === 2;
593
+ const hasExperienceMode = experienceMode === "standard" || experienceMode === "voice_presence";
594
+ if (hasProtocolVersion !== hasExperienceMode)
595
+ return null;
596
+ if (!hasProtocolVersion && !hasExperienceMode) {
597
+ // A legacy record does not preserve its original protocol body. Current
598
+ // configuration can never prove the historical request, even when the
599
+ // present experience is voice presence, so its idempotency key must not be
600
+ // replayed with inferred authority.
601
+ return null;
602
+ }
603
+ if ((protocolVersion !== 1 && protocolVersion !== 2) ||
604
+ (experienceMode !== "standard" && experienceMode !== "voice_presence") ||
605
+ experienceMode !== effectiveExperienceMode ||
606
+ !protocolVersionIsAdmitted(protocolVersion, bootstrap) ||
607
+ (protocolVersion === 1 && experienceMode !== "standard") ||
608
+ (experienceMode === "voice_presence" &&
609
+ (protocolVersion !== 2 || pendingLaunch.mode !== "voice"))) {
610
+ return null;
611
+ }
612
+ return {
613
+ mode: pendingLaunch.mode,
614
+ scopes: [...pendingLaunch.scopes],
615
+ idempotencyKey: pendingLaunch.idempotencyKey,
616
+ protocolVersion,
617
+ experienceMode,
618
+ };
619
+ };
620
+ const freshPendingLaunchIntent = (mode, scopes, idempotencyKey, bootstrap) => ({
621
+ mode,
622
+ scopes: [...scopes],
623
+ idempotencyKey,
624
+ protocolVersion: (0, exports.resolveAiGuideProtocolVersion)(bootstrap),
625
+ experienceMode: bootstrap.experienceMode ?? "standard",
626
+ });
565
627
  const parseTerminalEndIntent = (value) => {
566
628
  if (!value || typeof value !== "object" || Array.isArray(value))
567
629
  return null;
@@ -596,9 +658,13 @@ const parseFreshAiReturnProvisional = (value) => {
596
658
  if (!value || typeof value !== "object" || Array.isArray(value))
597
659
  return null;
598
660
  const raw = value;
599
- if (Object.keys(raw).sort().join(",") !==
600
- "conversationDisplayId,expiresAt,launchIdempotencyKey,linkId,mode,phase,sessionId,v" ||
601
- raw.v !== 1 ||
661
+ const recordVersion = raw.v === 1 || raw.v === 2 ? raw.v : null;
662
+ const scopes = Array.isArray(raw.scopes) ? raw.scopes : [];
663
+ const expectedKeys = recordVersion === 1
664
+ ? "conversationDisplayId,expiresAt,launchIdempotencyKey,linkId,mode,phase,sessionId,v"
665
+ : "conversationDisplayId,experienceMode,expiresAt,launchIdempotencyKey,linkId,mode,phase,protocolVersion,scopes,sessionId,v";
666
+ if (recordVersion === null ||
667
+ Object.keys(raw).sort().join(",") !== expectedKeys ||
602
668
  !PUBLIC_REQUEST_ID_PATTERN.test(raw.linkId ?? "") ||
603
669
  !/^[1-9]\d{0,18}$/.test(raw.conversationDisplayId ?? "") ||
604
670
  (raw.mode !== "text" && raw.mode !== "voice") ||
@@ -613,19 +679,42 @@ const parseFreshAiReturnProvisional = (value) => {
613
679
  (raw.launchIdempotencyKey === null || raw.sessionId !== null)) ||
614
680
  (raw.phase === "launched" &&
615
681
  (raw.launchIdempotencyKey === null || raw.sessionId === null)) ||
682
+ (recordVersion === 2 &&
683
+ raw.phase === "released" &&
684
+ (raw.protocolVersion !== null ||
685
+ raw.experienceMode !== null ||
686
+ raw.scopes !== null)) ||
687
+ (recordVersion === 2 &&
688
+ raw.phase !== "released" &&
689
+ ((raw.protocolVersion !== 1 && raw.protocolVersion !== 2) ||
690
+ (raw.experienceMode !== "standard" &&
691
+ raw.experienceMode !== "voice_presence") ||
692
+ (raw.protocolVersion === 1 && raw.experienceMode !== "standard") ||
693
+ (raw.experienceMode === "voice_presence" &&
694
+ (raw.protocolVersion !== 2 || raw.mode !== "voice")) ||
695
+ !Array.isArray(raw.scopes) ||
696
+ scopes.length > CONSENT_SCOPES.size ||
697
+ scopes.some((scope) => typeof scope !== "string" ||
698
+ !CONSENT_SCOPES.has(scope)) ||
699
+ new Set(scopes).size !== scopes.length)) ||
616
700
  !Number.isSafeInteger(raw.expiresAt) ||
617
701
  (raw.expiresAt ?? 0) <= Date.now() ||
618
702
  (raw.expiresAt ?? 0) > Date.now() + STORAGE_TTL_MS) {
619
703
  return null;
620
704
  }
621
705
  return {
622
- v: 1,
706
+ v: recordVersion,
623
707
  linkId: raw.linkId,
624
708
  conversationDisplayId: raw.conversationDisplayId,
625
709
  mode: raw.mode,
626
710
  phase: raw.phase,
627
711
  launchIdempotencyKey: raw.launchIdempotencyKey ?? null,
628
712
  sessionId: raw.sessionId ?? null,
713
+ protocolVersion: recordVersion === 2 ? (raw.protocolVersion ?? null) : null,
714
+ experienceMode: recordVersion === 2 ? (raw.experienceMode ?? null) : null,
715
+ scopes: recordVersion === 2 && raw.phase !== "released"
716
+ ? [...scopes]
717
+ : null,
629
718
  expiresAt: raw.expiresAt,
630
719
  };
631
720
  };
@@ -662,6 +751,16 @@ const parseResumeRecord = (value) => {
662
751
  const record = JSON.parse(value);
663
752
  const scopes = Array.isArray(record.scopes) ? record.scopes : [];
664
753
  const locale = typeof record.locale === "string" ? canonicalLocale(record.locale) : null;
754
+ const protocolFieldsAbsent = record.protocolVersion === undefined &&
755
+ record.experienceMode === undefined;
756
+ const protocolBindingValid = protocolFieldsAbsent ||
757
+ ((record.protocolVersion === 1 || record.protocolVersion === 2) &&
758
+ (record.experienceMode === "standard" ||
759
+ record.experienceMode === "voice_presence") &&
760
+ (record.protocolVersion !== 1 ||
761
+ record.experienceMode === "standard") &&
762
+ (record.experienceMode !== "voice_presence" ||
763
+ (record.protocolVersion === 2 && record.mode === "voice")));
665
764
  if (!PUBLIC_REQUEST_ID_PATTERN.test(record.sessionId ?? "") ||
666
765
  !(0, client_js_1.isValidAssistSessionGrant)(record.resumeCredential) ||
667
766
  !["text", "voice"].includes(record.mode ?? "") ||
@@ -670,6 +769,7 @@ const parseResumeRecord = (value) => {
670
769
  scopes.some((scope) => typeof scope !== "string" ||
671
770
  !CONSENT_SCOPES.has(scope)) ||
672
771
  new Set(scopes).size !== scopes.length ||
772
+ !protocolBindingValid ||
673
773
  !locale ||
674
774
  typeof record.expiresAt !== "number" ||
675
775
  !Number.isFinite(record.expiresAt) ||
@@ -903,10 +1003,10 @@ const parseResumeRecord = (value) => {
903
1003
  ? record.visualConsentGranted
904
1004
  : record.scopes.includes("visual_context"),
905
1005
  manualTurn: record.manualTurn === true,
906
- protocolVersion: record.protocolVersion === 2 ? 2 : 1,
907
- experienceMode: record.experienceMode === "voice_presence"
908
- ? "voice_presence"
909
- : "standard",
1006
+ protocolVersion: protocolFieldsAbsent ? 1 : record.protocolVersion,
1007
+ experienceMode: protocolFieldsAbsent
1008
+ ? "standard"
1009
+ : record.experienceMode,
910
1010
  fallbackIntent: record.fallbackIntent === "standard_assist" ||
911
1011
  record.fallbackIntent === "human_handoff"
912
1012
  ? record.fallbackIntent
@@ -985,11 +1085,62 @@ const parseTranscript = (value) => {
985
1085
  }
986
1086
  return entries;
987
1087
  };
1088
+ const parsePendingLaunchIntent = (value, recordVersion) => {
1089
+ if (!value || typeof value !== "object" || Array.isArray(value))
1090
+ return null;
1091
+ const candidate = value;
1092
+ const scopes = Array.isArray(candidate.scopes) ? candidate.scopes : [];
1093
+ const expectedKeys = recordVersion === 1
1094
+ ? ["idempotencyKey", "mode", "scopes"]
1095
+ : [
1096
+ "experienceMode",
1097
+ "idempotencyKey",
1098
+ "mode",
1099
+ "protocolVersion",
1100
+ "scopes",
1101
+ ];
1102
+ if (canonicalJson(Object.keys(candidate).sort()) !==
1103
+ canonicalJson(expectedKeys) ||
1104
+ (candidate.mode !== "text" && candidate.mode !== "voice") ||
1105
+ !Array.isArray(candidate.scopes) ||
1106
+ typeof candidate.idempotencyKey !== "string" ||
1107
+ !PUBLIC_REQUEST_ID_PATTERN.test(candidate.idempotencyKey) ||
1108
+ scopes.length > CONSENT_SCOPES.size ||
1109
+ scopes.some((scope) => typeof scope !== "string" || !CONSENT_SCOPES.has(scope)) ||
1110
+ new Set(scopes).size !== scopes.length) {
1111
+ return null;
1112
+ }
1113
+ const base = {
1114
+ mode: candidate.mode,
1115
+ scopes: [...scopes],
1116
+ idempotencyKey: candidate.idempotencyKey,
1117
+ };
1118
+ if (recordVersion === 1) {
1119
+ return { ...base, protocolVersion: null, experienceMode: null };
1120
+ }
1121
+ const protocolVersion = candidate.protocolVersion;
1122
+ const experienceMode = candidate.experienceMode;
1123
+ if ((protocolVersion !== 1 && protocolVersion !== 2) ||
1124
+ (experienceMode !== "standard" && experienceMode !== "voice_presence") ||
1125
+ (protocolVersion === 1 && experienceMode !== "standard") ||
1126
+ (experienceMode === "voice_presence" &&
1127
+ (protocolVersion !== 2 || candidate.mode !== "voice"))) {
1128
+ return null;
1129
+ }
1130
+ return { ...base, protocolVersion, experienceMode };
1131
+ };
988
1132
  const parseFocusedHandoffPayload = (value) => {
989
1133
  if (!value || typeof value !== "object" || Array.isArray(value))
990
1134
  return null;
991
1135
  const raw = value;
992
- if (raw.v === 1) {
1136
+ if (raw.v === 1 || raw.v === 2) {
1137
+ const expectedKeys = ["resume", "transcript", "v"];
1138
+ if (raw.pendingLaunch !== undefined)
1139
+ expectedKeys.push("pendingLaunch");
1140
+ if (canonicalJson(Object.keys(raw).sort()) !==
1141
+ canonicalJson(expectedKeys.sort())) {
1142
+ return null;
1143
+ }
993
1144
  const resume = raw.resume === null
994
1145
  ? null
995
1146
  : parseResumeRecord(JSON.stringify(raw.resume ?? null));
@@ -997,30 +1148,17 @@ const parseFocusedHandoffPayload = (value) => {
997
1148
  return null;
998
1149
  let pendingLaunch;
999
1150
  if (raw.pendingLaunch !== undefined) {
1000
- if (!raw.pendingLaunch ||
1001
- typeof raw.pendingLaunch !== "object" ||
1002
- Array.isArray(raw.pendingLaunch)) {
1151
+ pendingLaunch =
1152
+ parsePendingLaunchIntent(raw.pendingLaunch, raw.v) ?? undefined;
1153
+ if (resume || !pendingLaunch)
1003
1154
  return null;
1004
- }
1005
- const candidate = raw.pendingLaunch;
1006
- const scopes = Array.isArray(candidate.scopes) ? candidate.scopes : [];
1007
- if (resume ||
1008
- (candidate.mode !== "text" && candidate.mode !== "voice") ||
1009
- !PUBLIC_REQUEST_ID_PATTERN.test(String(candidate.idempotencyKey ?? "")) ||
1010
- scopes.length > CONSENT_SCOPES.size ||
1011
- scopes.some((scope) => typeof scope !== "string" ||
1012
- !CONSENT_SCOPES.has(scope)) ||
1013
- new Set(scopes).size !== scopes.length) {
1014
- return null;
1015
- }
1016
- pendingLaunch = {
1017
- mode: candidate.mode,
1018
- scopes: [...scopes],
1019
- idempotencyKey: candidate.idempotencyKey,
1020
- };
1155
+ }
1156
+ if (raw.v === 2 &&
1157
+ (!pendingLaunch || !pendingLaunchIsBound(pendingLaunch))) {
1158
+ return null;
1021
1159
  }
1022
1160
  return {
1023
- v: 1,
1161
+ v: raw.v,
1024
1162
  resume,
1025
1163
  transcript: parseTranscript(raw.transcript),
1026
1164
  ...(pendingLaunch ? { pendingLaunch } : {}),
@@ -1068,19 +1206,15 @@ const parseStagedPendingLaunch = (value) => {
1068
1206
  const claimAuthority = raw.claimAuthority === null
1069
1207
  ? null
1070
1208
  : parseTransferAuthority(JSON.stringify(raw.claimAuthority ?? null));
1209
+ const recordVersion = raw.v === 1 || raw.v === 2 ? raw.v : null;
1071
1210
  const payload = parseFocusedHandoffPayload({
1072
- v: 1,
1211
+ v: recordVersion,
1073
1212
  resume: null,
1074
1213
  transcript: [],
1075
1214
  pendingLaunch: raw.pendingLaunch,
1076
1215
  });
1077
1216
  const expiresAt = raw.expiresAt;
1078
1217
  const topLevelKeys = Object.keys(raw).sort();
1079
- const pendingKeys = raw.pendingLaunch &&
1080
- typeof raw.pendingLaunch === "object" &&
1081
- !Array.isArray(raw.pendingLaunch)
1082
- ? Object.keys(raw.pendingLaunch).sort()
1083
- : [];
1084
1218
  const authorityKeys = raw.authority &&
1085
1219
  typeof raw.authority === "object" &&
1086
1220
  !Array.isArray(raw.authority)
@@ -1101,14 +1235,12 @@ const parseStagedPendingLaunch = (value) => {
1101
1235
  "state",
1102
1236
  "v",
1103
1237
  ]) ||
1104
- canonicalJson(pendingKeys) !==
1105
- canonicalJson(["idempotencyKey", "mode", "scopes"]) ||
1106
1238
  canonicalJson(authorityKeys) !==
1107
1239
  canonicalJson(["ownerId", "revision", "term"]) ||
1108
1240
  (raw.claimAuthority !== null &&
1109
1241
  canonicalJson(claimAuthorityKeys) !==
1110
1242
  canonicalJson(["ownerId", "revision", "term"])) ||
1111
- raw.v !== 1 ||
1243
+ recordVersion === null ||
1112
1244
  typeof raw.sourceOwnerId !== "string" ||
1113
1245
  !PUBLIC_REQUEST_ID_PATTERN.test(raw.sourceOwnerId) ||
1114
1246
  !authority ||
@@ -1132,7 +1264,7 @@ const parseStagedPendingLaunch = (value) => {
1132
1264
  return null;
1133
1265
  }
1134
1266
  return {
1135
- v: 1,
1267
+ v: recordVersion,
1136
1268
  sourceOwnerId: raw.sourceOwnerId,
1137
1269
  authority,
1138
1270
  state: raw.state,
@@ -1525,10 +1657,12 @@ class AiLiveAssist {
1525
1657
  this.storageKey = `__oxvo_ai_live_assist_resume_${this.siteKey}`;
1526
1658
  this.transcriptStorageKey = `__oxvo_ai_live_assist_transcript_${this.siteKey}`;
1527
1659
  this.transferAuthorityStorageKey = `__oxvo_ai_live_assist_transfer_authority_${this.siteKey}`;
1528
- this.stagedPendingLaunchStorageKey = `__oxvo_ai_live_assist_pending_launch_${this.siteKey}`;
1660
+ this.legacyStagedPendingLaunchStorageKey = `__oxvo_ai_live_assist_pending_launch_${this.siteKey}`;
1661
+ this.stagedPendingLaunchStorageKey = `__oxvo_ai_live_assist_pending_launch_v2_${this.siteKey}`;
1529
1662
  this.terminalEndStorageKey = `__oxvo_ai_live_assist_terminal_end_${this.siteKey}`;
1530
1663
  this.messengerReturnEndStorageKey = `__oxvo_ai_live_assist_handoff_terminal_${this.siteKey}`;
1531
- this.freshAiReturnProvisionalStorageKey = `__oxvo_ai_live_assist_fresh_return_${this.siteKey}`;
1664
+ this.legacyFreshAiReturnProvisionalStorageKey = `__oxvo_ai_live_assist_fresh_return_${this.siteKey}`;
1665
+ this.freshAiReturnProvisionalStorageKey = `__oxvo_ai_live_assist_fresh_return_v2_${this.siteKey}`;
1532
1666
  this.pageAssistanceTrackerIdentityStorageKey = `__oxvo_ai_live_assist_page_assistance_tracker_${this.siteKey}`;
1533
1667
  this.transcript = this.readTranscript();
1534
1668
  this.visitorIdentity = this.resolveVisitorId();
@@ -1727,8 +1861,7 @@ class AiLiveAssist {
1727
1861
  pendingLaunch &&
1728
1862
  (!staged ||
1729
1863
  (staged.state === "in_flight" &&
1730
- staged.pendingLaunch.idempotencyKey ===
1731
- pendingLaunch.idempotencyKey))
1864
+ this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)))
1732
1865
  ? { ...pendingLaunch, scopes: [...pendingLaunch.scopes] }
1733
1866
  : null;
1734
1867
  const record = this.readResume();
@@ -1743,8 +1876,12 @@ class AiLiveAssist {
1743
1876
  this.persistTerminalEndIntent(intent);
1744
1877
  if (record)
1745
1878
  this.storeTerminalResume(record, intent);
1746
- else if (this.result)
1747
- this.captureTerminalAuthorizedResult(this.result);
1879
+ else if (this.result) {
1880
+ const retainedResult = this.result;
1881
+ if (!this.captureTerminalAuthorizedResult(retainedResult)) {
1882
+ this.rejectTerminalAuthorizedResult(retainedResult);
1883
+ }
1884
+ }
1748
1885
  }
1749
1886
  this.terminalRequestPending = true;
1750
1887
  if (!this.relinquishModeUpgradeForRecovery(false)) {
@@ -1908,8 +2045,7 @@ class AiLiveAssist {
1908
2045
  if (resume &&
1909
2046
  !terminalIntent &&
1910
2047
  !resumedFallback &&
1911
- (resume.experienceMode !== experienceMode ||
1912
- resume.protocolVersion !== requestedProtocol ||
2048
+ (!resumeProtocolIsSupported(resume, bootstrap) ||
1913
2049
  resume.fallbackIntent !== null)) {
1914
2050
  this.clearResume();
1915
2051
  resume = null;
@@ -2633,9 +2769,6 @@ class AiLiveAssist {
2633
2769
  !this.messengerSupportsConfirmedHandoff(messenger)) {
2634
2770
  return false;
2635
2771
  }
2636
- bridgeActivationAttempted = true;
2637
- if (messenger.setAiGuideHandoffEnabled(true) !== true)
2638
- return false;
2639
2772
  if (messenger.ready) {
2640
2773
  await this.withMessengerTimeout(messenger.ready(), MESSENGER_LOAD_TIMEOUT_MS);
2641
2774
  }
@@ -2645,6 +2778,9 @@ class AiLiveAssist {
2645
2778
  this.resolveMessenger() !== messenger) {
2646
2779
  return false;
2647
2780
  }
2781
+ bridgeActivationAttempted = true;
2782
+ if (messenger.setAiGuideHandoffEnabled(true) !== true)
2783
+ return false;
2648
2784
  if (this.messengerInitialLauncherVisible === null) {
2649
2785
  this.messengerInitialLauncherVisible =
2650
2786
  messenger.launcherVisible !== false;
@@ -3873,13 +4009,16 @@ class AiLiveAssist {
3873
4009
  if (expiresAt <= Date.now())
3874
4010
  return null;
3875
4011
  return {
3876
- v: 1,
4012
+ v: 2,
3877
4013
  linkId: companion.linkId,
3878
4014
  conversationDisplayId: conversationId,
3879
4015
  mode,
3880
4016
  phase: "released",
3881
4017
  launchIdempotencyKey: null,
3882
4018
  sessionId: null,
4019
+ protocolVersion: null,
4020
+ experienceMode: null,
4021
+ scopes: null,
3883
4022
  expiresAt,
3884
4023
  };
3885
4024
  }
@@ -3896,8 +4035,12 @@ class AiLiveAssist {
3896
4035
  }
3897
4036
  this.persistFreshAiReturnProvisional({
3898
4037
  ...provisional,
4038
+ v: 2,
3899
4039
  phase: "released",
3900
4040
  launchIdempotencyKey: null,
4041
+ protocolVersion: null,
4042
+ experienceMode: null,
4043
+ scopes: null,
3901
4044
  });
3902
4045
  }
3903
4046
  quiesceProvisionalFreshAiReturn() {
@@ -3911,22 +4054,59 @@ class AiLiveAssist {
3911
4054
  this.view?.setPaused(true);
3912
4055
  this.cleanupTransport();
3913
4056
  }
4057
+ boundFreshAiReturnProvisional(provisional) {
4058
+ if (provisional.phase === "released" ||
4059
+ provisional.launchIdempotencyKey === null ||
4060
+ provisional.protocolVersion === null ||
4061
+ provisional.experienceMode === null ||
4062
+ provisional.scopes === null ||
4063
+ !this.bootstrap) {
4064
+ return null;
4065
+ }
4066
+ const pendingLaunch = {
4067
+ mode: provisional.mode,
4068
+ scopes: [...provisional.scopes],
4069
+ idempotencyKey: provisional.launchIdempotencyKey,
4070
+ protocolVersion: provisional.protocolVersion,
4071
+ experienceMode: provisional.experienceMode,
4072
+ };
4073
+ const bound = bindPendingLaunchIntent(pendingLaunch, this.bootstrap);
4074
+ return bound && this.pendingLaunchMatches(bound, pendingLaunch)
4075
+ ? bound
4076
+ : null;
4077
+ }
4078
+ resumeMatchesFreshAiReturn(resume, pendingLaunch) {
4079
+ return (resume.protocolVersion === pendingLaunch.protocolVersion &&
4080
+ resume.experienceMode === pendingLaunch.experienceMode &&
4081
+ resume.mode === pendingLaunch.mode &&
4082
+ canonicalJson(resume.scopes) === canonicalJson(pendingLaunch.scopes));
4083
+ }
4084
+ resultMatchesFreshAiReturn(result, pendingLaunch) {
4085
+ return (result.schemaVersion === pendingLaunch.protocolVersion &&
4086
+ result.protocolVersion === pendingLaunch.protocolVersion &&
4087
+ result.experienceMode === pendingLaunch.experienceMode &&
4088
+ (result.mode === undefined || result.mode === pendingLaunch.mode));
4089
+ }
3914
4090
  async retireProvisionalFreshAiReturn(provisional) {
3915
4091
  if (provisional.phase === "released")
3916
4092
  return true;
4093
+ const pendingLaunch = this.boundFreshAiReturnProvisional(provisional);
4094
+ if (!pendingLaunch)
4095
+ return false;
3917
4096
  const resume = this.readResume();
3918
4097
  const result = this.result;
3919
4098
  const sessionId = provisional.sessionId ?? resume?.sessionId ?? result?.sessionId ?? null;
3920
4099
  if (!sessionId)
3921
4100
  return false;
3922
- const authority = resume?.sessionId === sessionId
4101
+ const authority = resume?.sessionId === sessionId &&
4102
+ this.resumeMatchesFreshAiReturn(resume, pendingLaunch)
3923
4103
  ? {
3924
4104
  resumeCredential: resume.resumeCredential,
3925
4105
  protocolVersion: resume.protocolVersion,
3926
4106
  }
3927
4107
  : result?.sessionId === sessionId &&
3928
4108
  typeof result.resumeCredential === "string" &&
3929
- (result.protocolVersion === 1 || result.protocolVersion === 2)
4109
+ this.resultMatchesFreshAiReturn(result, pendingLaunch)
3930
4110
  ? {
3931
4111
  resumeCredential: result.resumeCredential,
3932
4112
  protocolVersion: result.protocolVersion,
@@ -3984,9 +4164,13 @@ class AiLiveAssist {
3984
4164
  provisional.conversationDisplayId;
3985
4165
  const released = {
3986
4166
  ...provisional,
4167
+ v: 2,
3987
4168
  phase: "released",
3988
4169
  launchIdempotencyKey: null,
3989
4170
  sessionId: null,
4171
+ protocolVersion: null,
4172
+ experienceMode: null,
4173
+ scopes: null,
3990
4174
  };
3991
4175
  return this.persistFreshAiReturnProvisional(released);
3992
4176
  }
@@ -4007,6 +4191,46 @@ class AiLiveAssist {
4007
4191
  // interrupted must never be replaced by a second fresh operation.
4008
4192
  return provisional.phase === "launching" ? "launch" : "blocked";
4009
4193
  }
4194
+ if (provisional.protocolVersion === null ||
4195
+ provisional.experienceMode === null ||
4196
+ provisional.scopes === null) {
4197
+ if (provisional.launchIdempotencyKey === null ||
4198
+ !resume ||
4199
+ resume.sessionId !== sessionId) {
4200
+ return "blocked";
4201
+ }
4202
+ const upgraded = {
4203
+ ...provisional,
4204
+ v: 2,
4205
+ sessionId,
4206
+ protocolVersion: resume.protocolVersion,
4207
+ experienceMode: resume.experienceMode,
4208
+ scopes: [...resume.scopes],
4209
+ };
4210
+ if (!this.boundFreshAiReturnProvisional(upgraded) ||
4211
+ !this.persistFreshAiReturnProvisional(upgraded)) {
4212
+ return "blocked";
4213
+ }
4214
+ provisional = upgraded;
4215
+ }
4216
+ const pendingLaunch = this.boundFreshAiReturnProvisional(provisional);
4217
+ if (!pendingLaunch)
4218
+ return "blocked";
4219
+ let hasSessionAuthority = false;
4220
+ if (resume?.sessionId === sessionId) {
4221
+ if (!this.resumeMatchesFreshAiReturn(resume, pendingLaunch)) {
4222
+ return "blocked";
4223
+ }
4224
+ hasSessionAuthority = true;
4225
+ }
4226
+ if (this.result?.sessionId === sessionId) {
4227
+ if (!this.resultMatchesFreshAiReturn(this.result, pendingLaunch)) {
4228
+ return "blocked";
4229
+ }
4230
+ hasSessionAuthority = true;
4231
+ }
4232
+ if (!hasSessionAuthority)
4233
+ return "blocked";
4010
4234
  if (provisional.phase === "launching") {
4011
4235
  provisional = {
4012
4236
  ...provisional,
@@ -4208,17 +4432,61 @@ class AiLiveAssist {
4208
4432
  const currentProvisional = this.freshAiReturnProvisionalFor(conversationId);
4209
4433
  if (!currentProvisional)
4210
4434
  return false;
4435
+ if (currentProvisional.phase === "launching" &&
4436
+ currentProvisional.mode !== mode) {
4437
+ return false;
4438
+ }
4439
+ const persistedLaunchScopes = currentProvisional.phase === "launching"
4440
+ ? currentProvisional.scopes
4441
+ : null;
4442
+ if (currentProvisional.phase === "launching" &&
4443
+ (currentProvisional.protocolVersion === null ||
4444
+ currentProvisional.experienceMode === null ||
4445
+ persistedLaunchScopes === null)) {
4446
+ // A legacy Fresh-return record does not contain the complete request
4447
+ // body. Reusing its idempotency key with current bootstrap scopes or
4448
+ // protocol negotiation would create a different operation.
4449
+ return false;
4450
+ }
4211
4451
  const launchIdempotencyKey = currentProvisional.phase === "launching"
4212
4452
  ? currentProvisional.launchIdempotencyKey
4213
4453
  : (0, client_js_1.idempotencyKey)("launch");
4214
4454
  if (!launchIdempotencyKey)
4215
4455
  return false;
4456
+ const launchScopes = persistedLaunchScopes
4457
+ ? [...persistedLaunchScopes]
4458
+ : [...scopes];
4459
+ const provisionalPendingLaunch = currentProvisional.protocolVersion !== null &&
4460
+ currentProvisional.experienceMode !== null
4461
+ ? {
4462
+ mode: currentProvisional.mode,
4463
+ scopes: launchScopes,
4464
+ idempotencyKey: launchIdempotencyKey,
4465
+ protocolVersion: currentProvisional.protocolVersion,
4466
+ experienceMode: currentProvisional.experienceMode,
4467
+ }
4468
+ : {
4469
+ mode: currentProvisional.mode,
4470
+ scopes: launchScopes,
4471
+ idempotencyKey: launchIdempotencyKey,
4472
+ protocolVersion: null,
4473
+ experienceMode: null,
4474
+ };
4475
+ const pendingLaunch = currentProvisional.phase === "launching"
4476
+ ? bindPendingLaunchIntent(provisionalPendingLaunch, bootstrap)
4477
+ : freshPendingLaunchIntent(mode, scopes, launchIdempotencyKey, bootstrap);
4478
+ if (!pendingLaunch)
4479
+ return false;
4216
4480
  const launching = {
4217
4481
  ...currentProvisional,
4218
- mode,
4482
+ v: 2,
4483
+ mode: pendingLaunch.mode,
4219
4484
  phase: "launching",
4220
4485
  launchIdempotencyKey,
4221
4486
  sessionId: null,
4487
+ protocolVersion: pendingLaunch.protocolVersion,
4488
+ experienceMode: pendingLaunch.experienceMode,
4489
+ scopes: [...pendingLaunch.scopes],
4222
4490
  };
4223
4491
  if (!this.persistFreshAiReturnProvisional(launching))
4224
4492
  return false;
@@ -4234,7 +4502,7 @@ class AiLiveAssist {
4234
4502
  inboxId: messengerDescriptor.inboxId,
4235
4503
  linkId: this.companion.linkId,
4236
4504
  launchIdempotencyKey,
4237
- mode,
4505
+ mode: pendingLaunch.mode,
4238
4506
  previousAiSessionId: this.companion.currentAiSessionId,
4239
4507
  surface: this.messengerTransferredSurface,
4240
4508
  }
@@ -4242,10 +4510,11 @@ class AiLiveAssist {
4242
4510
  if (!messengerReturnAuthority)
4243
4511
  return false;
4244
4512
  this.messengerReturnRetryRemaining = 1;
4245
- await this.launch(mode, scopes, launchIdempotencyKey, {
4246
- startMuted: mode === "voice",
4513
+ await this.launch(pendingLaunch.mode, pendingLaunch.scopes, launchIdempotencyKey, {
4514
+ startMuted: pendingLaunch.mode === "voice",
4247
4515
  associateCompanion: false,
4248
4516
  messengerReturnAuthority,
4517
+ pendingLaunch,
4249
4518
  });
4250
4519
  this.messengerReturnRetryRemaining = null;
4251
4520
  const sessionId = this.result?.sessionId;
@@ -4440,6 +4709,7 @@ class AiLiveAssist {
4440
4709
  if (this.pushActive)
4441
4710
  this.media?.beginPushToTalk();
4442
4711
  this.closeMessengerBeforeOpen();
4712
+ await this.prepareMessengerIntegration();
4443
4713
  return true;
4444
4714
  }
4445
4715
  async associateFreshCompanionSession(mode, sessionId) {
@@ -4563,11 +4833,6 @@ class AiLiveAssist {
4563
4833
  const prepared = await this.withMessengerTimeout(messenger.openHandoffConversation(displayId, options), MESSENGER_LOAD_TIMEOUT_MS);
4564
4834
  if (!prepared || !surfaceCurrent())
4565
4835
  return;
4566
- const returnActivated = await this.createMessengerReturnActivationPromise(messenger, displayId);
4567
- if (!returnActivated || !surfaceCurrent()) {
4568
- this.closeMessengerBeforeOpen();
4569
- return;
4570
- }
4571
4836
  const switched = await this.requestCompanionSurface("messenger", mode, (0, client_js_1.idempotencyKey)("companion_switch"), result.sessionId);
4572
4837
  const switchedCurrent = () => generation === this.sessionEffectGeneration &&
4573
4838
  this.result === result &&
@@ -4635,6 +4900,11 @@ class AiLiveAssist {
4635
4900
  await this.restoreAiAfterFailedSupportSwitch(mode, result.sessionId, previousMuted, previousPushActive);
4636
4901
  return;
4637
4902
  }
4903
+ const returnActivated = await this.createMessengerReturnActivationPromise(messenger, displayId);
4904
+ if (!returnActivated || !switchedCurrent()) {
4905
+ await this.restoreAiAfterFailedSupportSwitch(mode, result.sessionId, previousMuted, previousPushActive);
4906
+ return;
4907
+ }
4638
4908
  this.messenger = messenger;
4639
4909
  this.messengerSurfaceActive = true;
4640
4910
  this.messengerTransferredConversationId = displayId;
@@ -6197,9 +6467,27 @@ class AiLiveAssist {
6197
6467
  releaseMessengerReturnIfPreRuntime();
6198
6468
  return;
6199
6469
  }
6200
- const experienceMode = bootstrap.experienceMode ?? "standard";
6470
+ const retryIntent = options.pendingLaunch;
6471
+ if (retryIntent &&
6472
+ canonicalJson({ mode, scopes, idempotencyKey: launchIdempotencyKey }) !==
6473
+ canonicalJson({
6474
+ mode: retryIntent.mode,
6475
+ scopes: retryIntent.scopes,
6476
+ idempotencyKey: retryIntent.idempotencyKey,
6477
+ })) {
6478
+ releaseMessengerReturnIfPreRuntime();
6479
+ return;
6480
+ }
6481
+ let durableLaunch = retryIntent
6482
+ ? bindPendingLaunchIntent(retryIntent, bootstrap)
6483
+ : freshPendingLaunchIntent(mode, scopes, launchIdempotencyKey, bootstrap);
6484
+ if (!durableLaunch) {
6485
+ this.view.showError(this.view.message("reconnectFailed"), false, false);
6486
+ releaseMessengerReturnIfPreRuntime();
6487
+ return;
6488
+ }
6489
+ const { experienceMode, protocolVersion } = durableLaunch;
6201
6490
  const voicePresence = experienceMode === "voice_presence";
6202
- const protocolVersion = (0, exports.resolveAiGuideProtocolVersion)(bootstrap);
6203
6491
  if (voicePresence && mode !== "voice") {
6204
6492
  this.view.showError("Voice assist requires microphone access.", false, false);
6205
6493
  releaseMessengerReturnIfPreRuntime();
@@ -6220,11 +6508,7 @@ class AiLiveAssist {
6220
6508
  this.visitorActiveTakeoverOnly = false;
6221
6509
  }
6222
6510
  if (this.visitorActiveTakeoverOnly) {
6223
- const pendingLaunch = {
6224
- mode,
6225
- scopes: [...scopes],
6226
- idempotencyKey: launchIdempotencyKey,
6227
- };
6511
+ const pendingLaunch = durableLaunch;
6228
6512
  this.lastLaunch = pendingLaunch;
6229
6513
  this.view.showActiveElsewhere(false);
6230
6514
  this.view.showConnecting(true, mode);
@@ -6238,11 +6522,7 @@ class AiLiveAssist {
6238
6522
  }
6239
6523
  this.pageAssistanceAuthorityGeneration += 1;
6240
6524
  this.rotatePageAssistanceBindingGeneration();
6241
- this.lastLaunch = {
6242
- mode,
6243
- scopes: [...scopes],
6244
- idempotencyKey: launchIdempotencyKey,
6245
- };
6525
+ this.lastLaunch = { ...durableLaunch, scopes: [...durableLaunch.scopes] };
6246
6526
  this.launchInFlight = true;
6247
6527
  if (!this.lock.acquire()) {
6248
6528
  this.view.showActiveElsewhere(false);
@@ -6300,7 +6580,8 @@ class AiLiveAssist {
6300
6580
  if (voicePresence ||
6301
6581
  mode !== "voice" ||
6302
6582
  !bootstrap.capabilities?.text ||
6303
- options.messengerReturnAuthority !== undefined) {
6583
+ options.messengerReturnAuthority !== undefined ||
6584
+ retryIntent !== undefined) {
6304
6585
  throw error;
6305
6586
  }
6306
6587
  this.releaseMedia(media);
@@ -6330,22 +6611,20 @@ class AiLiveAssist {
6330
6611
  this.navigationConsentResetCandidate = null;
6331
6612
  this.navigationConsentReset = null;
6332
6613
  this.navigationConsentResetReady = null;
6333
- this.lastLaunch = {
6334
- mode: activeMode,
6335
- scopes: [...activeScopes],
6336
- idempotencyKey: launchIdempotencyKey,
6337
- };
6614
+ if (!retryIntent) {
6615
+ durableLaunch = {
6616
+ ...durableLaunch,
6617
+ mode: activeMode,
6618
+ scopes: [...activeScopes],
6619
+ };
6620
+ }
6621
+ this.lastLaunch = { ...durableLaunch, scopes: [...durableLaunch.scopes] };
6338
6622
  this.visualConsentGranted = activeScopes.includes("visual_context");
6339
6623
  assertConnectionCurrent();
6340
6624
  if (this.lock.ownsLatestFocus?.() === false &&
6341
6625
  this.relinquishForNewerFocus()) {
6342
6626
  return;
6343
6627
  }
6344
- const durableLaunch = {
6345
- mode: activeMode,
6346
- scopes: [...activeScopes],
6347
- idempotencyKey: launchIdempotencyKey,
6348
- };
6349
6628
  if (!this.markStagedPendingLaunchInFlight(durableLaunch)) {
6350
6629
  this.releaseMedia(media);
6351
6630
  this.cleanupTransport();
@@ -6390,16 +6669,19 @@ class AiLiveAssist {
6390
6669
  }
6391
6670
  }
6392
6671
  }
6672
+ if (result.schemaVersion !== protocolVersion ||
6673
+ result.protocolVersion !== protocolVersion ||
6674
+ result.experienceMode !== experienceMode) {
6675
+ throw new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
6676
+ }
6393
6677
  if (this.terminalRequestPending) {
6394
- this.captureTerminalAuthorizedResult(result);
6678
+ if (!this.captureTerminalAuthorizedResult(result)) {
6679
+ throw new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
6680
+ }
6395
6681
  runtimeSessionCreated = true;
6396
6682
  return;
6397
6683
  }
6398
6684
  assertConnectionCurrent();
6399
- if (result.experienceMode !== experienceMode ||
6400
- result.protocolVersion !== protocolVersion) {
6401
- throw new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
6402
- }
6403
6685
  if (!this.commitAuthorizedRuntimePath(requestedLaunchPath, true)) {
6404
6686
  throw new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime authorized an invalid page path.", false, null, 502);
6405
6687
  }
@@ -6534,7 +6816,8 @@ class AiLiveAssist {
6534
6816
  this.terminalRequestPending ||
6535
6817
  this.ended ||
6536
6818
  this.destroyed ||
6537
- this.lastLaunch?.idempotencyKey !== pendingLaunch.idempotencyKey ||
6819
+ !this.lastLaunch ||
6820
+ !this.pendingLaunchMatches(this.lastLaunch, pendingLaunch) ||
6538
6821
  this.readResume()) {
6539
6822
  return;
6540
6823
  }
@@ -6566,7 +6849,8 @@ class AiLiveAssist {
6566
6849
  this.visitorActiveEligibilityRecheckKey = pendingLaunch.idempotencyKey;
6567
6850
  const pendingIntentIsCurrent = () => launchGeneration === this.launchGeneration &&
6568
6851
  this.visitorActiveEligibilityRecheckController === controller &&
6569
- this.lastLaunch?.idempotencyKey === pendingLaunch.idempotencyKey &&
6852
+ Boolean(this.lastLaunch &&
6853
+ this.pendingLaunchMatches(this.lastLaunch, pendingLaunch)) &&
6570
6854
  !this.terminalRequestPending &&
6571
6855
  !this.ended &&
6572
6856
  !this.destroyed &&
@@ -6626,7 +6910,7 @@ class AiLiveAssist {
6626
6910
  this.visitorActiveTakeoverOnly = false;
6627
6911
  this.lastLaunch = null;
6628
6912
  this.view?.showActiveElsewhere(false);
6629
- await this.launch(pendingLaunch.mode, pendingLaunch.scopes, pendingLaunch.idempotencyKey);
6913
+ await this.launch(pendingLaunch.mode, pendingLaunch.scopes, pendingLaunch.idempotencyKey, { pendingLaunch });
6630
6914
  return;
6631
6915
  }
6632
6916
  if (status === "visitor_limited") {
@@ -6658,7 +6942,8 @@ class AiLiveAssist {
6658
6942
  this.visitorActiveEligibilityRecheck = operation;
6659
6943
  }
6660
6944
  finishVisitorActiveEligibilityRecheck(pendingLaunch, messageKey) {
6661
- if (this.lastLaunch?.idempotencyKey !== pendingLaunch.idempotencyKey) {
6945
+ if (!this.lastLaunch ||
6946
+ !this.pendingLaunchMatches(this.lastLaunch, pendingLaunch)) {
6662
6947
  return;
6663
6948
  }
6664
6949
  this.lastLaunch = null;
@@ -6815,8 +7100,15 @@ class AiLiveAssist {
6815
7100
  }
6816
7101
  }
6817
7102
  }
7103
+ if (result.schemaVersion !== record.protocolVersion ||
7104
+ result.protocolVersion !== record.protocolVersion ||
7105
+ result.experienceMode !== record.experienceMode) {
7106
+ throw new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The saved voice assist session is no longer compatible.", false, null, 409);
7107
+ }
6818
7108
  if (this.terminalRequestPending) {
6819
- this.captureTerminalAuthorizedResult(result, record);
7109
+ if (!this.captureTerminalAuthorizedResult(result, record)) {
7110
+ throw new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The saved voice assist session is no longer compatible.", false, null, 409);
7111
+ }
6820
7112
  if (modeUpgrade)
6821
7113
  this.modeUpgradeTerminalResult = result;
6822
7114
  attemptedCredentials.add(result.resumeCredential);
@@ -6826,8 +7118,6 @@ class AiLiveAssist {
6826
7118
  return;
6827
7119
  assertConnectionCurrent();
6828
7120
  if (result.sessionId !== record.sessionId ||
6829
- result.protocolVersion !== record.protocolVersion ||
6830
- result.experienceMode !== record.experienceMode ||
6831
7121
  (result.mode !== undefined &&
6832
7122
  result.mode !== "text" &&
6833
7123
  result.mode !== "voice") ||
@@ -13105,7 +13395,7 @@ class AiLiveAssist {
13105
13395
  return;
13106
13396
  }
13107
13397
  this.messengerReturnRetryRemaining -= 1;
13108
- const retryAttempt = this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey);
13398
+ const retryAttempt = this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey, { pendingLaunch: this.lastLaunch });
13109
13399
  return retryAttempt.finally(() => {
13110
13400
  if (this.result && !this.ended) {
13111
13401
  this.messengerReturnRetryRemaining = null;
@@ -13125,7 +13415,7 @@ class AiLiveAssist {
13125
13415
  return this.resume(resume);
13126
13416
  }
13127
13417
  if (this.lastLaunch) {
13128
- return this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey);
13418
+ return this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey, { pendingLaunch: this.lastLaunch });
13129
13419
  }
13130
13420
  }
13131
13421
  scheduleReconnect(force = false) {
@@ -13455,6 +13745,18 @@ class AiLiveAssist {
13455
13745
  canonicalJson(left.messengerBinding) ===
13456
13746
  canonicalJson(right.messengerBinding));
13457
13747
  }
13748
+ sameResumeAuthorityIdentity(left, right) {
13749
+ const credentialRotated = left.resumeCredential !== right.resumeCredential;
13750
+ if (!credentialRotated && left.expiresAt !== right.expiresAt)
13751
+ return false;
13752
+ const { resumeCredential: _leftCredential, expiresAt: _leftCredentialExpiry, ...leftAuthority } = left;
13753
+ const { resumeCredential: _rightCredential, expiresAt: _rightCredentialExpiry, ...rightAuthority } = right;
13754
+ void _leftCredential;
13755
+ void _rightCredential;
13756
+ void _leftCredentialExpiry;
13757
+ void _rightCredentialExpiry;
13758
+ return canonicalJson(leftAuthority) === canonicalJson(rightAuthority);
13759
+ }
13458
13760
  sameModeUpgradeIntent(left, right) {
13459
13761
  return (left?.requestId === right?.requestId &&
13460
13762
  left?.resumeIdempotencyKey === right?.resumeIdempotencyKey &&
@@ -13823,10 +14125,22 @@ class AiLiveAssist {
13823
14125
  this.state("ended");
13824
14126
  }
13825
14127
  captureTerminalAuthorizedResult(result, sourceRecord = null) {
13826
- if (sourceRecord &&
13827
- (result.sessionId !== sourceRecord.sessionId ||
13828
- result.protocolVersion !== sourceRecord.protocolVersion ||
13829
- result.experienceMode !== sourceRecord.experienceMode)) {
14128
+ const pendingLaunch = this.terminalPendingLaunch &&
14129
+ pendingLaunchIsBound(this.terminalPendingLaunch)
14130
+ ? this.terminalPendingLaunch
14131
+ : null;
14132
+ const expectedProtocolVersion = sourceRecord?.protocolVersion ??
14133
+ pendingLaunch?.protocolVersion ??
14134
+ result.protocolVersion;
14135
+ const expectedExperienceMode = sourceRecord?.experienceMode ??
14136
+ pendingLaunch?.experienceMode ??
14137
+ result.experienceMode;
14138
+ if (result.schemaVersion !== expectedProtocolVersion ||
14139
+ result.protocolVersion !== expectedProtocolVersion ||
14140
+ result.experienceMode !== expectedExperienceMode ||
14141
+ (sourceRecord &&
14142
+ (result.sessionId !== sourceRecord.sessionId ||
14143
+ result.protocolVersion !== sourceRecord.protocolVersion))) {
13830
14144
  return false;
13831
14145
  }
13832
14146
  const currentIntent = this.terminalEndIntent;
@@ -13852,6 +14166,22 @@ class AiLiveAssist {
13852
14166
  }
13853
14167
  return true;
13854
14168
  }
14169
+ retryRejectedTerminalAuthorizedResult(result) {
14170
+ const error = this.rejectTerminalAuthorizedResult(result);
14171
+ return this.retryTerminalEnd(error);
14172
+ }
14173
+ rejectTerminalAuthorizedResult(result) {
14174
+ if (this.result === result)
14175
+ this.result = null;
14176
+ if (this.modeUpgradeTerminalResult === result) {
14177
+ this.modeUpgradeTerminalResult = null;
14178
+ }
14179
+ this.terminalSessionAuthorization = null;
14180
+ this.terminalSessionAuthorizationController = null;
14181
+ const error = new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The assist runtime returned an incompatible terminal session.", false, null, 409);
14182
+ this.terminalSessionAuthorizationFailure = error;
14183
+ return error;
14184
+ }
13855
14185
  completeRuntimeEnd() {
13856
14186
  if (this.ended)
13857
14187
  return Promise.resolve();
@@ -13882,8 +14212,9 @@ class AiLiveAssist {
13882
14212
  if (terminalAuthorization) {
13883
14213
  return this.waitForTerminalRequestDeadline(terminalAuthorization, this.terminalSessionAuthorizationController).then((authorized) => {
13884
14214
  const result = authorized ?? this.result ?? this.modeUpgradeTerminalResult;
13885
- if (result) {
13886
- this.captureTerminalAuthorizedResult(result, this.terminalResumeRecord);
14215
+ if (result &&
14216
+ !this.captureTerminalAuthorizedResult(result, this.terminalResumeRecord)) {
14217
+ return this.retryRejectedTerminalAuthorizedResult(result);
13887
14218
  }
13888
14219
  return this.finishRuntimeEndAttempt(result);
13889
14220
  }, (error) => {
@@ -13900,7 +14231,9 @@ class AiLiveAssist {
13900
14231
  }
13901
14232
  if (this.terminalPendingLaunch) {
13902
14233
  return this.requestTerminalPendingLaunch(this.terminalPendingLaunch).then((result) => {
13903
- this.captureTerminalAuthorizedResult(result);
14234
+ if (!this.captureTerminalAuthorizedResult(result)) {
14235
+ return this.retryRejectedTerminalAuthorizedResult(result);
14236
+ }
13904
14237
  return this.finishRuntimeEndAttempt(result);
13905
14238
  }, (recoveryError) => this.retryTerminalEnd(recoveryError));
13906
14239
  }
@@ -13927,7 +14260,9 @@ class AiLiveAssist {
13927
14260
  return this.finishRuntimeEndAttempt(null);
13928
14261
  if (this.terminalPendingLaunch) {
13929
14262
  return this.requestTerminalPendingLaunch(this.terminalPendingLaunch).then((result) => {
13930
- this.captureTerminalAuthorizedResult(result);
14263
+ if (!this.captureTerminalAuthorizedResult(result)) {
14264
+ return this.retryRejectedTerminalAuthorizedResult(result);
14265
+ }
13931
14266
  return this.finishRuntimeEndAttempt(result);
13932
14267
  }, (error) => this.retryTerminalEnd(error));
13933
14268
  }
@@ -14084,13 +14419,21 @@ class AiLiveAssist {
14084
14419
  requestTerminalPendingLaunch(pendingLaunch) {
14085
14420
  const intent = this.terminalEndIntent;
14086
14421
  if (!intent ||
14087
- intent.launchIdempotencyKey !== pendingLaunch.idempotencyKey ||
14088
- !this.markStagedPendingLaunchInFlight(pendingLaunch)) {
14422
+ intent.launchIdempotencyKey !== pendingLaunch.idempotencyKey) {
14089
14423
  return Promise.reject(new TerminalRequestDeferredError());
14090
14424
  }
14091
14425
  const controller = new AbortController();
14092
14426
  const request = (async () => {
14093
14427
  const bootstrap = await this.terminalPendingLaunchBootstrap(controller.signal);
14428
+ const boundPendingLaunch = bindPendingLaunchIntent(pendingLaunch, bootstrap);
14429
+ if (!boundPendingLaunch ||
14430
+ !this.markStagedPendingLaunchInFlight(boundPendingLaunch)) {
14431
+ throw new TerminalRequestDeferredError();
14432
+ }
14433
+ this.terminalPendingLaunch = {
14434
+ ...boundPendingLaunch,
14435
+ scopes: [...boundPendingLaunch.scopes],
14436
+ };
14094
14437
  const replay = await this.replayReadiness.wait();
14095
14438
  if (this.destroyed ||
14096
14439
  this.ended ||
@@ -14099,28 +14442,34 @@ class AiLiveAssist {
14099
14442
  throw new TerminalRequestDeferredError();
14100
14443
  }
14101
14444
  this.replayReadiness.assertCurrent(replay);
14102
- return this.runtime.launch({
14445
+ const result = await this.runtime.launch({
14103
14446
  siteKey: this.siteKey,
14104
14447
  configRevision: bootstrap.configRevision,
14105
14448
  path: path(),
14106
14449
  tabId: this.tabId,
14107
- mode: pendingLaunch.mode,
14450
+ mode: boundPendingLaunch.mode,
14108
14451
  anonymousId: this.visitorId(),
14109
14452
  replay,
14110
14453
  disclosureChecksum: bootstrap.consent.disclosureChecksum,
14111
- scopes: pendingLaunch.scopes,
14454
+ scopes: boundPendingLaunch.scopes,
14112
14455
  locale: this.locale,
14113
- microphone: pendingLaunch.mode === "voice",
14456
+ microphone: boundPendingLaunch.mode === "voice",
14114
14457
  reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)")
14115
14458
  .matches,
14116
- protocolVersion: (0, exports.resolveAiGuideProtocolVersion)(bootstrap),
14117
- experienceMode: bootstrap.experienceMode ?? "standard",
14459
+ protocolVersion: boundPendingLaunch.protocolVersion,
14460
+ experienceMode: boundPendingLaunch.experienceMode,
14118
14461
  ...(this.activeFallback
14119
14462
  ? { fallbackIntent: this.activeFallback }
14120
14463
  : {}),
14121
14464
  lowPower: this.lowPowerMode(),
14122
- idempotencyKey: pendingLaunch.idempotencyKey,
14465
+ idempotencyKey: boundPendingLaunch.idempotencyKey,
14123
14466
  }, controller.signal);
14467
+ if (result.schemaVersion !== boundPendingLaunch.protocolVersion ||
14468
+ result.protocolVersion !== boundPendingLaunch.protocolVersion ||
14469
+ result.experienceMode !== boundPendingLaunch.experienceMode) {
14470
+ throw new client_js_1.RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
14471
+ }
14472
+ return result;
14124
14473
  })();
14125
14474
  return this.waitForTerminalRequestDeadline(request, controller);
14126
14475
  }
@@ -14284,8 +14633,7 @@ class AiLiveAssist {
14284
14633
  const retainTerminalLaunch = Boolean(this.terminalSessionAuthorization &&
14285
14634
  this.terminalPendingLaunch &&
14286
14635
  staged?.state === "in_flight" &&
14287
- staged.pendingLaunch.idempotencyKey ===
14288
- this.terminalPendingLaunch.idempotencyKey);
14636
+ this.pendingLaunchMatches(staged.pendingLaunch, this.terminalPendingLaunch));
14289
14637
  if (!retainTerminalLaunch)
14290
14638
  this.clearOwnedStagedPendingLaunch();
14291
14639
  this.lock?.cancelPendingHandoff?.();
@@ -14709,15 +15057,18 @@ class AiLiveAssist {
14709
15057
  focusedHandoffPayload() {
14710
15058
  this.transcript ?? (this.transcript = []);
14711
15059
  const resume = this.readResume();
15060
+ const pendingLaunch = !resume && this.lastLaunch && this.bootstrap
15061
+ ? bindPendingLaunchIntent(this.lastLaunch, this.bootstrap)
15062
+ : null;
14712
15063
  return {
14713
- v: 1,
15064
+ v: pendingLaunch ? 2 : 1,
14714
15065
  resume,
14715
15066
  transcript: this.transcript.map((entry) => ({ ...entry })),
14716
- ...(!resume && this.lastLaunch
15067
+ ...(pendingLaunch
14717
15068
  ? {
14718
15069
  pendingLaunch: {
14719
- ...this.lastLaunch,
14720
- scopes: [...this.lastLaunch.scopes],
15070
+ ...pendingLaunch,
15071
+ scopes: [...pendingLaunch.scopes],
14721
15072
  },
14722
15073
  }
14723
15074
  : {}),
@@ -14732,33 +15083,157 @@ class AiLiveAssist {
14732
15083
  }
14733
15084
  }
14734
15085
  readStagedPendingLaunch() {
14735
- let raw = null;
14736
15086
  try {
14737
- raw = localStorage.getItem(this.stagedPendingLaunchStorageKey);
14738
- const staged = parseStagedPendingLaunch(raw);
14739
- if (raw && !staged) {
15087
+ const v2Raw = localStorage.getItem(this.stagedPendingLaunchStorageKey);
15088
+ const parsedV2 = parseStagedPendingLaunch(v2Raw);
15089
+ const v2 = parsedV2?.v === 2 ? parsedV2 : null;
15090
+ if (v2Raw && !v2) {
14740
15091
  localStorage.removeItem(this.stagedPendingLaunchStorageKey);
14741
15092
  }
14742
- return staged;
15093
+ const legacyRaw = localStorage.getItem(this.legacyStagedPendingLaunchStorageKey);
15094
+ const parsedLegacy = parseStagedPendingLaunch(legacyRaw);
15095
+ const legacy = parsedLegacy?.v === 1 ? parsedLegacy : null;
15096
+ if (legacyRaw && !legacy) {
15097
+ localStorage.removeItem(this.legacyStagedPendingLaunchStorageKey);
15098
+ }
15099
+ if (!v2)
15100
+ return legacy;
15101
+ if (!legacy)
15102
+ return v2;
15103
+ const authorityOrder = compareTransferAuthority(v2.authority, legacy.authority);
15104
+ if (authorityOrder !== 0)
15105
+ return authorityOrder > 0 ? v2 : legacy;
15106
+ if (v2.sourceOwnerId !== legacy.sourceOwnerId ||
15107
+ !this.pendingLaunchBaseMatches(v2.pendingLaunch, legacy.pendingLaunch)) {
15108
+ return null;
15109
+ }
15110
+ // A V2 record is the only copy that preserves the exact launch body.
15111
+ // Prefer it for an equal authority; the legacy copy is cleaned only
15112
+ // after a matching V2 write has been verified.
15113
+ return v2;
14743
15114
  }
14744
15115
  catch {
14745
15116
  return null;
14746
15117
  }
14747
15118
  }
14748
15119
  persistStagedPendingLaunch(staged) {
15120
+ let rollback = null;
14749
15121
  try {
14750
- localStorage.setItem(this.stagedPendingLaunchStorageKey, JSON.stringify(staged));
14751
- return (canonicalJson(this.readStagedPendingLaunch()) === canonicalJson(staged));
15122
+ const serializedPendingLaunch = staged.v === 1
15123
+ ? {
15124
+ mode: staged.pendingLaunch.mode,
15125
+ scopes: [...staged.pendingLaunch.scopes],
15126
+ idempotencyKey: staged.pendingLaunch.idempotencyKey,
15127
+ }
15128
+ : staged.pendingLaunch;
15129
+ const storageKey = staged.v === 2
15130
+ ? this.stagedPendingLaunchStorageKey
15131
+ : this.legacyStagedPendingLaunchStorageKey;
15132
+ const serialized = JSON.stringify({
15133
+ ...staged,
15134
+ pendingLaunch: serializedPendingLaunch,
15135
+ });
15136
+ const previousSerialized = localStorage.getItem(storageKey);
15137
+ const previousLegacySerialized = localStorage.getItem(this.legacyStagedPendingLaunchStorageKey);
15138
+ let removedMatchingLegacy = false;
15139
+ rollback = () => {
15140
+ if (localStorage.getItem(storageKey) === serialized) {
15141
+ if (previousSerialized === null)
15142
+ localStorage.removeItem(storageKey);
15143
+ else
15144
+ localStorage.setItem(storageKey, previousSerialized);
15145
+ }
15146
+ if (removedMatchingLegacy &&
15147
+ previousLegacySerialized !== null &&
15148
+ localStorage.getItem(this.legacyStagedPendingLaunchStorageKey) ===
15149
+ null) {
15150
+ localStorage.setItem(this.legacyStagedPendingLaunchStorageKey, previousLegacySerialized);
15151
+ }
15152
+ };
15153
+ localStorage.setItem(storageKey, serialized);
15154
+ const normalizedPendingLaunch = staged.v === 1
15155
+ ? {
15156
+ ...serializedPendingLaunch,
15157
+ protocolVersion: null,
15158
+ experienceMode: null,
15159
+ }
15160
+ : staged.pendingLaunch;
15161
+ const expected = {
15162
+ ...staged,
15163
+ pendingLaunch: normalizedPendingLaunch,
15164
+ };
15165
+ const directReadback = parseStagedPendingLaunch(localStorage.getItem(storageKey));
15166
+ if (canonicalJson(directReadback) !== canonicalJson(expected)) {
15167
+ rollback();
15168
+ return false;
15169
+ }
15170
+ if (staged.v === 2) {
15171
+ const legacySerialized = localStorage.getItem(this.legacyStagedPendingLaunchStorageKey);
15172
+ const legacy = parseStagedPendingLaunch(legacySerialized);
15173
+ const equalAuthority = legacy?.v === 1 &&
15174
+ compareTransferAuthority(legacy.authority, staged.authority) === 0;
15175
+ if (equalAuthority &&
15176
+ (legacy.sourceOwnerId !== staged.sourceOwnerId ||
15177
+ !this.pendingLaunchBaseMatches(legacy.pendingLaunch, staged.pendingLaunch))) {
15178
+ rollback();
15179
+ return false;
15180
+ }
15181
+ if (legacy?.v === 1 &&
15182
+ legacy.sourceOwnerId === staged.sourceOwnerId &&
15183
+ equalAuthority &&
15184
+ this.pendingLaunchBaseMatches(legacy.pendingLaunch, staged.pendingLaunch) &&
15185
+ legacySerialized !== null &&
15186
+ localStorage.getItem(this.legacyStagedPendingLaunchStorageKey) ===
15187
+ legacySerialized) {
15188
+ localStorage.removeItem(this.legacyStagedPendingLaunchStorageKey);
15189
+ removedMatchingLegacy = true;
15190
+ }
15191
+ }
15192
+ const persisted = canonicalJson(this.readStagedPendingLaunch()) ===
15193
+ canonicalJson(expected);
15194
+ if (!persisted)
15195
+ rollback();
15196
+ return persisted;
14752
15197
  }
14753
15198
  catch {
15199
+ try {
15200
+ rollback?.();
15201
+ }
15202
+ catch {
15203
+ // Storage is already unavailable. Leave the launch unclaimed and
15204
+ // fail closed rather than trusting a partially observed write.
15205
+ }
14754
15206
  return false;
14755
15207
  }
14756
15208
  }
15209
+ pendingLaunchBaseMatches(left, right) {
15210
+ return (canonicalJson({
15211
+ mode: left.mode,
15212
+ scopes: left.scopes,
15213
+ idempotencyKey: left.idempotencyKey,
15214
+ }) ===
15215
+ canonicalJson({
15216
+ mode: right.mode,
15217
+ scopes: right.scopes,
15218
+ idempotencyKey: right.idempotencyKey,
15219
+ }));
15220
+ }
14757
15221
  pendingLaunchMatches(left, right) {
14758
- return canonicalJson(left) === canonicalJson(right);
15222
+ const leftBound = pendingLaunchIsBound(left);
15223
+ const rightBound = pendingLaunchIsBound(right);
15224
+ if (leftBound !== rightBound)
15225
+ return false;
15226
+ if (leftBound && rightBound) {
15227
+ return canonicalJson(left) === canonicalJson(right);
15228
+ }
15229
+ return this.pendingLaunchBaseMatches(left, right);
14759
15230
  }
14760
15231
  stagePendingLaunch(pendingLaunch, authority, sourceOwnerId) {
14761
- if (!PUBLIC_REQUEST_ID_PATTERN.test(sourceOwnerId) ||
15232
+ const boundPendingLaunch = this.bootstrap
15233
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15234
+ : null;
15235
+ if (!boundPendingLaunch ||
15236
+ !PUBLIC_REQUEST_ID_PATTERN.test(sourceOwnerId) ||
14762
15237
  sourceOwnerId === authority.ownerId) {
14763
15238
  return false;
14764
15239
  }
@@ -14769,35 +15244,82 @@ class AiLiveAssist {
14769
15244
  if (authorityOrder < 0 ||
14770
15245
  (authorityOrder === 0 &&
14771
15246
  (existing?.sourceOwnerId !== sourceOwnerId ||
14772
- !this.pendingLaunchMatches(existing.pendingLaunch, pendingLaunch) ||
15247
+ !this.pendingLaunchMatches(!pendingLaunchIsBound(existing.pendingLaunch) && this.bootstrap
15248
+ ? (bindPendingLaunchIntent(existing.pendingLaunch, this.bootstrap) ?? existing.pendingLaunch)
15249
+ : existing.pendingLaunch, boundPendingLaunch) ||
14773
15250
  existing.state !== "prepared"))) {
14774
15251
  return false;
14775
15252
  }
14776
15253
  return this.persistStagedPendingLaunch({
14777
- v: 1,
15254
+ v: 2,
14778
15255
  sourceOwnerId,
14779
15256
  authority: { ...authority },
14780
15257
  state: "prepared",
14781
15258
  claimAuthority: null,
14782
15259
  expiresAt: Date.now() + STAGED_PENDING_LAUNCH_TTL_MS,
14783
15260
  pendingLaunch: {
14784
- ...pendingLaunch,
14785
- scopes: [...pendingLaunch.scopes],
15261
+ ...boundPendingLaunch,
15262
+ scopes: [...boundPendingLaunch.scopes],
14786
15263
  },
14787
15264
  });
14788
15265
  }
14789
15266
  clearStagedPendingLaunch(pendingLaunch, authority) {
14790
15267
  const staged = this.readStagedPendingLaunch();
15268
+ const comparablePending = this.bootstrap && !pendingLaunchIsBound(pendingLaunch)
15269
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15270
+ : pendingLaunch;
15271
+ const comparableStaged = this.bootstrap && staged && !pendingLaunchIsBound(staged.pendingLaunch)
15272
+ ? bindPendingLaunchIntent(staged.pendingLaunch, this.bootstrap)
15273
+ : staged?.pendingLaunch;
14791
15274
  if (!staged ||
15275
+ !comparablePending ||
15276
+ !comparableStaged ||
14792
15277
  compareTransferAuthority(staged.authority, authority) !== 0 ||
14793
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
14794
- return;
15278
+ !this.pendingLaunchMatches(comparableStaged, comparablePending)) {
15279
+ return false;
14795
15280
  }
14796
15281
  try {
14797
- localStorage.removeItem(this.stagedPendingLaunchStorageKey);
15282
+ const snapshots = [
15283
+ {
15284
+ key: this.stagedPendingLaunchStorageKey,
15285
+ raw: localStorage.getItem(this.stagedPendingLaunchStorageKey),
15286
+ version: 2,
15287
+ },
15288
+ {
15289
+ key: this.legacyStagedPendingLaunchStorageKey,
15290
+ raw: localStorage.getItem(this.legacyStagedPendingLaunchStorageKey),
15291
+ version: 1,
15292
+ },
15293
+ ];
15294
+ const removable = [];
15295
+ for (const snapshot of snapshots) {
15296
+ if (snapshot.raw === null)
15297
+ continue;
15298
+ const shadow = parseStagedPendingLaunch(snapshot.raw);
15299
+ if (!shadow || shadow.v !== snapshot.version)
15300
+ return false;
15301
+ const authorityOrder = compareTransferAuthority(shadow.authority, staged.authority);
15302
+ if (authorityOrder > 0)
15303
+ continue;
15304
+ if (authorityOrder === 0 &&
15305
+ (shadow.sourceOwnerId !== staged.sourceOwnerId ||
15306
+ !this.pendingLaunchBaseMatches(shadow.pendingLaunch, staged.pendingLaunch))) {
15307
+ return false;
15308
+ }
15309
+ removable.push(snapshot);
15310
+ }
15311
+ if (removable.some((snapshot) => localStorage.getItem(snapshot.key) !== snapshot.raw)) {
15312
+ return false;
15313
+ }
15314
+ for (const snapshot of removable) {
15315
+ if (localStorage.getItem(snapshot.key) !== snapshot.raw)
15316
+ return false;
15317
+ localStorage.removeItem(snapshot.key);
15318
+ }
15319
+ return removable.every((snapshot) => localStorage.getItem(snapshot.key) === null);
14798
15320
  }
14799
15321
  catch {
14800
- /* noop */
15322
+ return false;
14801
15323
  }
14802
15324
  }
14803
15325
  clearOwnedStagedPendingLaunch() {
@@ -14814,7 +15336,7 @@ class AiLiveAssist {
14814
15336
  const staged = this.readStagedPendingLaunch();
14815
15337
  if (!pendingLaunch ||
14816
15338
  !staged ||
14817
- staged.pendingLaunch.idempotencyKey !== pendingLaunch.idempotencyKey ||
15339
+ !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch) ||
14818
15340
  this.terminalEndIntent?.launchIdempotencyKey !==
14819
15341
  pendingLaunch.idempotencyKey) {
14820
15342
  return;
@@ -14825,11 +15347,15 @@ class AiLiveAssist {
14825
15347
  const pendingLaunch = parseFocusedHandoffPayload(value)?.pendingLaunch;
14826
15348
  if (!pendingLaunch)
14827
15349
  return true;
15350
+ const boundPendingLaunch = this.bootstrap
15351
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15352
+ : null;
14828
15353
  const staged = this.readStagedPendingLaunch();
14829
- if (!staged ||
15354
+ if (!boundPendingLaunch ||
15355
+ !staged ||
14830
15356
  staged.state !== "prepared" ||
14831
15357
  compareTransferAuthority(staged.authority, authority) !== 0 ||
14832
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15358
+ !this.pendingLaunchMatches(staged.pendingLaunch, boundPendingLaunch)) {
14833
15359
  return false;
14834
15360
  }
14835
15361
  return this.persistStagedPendingLaunch({
@@ -14873,6 +15399,12 @@ class AiLiveAssist {
14873
15399
  }
14874
15400
  markStagedPendingLaunchInFlight(pendingLaunch) {
14875
15401
  const staged = this.readStagedPendingLaunch();
15402
+ const boundPendingLaunch = pendingLaunchIsBound(pendingLaunch)
15403
+ ? pendingLaunch
15404
+ : this.bootstrap
15405
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15406
+ : null;
15407
+ const durablePendingLaunch = boundPendingLaunch ?? pendingLaunch;
14876
15408
  // AiLiveAssist always installs a TabActivityLock with durable authority.
14877
15409
  // Keep prototype-level test harnesses that intentionally omit the lock
14878
15410
  // implementation isolated from the persistence contract.
@@ -14884,40 +15416,54 @@ class AiLiveAssist {
14884
15416
  }
14885
15417
  if (!staged) {
14886
15418
  return this.persistStagedPendingLaunch({
14887
- v: 1,
15419
+ v: boundPendingLaunch ? 2 : 1,
14888
15420
  sourceOwnerId: randomId("launch_source"),
14889
15421
  authority: { ...ownedAuthority },
14890
15422
  state: "in_flight",
14891
15423
  claimAuthority: { ...ownedAuthority },
14892
15424
  expiresAt: Date.now() + STAGED_PENDING_LAUNCH_TTL_MS,
14893
15425
  pendingLaunch: {
14894
- ...pendingLaunch,
14895
- scopes: [...pendingLaunch.scopes],
15426
+ ...durablePendingLaunch,
15427
+ scopes: [...durablePendingLaunch.scopes],
14896
15428
  },
14897
15429
  });
14898
15430
  }
15431
+ const comparableStagedPendingLaunch = pendingLaunchIsBound(staged.pendingLaunch)
15432
+ ? staged.pendingLaunch
15433
+ : this.bootstrap
15434
+ ? bindPendingLaunchIntent(staged.pendingLaunch, this.bootstrap)
15435
+ : null;
14899
15436
  if (staged.pendingLaunch.idempotencyKey !== pendingLaunch.idempotencyKey ||
14900
15437
  !staged.claimAuthority ||
14901
15438
  compareTransferAuthority(staged.claimAuthority, ownedAuthority) !== 0 ||
14902
- (staged.state !== "claimed" && staged.state !== "in_flight")) {
15439
+ (staged.state !== "claimed" && staged.state !== "in_flight") ||
15440
+ (boundPendingLaunch
15441
+ ? !comparableStagedPendingLaunch ||
15442
+ !this.pendingLaunchMatches(comparableStagedPendingLaunch, boundPendingLaunch)
15443
+ : !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch))) {
14903
15444
  return false;
14904
15445
  }
14905
- if (staged.state === "in_flight")
15446
+ if (staged.state === "in_flight" &&
15447
+ staged.v === (boundPendingLaunch ? 2 : 1) &&
15448
+ this.pendingLaunchMatches(staged.pendingLaunch, durablePendingLaunch)) {
14906
15449
  return true;
15450
+ }
14907
15451
  return this.persistStagedPendingLaunch({
14908
15452
  ...staged,
15453
+ v: boundPendingLaunch ? 2 : 1,
14909
15454
  state: "in_flight",
14910
15455
  pendingLaunch: {
14911
- ...pendingLaunch,
14912
- scopes: [...pendingLaunch.scopes],
15456
+ ...durablePendingLaunch,
15457
+ scopes: [...durablePendingLaunch.scopes],
14913
15458
  },
14914
15459
  });
14915
15460
  }
14916
15461
  acceptStagedPendingLaunch(pendingLaunch, result) {
14917
15462
  const staged = this.readStagedPendingLaunch();
14918
- if (!staged ||
14919
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15463
+ if (!staged)
14920
15464
  return true;
15465
+ if (!this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15466
+ return false;
14921
15467
  }
14922
15468
  const ownedAuthority = this.lock.ownedAuthority?.() ?? null;
14923
15469
  const stored = this.readResume();
@@ -14941,17 +15487,11 @@ class AiLiveAssist {
14941
15487
  })) {
14942
15488
  return false;
14943
15489
  }
14944
- try {
14945
- localStorage.removeItem(this.stagedPendingLaunchStorageKey);
14946
- return localStorage.getItem(this.stagedPendingLaunchStorageKey) === null;
14947
- }
14948
- catch {
14949
- return false;
14950
- }
15490
+ return this.clearStagedPendingLaunch(staged.pendingLaunch, staged.authority);
14951
15491
  }
14952
15492
  hasStagedPendingLaunch(pendingLaunch) {
14953
- return (this.readStagedPendingLaunch()?.pendingLaunch.idempotencyKey ===
14954
- pendingLaunch.idempotencyKey);
15493
+ const staged = this.readStagedPendingLaunch();
15494
+ return Boolean(staged && this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch));
14955
15495
  }
14956
15496
  storeTransferAuthority(authority) {
14957
15497
  const current = this.readTransferAuthority();
@@ -15029,12 +15569,21 @@ class AiLiveAssist {
15029
15569
  if (!this.freshAiReturnProvisionalStorageKey)
15030
15570
  return null;
15031
15571
  try {
15032
- const serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15033
- const provisional = parseFreshAiReturnProvisional(serialized ? JSON.parse(serialized) : null);
15034
- if (serialized && !provisional) {
15572
+ const v2Serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15573
+ const parsedV2 = parseFreshAiReturnProvisional(v2Serialized ? JSON.parse(v2Serialized) : null);
15574
+ const v2 = parsedV2?.v === 2 ? parsedV2 : null;
15575
+ if (v2Serialized && !v2) {
15035
15576
  sessionStorage.removeItem(this.freshAiReturnProvisionalStorageKey);
15036
15577
  }
15037
- return provisional;
15578
+ if (v2)
15579
+ return v2;
15580
+ const legacySerialized = sessionStorage.getItem(this.legacyFreshAiReturnProvisionalStorageKey);
15581
+ const parsedLegacy = parseFreshAiReturnProvisional(legacySerialized ? JSON.parse(legacySerialized) : null);
15582
+ const legacy = parsedLegacy?.v === 1 ? parsedLegacy : null;
15583
+ if (legacySerialized && !legacy) {
15584
+ sessionStorage.removeItem(this.legacyFreshAiReturnProvisionalStorageKey);
15585
+ }
15586
+ return legacy;
15038
15587
  }
15039
15588
  catch {
15040
15589
  return null;
@@ -15044,9 +15593,54 @@ class AiLiveAssist {
15044
15593
  if (!this.freshAiReturnProvisionalStorageKey)
15045
15594
  return false;
15046
15595
  try {
15047
- sessionStorage.setItem(this.freshAiReturnProvisionalStorageKey, JSON.stringify(provisional));
15596
+ const serialized = provisional.v === 1
15597
+ ? {
15598
+ v: 1,
15599
+ linkId: provisional.linkId,
15600
+ conversationDisplayId: provisional.conversationDisplayId,
15601
+ mode: provisional.mode,
15602
+ phase: provisional.phase,
15603
+ launchIdempotencyKey: provisional.launchIdempotencyKey,
15604
+ sessionId: provisional.sessionId,
15605
+ expiresAt: provisional.expiresAt,
15606
+ }
15607
+ : provisional;
15608
+ const storageKey = provisional.v === 2
15609
+ ? this.freshAiReturnProvisionalStorageKey
15610
+ : this.legacyFreshAiReturnProvisionalStorageKey;
15611
+ sessionStorage.setItem(storageKey, JSON.stringify(serialized));
15612
+ const normalized = provisional.v === 1
15613
+ ? {
15614
+ ...provisional,
15615
+ protocolVersion: null,
15616
+ experienceMode: null,
15617
+ scopes: null,
15618
+ }
15619
+ : provisional;
15620
+ const directReadback = parseFreshAiReturnProvisional(JSON.parse(sessionStorage.getItem(storageKey) ?? "null"));
15621
+ if (canonicalJson(directReadback) !== canonicalJson(normalized)) {
15622
+ return false;
15623
+ }
15624
+ if (provisional.v === 2) {
15625
+ const legacySerialized = sessionStorage.getItem(this.legacyFreshAiReturnProvisionalStorageKey);
15626
+ const legacy = parseFreshAiReturnProvisional(legacySerialized ? JSON.parse(legacySerialized) : null);
15627
+ const sameReturnLineage = legacy?.v === 1 &&
15628
+ legacy.linkId === provisional.linkId &&
15629
+ legacy.conversationDisplayId === provisional.conversationDisplayId &&
15630
+ legacy.mode === provisional.mode &&
15631
+ legacy.expiresAt === provisional.expiresAt;
15632
+ if (sameReturnLineage &&
15633
+ (legacy.phase === "released" ||
15634
+ provisional.phase === "released" ||
15635
+ (legacy.phase === provisional.phase &&
15636
+ legacy.launchIdempotencyKey ===
15637
+ provisional.launchIdempotencyKey &&
15638
+ legacy.sessionId === provisional.sessionId))) {
15639
+ sessionStorage.removeItem(this.legacyFreshAiReturnProvisionalStorageKey);
15640
+ }
15641
+ }
15048
15642
  return (canonicalJson(this.readFreshAiReturnProvisional()) ===
15049
- canonicalJson(provisional));
15643
+ canonicalJson(normalized));
15050
15644
  }
15051
15645
  catch {
15052
15646
  return false;
@@ -15056,19 +15650,21 @@ class AiLiveAssist {
15056
15650
  if (!this.freshAiReturnProvisionalStorageKey)
15057
15651
  return;
15058
15652
  try {
15059
- const serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15653
+ const storageKey = expected?.v === 1
15654
+ ? this.legacyFreshAiReturnProvisionalStorageKey
15655
+ : this.freshAiReturnProvisionalStorageKey;
15656
+ const serialized = sessionStorage.getItem(storageKey);
15060
15657
  if (!serialized)
15061
15658
  return;
15062
15659
  if (expected) {
15063
15660
  const stored = parseFreshAiReturnProvisional(JSON.parse(serialized));
15064
15661
  if (canonicalJson(stored) !== canonicalJson(expected))
15065
15662
  return;
15066
- if (sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey) !==
15067
- serialized) {
15663
+ if (sessionStorage.getItem(storageKey) !== serialized) {
15068
15664
  return;
15069
15665
  }
15070
15666
  }
15071
- sessionStorage.removeItem(this.freshAiReturnProvisionalStorageKey);
15667
+ sessionStorage.removeItem(storageKey);
15072
15668
  }
15073
15669
  catch {
15074
15670
  /* noop */
@@ -15659,10 +16255,7 @@ class AiLiveAssist {
15659
16255
  !this.view ||
15660
16256
  (record &&
15661
16257
  (!record.tabId ||
15662
- record.protocolVersion !==
15663
- (0, exports.resolveAiGuideProtocolVersion)(this.bootstrap) ||
15664
- record.experienceMode !==
15665
- (this.bootstrap.experienceMode ?? "standard"))) ||
16258
+ !resumeProtocolIsSupported(record, this.bootstrap))) ||
15666
16259
  (pendingLaunch &&
15667
16260
  (this.visitorActiveTakeoverOnly ||
15668
16261
  ((this.bootstrap.experienceMode ?? "standard") === "voice_presence" &&
@@ -15698,10 +16291,11 @@ class AiLiveAssist {
15698
16291
  this.lock.release();
15699
16292
  };
15700
16293
  const preparedMatches = record
15701
- ? prepared?.resume?.sessionId === record.sessionId
16294
+ ? Boolean(prepared?.resume &&
16295
+ this.sameResumeAuthorityIdentity(prepared.resume, record))
15702
16296
  : Boolean(pendingLaunch &&
15703
- prepared?.pendingLaunch?.idempotencyKey ===
15704
- pendingLaunch.idempotencyKey);
16297
+ prepared?.pendingLaunch &&
16298
+ this.pendingLaunchMatches(prepared.pendingLaunch, pendingLaunch));
15705
16299
  if (!payload ||
15706
16300
  (!record && !pendingLaunch) ||
15707
16301
  !this.bootstrap ||
@@ -15714,10 +16308,7 @@ class AiLiveAssist {
15714
16308
  !preparedMatches ||
15715
16309
  (record &&
15716
16310
  (!record.tabId ||
15717
- record.protocolVersion !==
15718
- (0, exports.resolveAiGuideProtocolVersion)(this.bootstrap) ||
15719
- record.experienceMode !==
15720
- (this.bootstrap.experienceMode ?? "standard"))) ||
16311
+ !resumeProtocolIsSupported(record, this.bootstrap))) ||
15721
16312
  (pendingLaunch && this.visitorActiveTakeoverOnly)) {
15722
16313
  abandonUnusableCommit();
15723
16314
  return;
@@ -15729,7 +16320,7 @@ class AiLiveAssist {
15729
16320
  return;
15730
16321
  }
15731
16322
  if (this.lastLaunch &&
15732
- this.lastLaunch.idempotencyKey !== stagedPending.idempotencyKey) {
16323
+ !this.pendingLaunchMatches(this.lastLaunch, stagedPending)) {
15733
16324
  this.clearStagedPendingLaunch(stagedPending, authority);
15734
16325
  }
15735
16326
  else {
@@ -15741,6 +16332,12 @@ class AiLiveAssist {
15741
16332
  return;
15742
16333
  }
15743
16334
  const storedResume = record ? this.readResume() : null;
16335
+ if (record &&
16336
+ storedResume?.sessionId === record.sessionId &&
16337
+ !this.sameResumeAuthorityIdentity(record, storedResume)) {
16338
+ abandonUnusableCommit();
16339
+ return;
16340
+ }
15744
16341
  const effectivePayload = record &&
15745
16342
  storedResume?.sessionId === record.sessionId &&
15746
16343
  storedResume.resumeCredential !== record.resumeCredential
@@ -15781,11 +16378,11 @@ class AiLiveAssist {
15781
16378
  completeFocusedTabRelinquish(value) {
15782
16379
  const payload = parseFocusedHandoffPayload(value);
15783
16380
  const sameResume = Boolean(payload?.resume &&
15784
- this.focusedHandoffInFlight?.resume?.sessionId ===
15785
- payload.resume.sessionId);
16381
+ this.focusedHandoffInFlight?.resume &&
16382
+ this.sameResumeAuthorityIdentity(this.focusedHandoffInFlight.resume, payload.resume));
15786
16383
  const samePendingLaunch = Boolean(payload?.pendingLaunch &&
15787
- this.focusedHandoffInFlight?.pendingLaunch?.idempotencyKey ===
15788
- payload.pendingLaunch.idempotencyKey);
16384
+ this.focusedHandoffInFlight?.pendingLaunch &&
16385
+ this.pendingLaunchMatches(this.focusedHandoffInFlight.pendingLaunch, payload.pendingLaunch));
15789
16386
  if (!payload || (!sameResume && !samePendingLaunch)) {
15790
16387
  return;
15791
16388
  }
@@ -15816,8 +16413,8 @@ class AiLiveAssist {
15816
16413
  this.clearStagedPendingLaunch(pendingLaunch, authority);
15817
16414
  }
15818
16415
  if (pendingLaunch &&
15819
- this.focusedHandoffInFlight?.pendingLaunch?.idempotencyKey ===
15820
- pendingLaunch.idempotencyKey) {
16416
+ this.focusedHandoffInFlight?.pendingLaunch &&
16417
+ this.pendingLaunchMatches(this.focusedHandoffInFlight.pendingLaunch, pendingLaunch)) {
15821
16418
  this.focusedHandoffInFlight = null;
15822
16419
  if (this.terminalRequestPending ||
15823
16420
  this.ended ||
@@ -15839,7 +16436,7 @@ class AiLiveAssist {
15839
16436
  queueMicrotask(() => {
15840
16437
  const retry = this.lastLaunch;
15841
16438
  if (!retry ||
15842
- retry.idempotencyKey !== pendingLaunch.idempotencyKey ||
16439
+ !this.pendingLaunchMatches(retry, pendingLaunch) ||
15843
16440
  this.terminalRequestPending ||
15844
16441
  this.ended ||
15845
16442
  this.destroyed ||
@@ -15848,7 +16445,9 @@ class AiLiveAssist {
15848
16445
  return;
15849
16446
  }
15850
16447
  this.lastLaunch = null;
15851
- void this.launch(retry.mode, retry.scopes, retry.idempotencyKey);
16448
+ void this.launch(retry.mode, retry.scopes, retry.idempotencyKey, {
16449
+ pendingLaunch: retry,
16450
+ });
15852
16451
  });
15853
16452
  return;
15854
16453
  }
@@ -15914,7 +16513,7 @@ class AiLiveAssist {
15914
16513
  this.view &&
15915
16514
  !this.visitorActiveTakeoverOnly) {
15916
16515
  this.lastLaunch = null;
15917
- await this.launch(deferredLaunch.mode, deferredLaunch.scopes, deferredLaunch.idempotencyKey);
16516
+ await this.launch(deferredLaunch.mode, deferredLaunch.scopes, deferredLaunch.idempotencyKey, { pendingLaunch: deferredLaunch });
15918
16517
  return;
15919
16518
  }
15920
16519
  this.lock.release();