@oxvo/ai-live-assist 7.4.36 → 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") ||
@@ -7358,6 +7648,7 @@ class AiLiveAssist {
7358
7648
  ? null
7359
7649
  : (result.assistant?.welcomeMessage ??
7360
7650
  this.bootstrap?.appearance?.welcomeMessage);
7651
+ let renderedPendingTextQuestion = false;
7361
7652
  if (mode === "text" &&
7362
7653
  this.pendingQuestion &&
7363
7654
  !this.pendingQuestion.shown) {
@@ -7365,8 +7656,11 @@ class AiLiveAssist {
7365
7656
  this.pendingQuestion.shown = true;
7366
7657
  this.rememberTranscript(this.pendingQuestion.submissionId, "user", this.pendingQuestion.text);
7367
7658
  (this.renderedTranscriptIds ?? (this.renderedTranscriptIds = new Set())).add(this.pendingQuestion.submissionId);
7659
+ renderedPendingTextQuestion = true;
7368
7660
  }
7369
- const visibleWelcome = this.transcriptRestoredFromCache ||
7661
+ const visibleWelcome = resume ||
7662
+ renderedPendingTextQuestion ||
7663
+ this.transcriptRestoredFromCache ||
7370
7664
  (this.view instanceof voicePresenceUi_js_1.VoicePresenceView && mode === "voice")
7371
7665
  ? null
7372
7666
  : welcomeMessage;
@@ -13101,7 +13395,7 @@ class AiLiveAssist {
13101
13395
  return;
13102
13396
  }
13103
13397
  this.messengerReturnRetryRemaining -= 1;
13104
- 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 });
13105
13399
  return retryAttempt.finally(() => {
13106
13400
  if (this.result && !this.ended) {
13107
13401
  this.messengerReturnRetryRemaining = null;
@@ -13121,7 +13415,7 @@ class AiLiveAssist {
13121
13415
  return this.resume(resume);
13122
13416
  }
13123
13417
  if (this.lastLaunch) {
13124
- 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 });
13125
13419
  }
13126
13420
  }
13127
13421
  scheduleReconnect(force = false) {
@@ -13451,6 +13745,18 @@ class AiLiveAssist {
13451
13745
  canonicalJson(left.messengerBinding) ===
13452
13746
  canonicalJson(right.messengerBinding));
13453
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
+ }
13454
13760
  sameModeUpgradeIntent(left, right) {
13455
13761
  return (left?.requestId === right?.requestId &&
13456
13762
  left?.resumeIdempotencyKey === right?.resumeIdempotencyKey &&
@@ -13819,10 +14125,22 @@ class AiLiveAssist {
13819
14125
  this.state("ended");
13820
14126
  }
13821
14127
  captureTerminalAuthorizedResult(result, sourceRecord = null) {
13822
- if (sourceRecord &&
13823
- (result.sessionId !== sourceRecord.sessionId ||
13824
- result.protocolVersion !== sourceRecord.protocolVersion ||
13825
- 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))) {
13826
14144
  return false;
13827
14145
  }
13828
14146
  const currentIntent = this.terminalEndIntent;
@@ -13848,6 +14166,22 @@ class AiLiveAssist {
13848
14166
  }
13849
14167
  return true;
13850
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
+ }
13851
14185
  completeRuntimeEnd() {
13852
14186
  if (this.ended)
13853
14187
  return Promise.resolve();
@@ -13878,8 +14212,9 @@ class AiLiveAssist {
13878
14212
  if (terminalAuthorization) {
13879
14213
  return this.waitForTerminalRequestDeadline(terminalAuthorization, this.terminalSessionAuthorizationController).then((authorized) => {
13880
14214
  const result = authorized ?? this.result ?? this.modeUpgradeTerminalResult;
13881
- if (result) {
13882
- this.captureTerminalAuthorizedResult(result, this.terminalResumeRecord);
14215
+ if (result &&
14216
+ !this.captureTerminalAuthorizedResult(result, this.terminalResumeRecord)) {
14217
+ return this.retryRejectedTerminalAuthorizedResult(result);
13883
14218
  }
13884
14219
  return this.finishRuntimeEndAttempt(result);
13885
14220
  }, (error) => {
@@ -13896,7 +14231,9 @@ class AiLiveAssist {
13896
14231
  }
13897
14232
  if (this.terminalPendingLaunch) {
13898
14233
  return this.requestTerminalPendingLaunch(this.terminalPendingLaunch).then((result) => {
13899
- this.captureTerminalAuthorizedResult(result);
14234
+ if (!this.captureTerminalAuthorizedResult(result)) {
14235
+ return this.retryRejectedTerminalAuthorizedResult(result);
14236
+ }
13900
14237
  return this.finishRuntimeEndAttempt(result);
13901
14238
  }, (recoveryError) => this.retryTerminalEnd(recoveryError));
13902
14239
  }
@@ -13923,7 +14260,9 @@ class AiLiveAssist {
13923
14260
  return this.finishRuntimeEndAttempt(null);
13924
14261
  if (this.terminalPendingLaunch) {
13925
14262
  return this.requestTerminalPendingLaunch(this.terminalPendingLaunch).then((result) => {
13926
- this.captureTerminalAuthorizedResult(result);
14263
+ if (!this.captureTerminalAuthorizedResult(result)) {
14264
+ return this.retryRejectedTerminalAuthorizedResult(result);
14265
+ }
13927
14266
  return this.finishRuntimeEndAttempt(result);
13928
14267
  }, (error) => this.retryTerminalEnd(error));
13929
14268
  }
@@ -14080,13 +14419,21 @@ class AiLiveAssist {
14080
14419
  requestTerminalPendingLaunch(pendingLaunch) {
14081
14420
  const intent = this.terminalEndIntent;
14082
14421
  if (!intent ||
14083
- intent.launchIdempotencyKey !== pendingLaunch.idempotencyKey ||
14084
- !this.markStagedPendingLaunchInFlight(pendingLaunch)) {
14422
+ intent.launchIdempotencyKey !== pendingLaunch.idempotencyKey) {
14085
14423
  return Promise.reject(new TerminalRequestDeferredError());
14086
14424
  }
14087
14425
  const controller = new AbortController();
14088
14426
  const request = (async () => {
14089
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
+ };
14090
14437
  const replay = await this.replayReadiness.wait();
14091
14438
  if (this.destroyed ||
14092
14439
  this.ended ||
@@ -14095,28 +14442,34 @@ class AiLiveAssist {
14095
14442
  throw new TerminalRequestDeferredError();
14096
14443
  }
14097
14444
  this.replayReadiness.assertCurrent(replay);
14098
- return this.runtime.launch({
14445
+ const result = await this.runtime.launch({
14099
14446
  siteKey: this.siteKey,
14100
14447
  configRevision: bootstrap.configRevision,
14101
14448
  path: path(),
14102
14449
  tabId: this.tabId,
14103
- mode: pendingLaunch.mode,
14450
+ mode: boundPendingLaunch.mode,
14104
14451
  anonymousId: this.visitorId(),
14105
14452
  replay,
14106
14453
  disclosureChecksum: bootstrap.consent.disclosureChecksum,
14107
- scopes: pendingLaunch.scopes,
14454
+ scopes: boundPendingLaunch.scopes,
14108
14455
  locale: this.locale,
14109
- microphone: pendingLaunch.mode === "voice",
14456
+ microphone: boundPendingLaunch.mode === "voice",
14110
14457
  reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)")
14111
14458
  .matches,
14112
- protocolVersion: (0, exports.resolveAiGuideProtocolVersion)(bootstrap),
14113
- experienceMode: bootstrap.experienceMode ?? "standard",
14459
+ protocolVersion: boundPendingLaunch.protocolVersion,
14460
+ experienceMode: boundPendingLaunch.experienceMode,
14114
14461
  ...(this.activeFallback
14115
14462
  ? { fallbackIntent: this.activeFallback }
14116
14463
  : {}),
14117
14464
  lowPower: this.lowPowerMode(),
14118
- idempotencyKey: pendingLaunch.idempotencyKey,
14465
+ idempotencyKey: boundPendingLaunch.idempotencyKey,
14119
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;
14120
14473
  })();
14121
14474
  return this.waitForTerminalRequestDeadline(request, controller);
14122
14475
  }
@@ -14280,8 +14633,7 @@ class AiLiveAssist {
14280
14633
  const retainTerminalLaunch = Boolean(this.terminalSessionAuthorization &&
14281
14634
  this.terminalPendingLaunch &&
14282
14635
  staged?.state === "in_flight" &&
14283
- staged.pendingLaunch.idempotencyKey ===
14284
- this.terminalPendingLaunch.idempotencyKey);
14636
+ this.pendingLaunchMatches(staged.pendingLaunch, this.terminalPendingLaunch));
14285
14637
  if (!retainTerminalLaunch)
14286
14638
  this.clearOwnedStagedPendingLaunch();
14287
14639
  this.lock?.cancelPendingHandoff?.();
@@ -14705,15 +15057,18 @@ class AiLiveAssist {
14705
15057
  focusedHandoffPayload() {
14706
15058
  this.transcript ?? (this.transcript = []);
14707
15059
  const resume = this.readResume();
15060
+ const pendingLaunch = !resume && this.lastLaunch && this.bootstrap
15061
+ ? bindPendingLaunchIntent(this.lastLaunch, this.bootstrap)
15062
+ : null;
14708
15063
  return {
14709
- v: 1,
15064
+ v: pendingLaunch ? 2 : 1,
14710
15065
  resume,
14711
15066
  transcript: this.transcript.map((entry) => ({ ...entry })),
14712
- ...(!resume && this.lastLaunch
15067
+ ...(pendingLaunch
14713
15068
  ? {
14714
15069
  pendingLaunch: {
14715
- ...this.lastLaunch,
14716
- scopes: [...this.lastLaunch.scopes],
15070
+ ...pendingLaunch,
15071
+ scopes: [...pendingLaunch.scopes],
14717
15072
  },
14718
15073
  }
14719
15074
  : {}),
@@ -14728,33 +15083,157 @@ class AiLiveAssist {
14728
15083
  }
14729
15084
  }
14730
15085
  readStagedPendingLaunch() {
14731
- let raw = null;
14732
15086
  try {
14733
- raw = localStorage.getItem(this.stagedPendingLaunchStorageKey);
14734
- const staged = parseStagedPendingLaunch(raw);
14735
- 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) {
14736
15091
  localStorage.removeItem(this.stagedPendingLaunchStorageKey);
14737
15092
  }
14738
- 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;
14739
15114
  }
14740
15115
  catch {
14741
15116
  return null;
14742
15117
  }
14743
15118
  }
14744
15119
  persistStagedPendingLaunch(staged) {
15120
+ let rollback = null;
14745
15121
  try {
14746
- localStorage.setItem(this.stagedPendingLaunchStorageKey, JSON.stringify(staged));
14747
- 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;
14748
15197
  }
14749
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
+ }
14750
15206
  return false;
14751
15207
  }
14752
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
+ }
14753
15221
  pendingLaunchMatches(left, right) {
14754
- 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);
14755
15230
  }
14756
15231
  stagePendingLaunch(pendingLaunch, authority, sourceOwnerId) {
14757
- 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) ||
14758
15237
  sourceOwnerId === authority.ownerId) {
14759
15238
  return false;
14760
15239
  }
@@ -14765,35 +15244,82 @@ class AiLiveAssist {
14765
15244
  if (authorityOrder < 0 ||
14766
15245
  (authorityOrder === 0 &&
14767
15246
  (existing?.sourceOwnerId !== sourceOwnerId ||
14768
- !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) ||
14769
15250
  existing.state !== "prepared"))) {
14770
15251
  return false;
14771
15252
  }
14772
15253
  return this.persistStagedPendingLaunch({
14773
- v: 1,
15254
+ v: 2,
14774
15255
  sourceOwnerId,
14775
15256
  authority: { ...authority },
14776
15257
  state: "prepared",
14777
15258
  claimAuthority: null,
14778
15259
  expiresAt: Date.now() + STAGED_PENDING_LAUNCH_TTL_MS,
14779
15260
  pendingLaunch: {
14780
- ...pendingLaunch,
14781
- scopes: [...pendingLaunch.scopes],
15261
+ ...boundPendingLaunch,
15262
+ scopes: [...boundPendingLaunch.scopes],
14782
15263
  },
14783
15264
  });
14784
15265
  }
14785
15266
  clearStagedPendingLaunch(pendingLaunch, authority) {
14786
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;
14787
15274
  if (!staged ||
15275
+ !comparablePending ||
15276
+ !comparableStaged ||
14788
15277
  compareTransferAuthority(staged.authority, authority) !== 0 ||
14789
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
14790
- return;
15278
+ !this.pendingLaunchMatches(comparableStaged, comparablePending)) {
15279
+ return false;
14791
15280
  }
14792
15281
  try {
14793
- 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);
14794
15320
  }
14795
15321
  catch {
14796
- /* noop */
15322
+ return false;
14797
15323
  }
14798
15324
  }
14799
15325
  clearOwnedStagedPendingLaunch() {
@@ -14810,7 +15336,7 @@ class AiLiveAssist {
14810
15336
  const staged = this.readStagedPendingLaunch();
14811
15337
  if (!pendingLaunch ||
14812
15338
  !staged ||
14813
- staged.pendingLaunch.idempotencyKey !== pendingLaunch.idempotencyKey ||
15339
+ !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch) ||
14814
15340
  this.terminalEndIntent?.launchIdempotencyKey !==
14815
15341
  pendingLaunch.idempotencyKey) {
14816
15342
  return;
@@ -14821,11 +15347,15 @@ class AiLiveAssist {
14821
15347
  const pendingLaunch = parseFocusedHandoffPayload(value)?.pendingLaunch;
14822
15348
  if (!pendingLaunch)
14823
15349
  return true;
15350
+ const boundPendingLaunch = this.bootstrap
15351
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15352
+ : null;
14824
15353
  const staged = this.readStagedPendingLaunch();
14825
- if (!staged ||
15354
+ if (!boundPendingLaunch ||
15355
+ !staged ||
14826
15356
  staged.state !== "prepared" ||
14827
15357
  compareTransferAuthority(staged.authority, authority) !== 0 ||
14828
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15358
+ !this.pendingLaunchMatches(staged.pendingLaunch, boundPendingLaunch)) {
14829
15359
  return false;
14830
15360
  }
14831
15361
  return this.persistStagedPendingLaunch({
@@ -14869,6 +15399,12 @@ class AiLiveAssist {
14869
15399
  }
14870
15400
  markStagedPendingLaunchInFlight(pendingLaunch) {
14871
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;
14872
15408
  // AiLiveAssist always installs a TabActivityLock with durable authority.
14873
15409
  // Keep prototype-level test harnesses that intentionally omit the lock
14874
15410
  // implementation isolated from the persistence contract.
@@ -14880,40 +15416,54 @@ class AiLiveAssist {
14880
15416
  }
14881
15417
  if (!staged) {
14882
15418
  return this.persistStagedPendingLaunch({
14883
- v: 1,
15419
+ v: boundPendingLaunch ? 2 : 1,
14884
15420
  sourceOwnerId: randomId("launch_source"),
14885
15421
  authority: { ...ownedAuthority },
14886
15422
  state: "in_flight",
14887
15423
  claimAuthority: { ...ownedAuthority },
14888
15424
  expiresAt: Date.now() + STAGED_PENDING_LAUNCH_TTL_MS,
14889
15425
  pendingLaunch: {
14890
- ...pendingLaunch,
14891
- scopes: [...pendingLaunch.scopes],
15426
+ ...durablePendingLaunch,
15427
+ scopes: [...durablePendingLaunch.scopes],
14892
15428
  },
14893
15429
  });
14894
15430
  }
15431
+ const comparableStagedPendingLaunch = pendingLaunchIsBound(staged.pendingLaunch)
15432
+ ? staged.pendingLaunch
15433
+ : this.bootstrap
15434
+ ? bindPendingLaunchIntent(staged.pendingLaunch, this.bootstrap)
15435
+ : null;
14895
15436
  if (staged.pendingLaunch.idempotencyKey !== pendingLaunch.idempotencyKey ||
14896
15437
  !staged.claimAuthority ||
14897
15438
  compareTransferAuthority(staged.claimAuthority, ownedAuthority) !== 0 ||
14898
- (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))) {
14899
15444
  return false;
14900
15445
  }
14901
- if (staged.state === "in_flight")
15446
+ if (staged.state === "in_flight" &&
15447
+ staged.v === (boundPendingLaunch ? 2 : 1) &&
15448
+ this.pendingLaunchMatches(staged.pendingLaunch, durablePendingLaunch)) {
14902
15449
  return true;
15450
+ }
14903
15451
  return this.persistStagedPendingLaunch({
14904
15452
  ...staged,
15453
+ v: boundPendingLaunch ? 2 : 1,
14905
15454
  state: "in_flight",
14906
15455
  pendingLaunch: {
14907
- ...pendingLaunch,
14908
- scopes: [...pendingLaunch.scopes],
15456
+ ...durablePendingLaunch,
15457
+ scopes: [...durablePendingLaunch.scopes],
14909
15458
  },
14910
15459
  });
14911
15460
  }
14912
15461
  acceptStagedPendingLaunch(pendingLaunch, result) {
14913
15462
  const staged = this.readStagedPendingLaunch();
14914
- if (!staged ||
14915
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15463
+ if (!staged)
14916
15464
  return true;
15465
+ if (!this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15466
+ return false;
14917
15467
  }
14918
15468
  const ownedAuthority = this.lock.ownedAuthority?.() ?? null;
14919
15469
  const stored = this.readResume();
@@ -14937,17 +15487,11 @@ class AiLiveAssist {
14937
15487
  })) {
14938
15488
  return false;
14939
15489
  }
14940
- try {
14941
- localStorage.removeItem(this.stagedPendingLaunchStorageKey);
14942
- return localStorage.getItem(this.stagedPendingLaunchStorageKey) === null;
14943
- }
14944
- catch {
14945
- return false;
14946
- }
15490
+ return this.clearStagedPendingLaunch(staged.pendingLaunch, staged.authority);
14947
15491
  }
14948
15492
  hasStagedPendingLaunch(pendingLaunch) {
14949
- return (this.readStagedPendingLaunch()?.pendingLaunch.idempotencyKey ===
14950
- pendingLaunch.idempotencyKey);
15493
+ const staged = this.readStagedPendingLaunch();
15494
+ return Boolean(staged && this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch));
14951
15495
  }
14952
15496
  storeTransferAuthority(authority) {
14953
15497
  const current = this.readTransferAuthority();
@@ -15025,12 +15569,21 @@ class AiLiveAssist {
15025
15569
  if (!this.freshAiReturnProvisionalStorageKey)
15026
15570
  return null;
15027
15571
  try {
15028
- const serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15029
- const provisional = parseFreshAiReturnProvisional(serialized ? JSON.parse(serialized) : null);
15030
- 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) {
15031
15576
  sessionStorage.removeItem(this.freshAiReturnProvisionalStorageKey);
15032
15577
  }
15033
- 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;
15034
15587
  }
15035
15588
  catch {
15036
15589
  return null;
@@ -15040,9 +15593,54 @@ class AiLiveAssist {
15040
15593
  if (!this.freshAiReturnProvisionalStorageKey)
15041
15594
  return false;
15042
15595
  try {
15043
- 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
+ }
15044
15642
  return (canonicalJson(this.readFreshAiReturnProvisional()) ===
15045
- canonicalJson(provisional));
15643
+ canonicalJson(normalized));
15046
15644
  }
15047
15645
  catch {
15048
15646
  return false;
@@ -15052,19 +15650,21 @@ class AiLiveAssist {
15052
15650
  if (!this.freshAiReturnProvisionalStorageKey)
15053
15651
  return;
15054
15652
  try {
15055
- const serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15653
+ const storageKey = expected?.v === 1
15654
+ ? this.legacyFreshAiReturnProvisionalStorageKey
15655
+ : this.freshAiReturnProvisionalStorageKey;
15656
+ const serialized = sessionStorage.getItem(storageKey);
15056
15657
  if (!serialized)
15057
15658
  return;
15058
15659
  if (expected) {
15059
15660
  const stored = parseFreshAiReturnProvisional(JSON.parse(serialized));
15060
15661
  if (canonicalJson(stored) !== canonicalJson(expected))
15061
15662
  return;
15062
- if (sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey) !==
15063
- serialized) {
15663
+ if (sessionStorage.getItem(storageKey) !== serialized) {
15064
15664
  return;
15065
15665
  }
15066
15666
  }
15067
- sessionStorage.removeItem(this.freshAiReturnProvisionalStorageKey);
15667
+ sessionStorage.removeItem(storageKey);
15068
15668
  }
15069
15669
  catch {
15070
15670
  /* noop */
@@ -15655,10 +16255,7 @@ class AiLiveAssist {
15655
16255
  !this.view ||
15656
16256
  (record &&
15657
16257
  (!record.tabId ||
15658
- record.protocolVersion !==
15659
- (0, exports.resolveAiGuideProtocolVersion)(this.bootstrap) ||
15660
- record.experienceMode !==
15661
- (this.bootstrap.experienceMode ?? "standard"))) ||
16258
+ !resumeProtocolIsSupported(record, this.bootstrap))) ||
15662
16259
  (pendingLaunch &&
15663
16260
  (this.visitorActiveTakeoverOnly ||
15664
16261
  ((this.bootstrap.experienceMode ?? "standard") === "voice_presence" &&
@@ -15694,10 +16291,11 @@ class AiLiveAssist {
15694
16291
  this.lock.release();
15695
16292
  };
15696
16293
  const preparedMatches = record
15697
- ? prepared?.resume?.sessionId === record.sessionId
16294
+ ? Boolean(prepared?.resume &&
16295
+ this.sameResumeAuthorityIdentity(prepared.resume, record))
15698
16296
  : Boolean(pendingLaunch &&
15699
- prepared?.pendingLaunch?.idempotencyKey ===
15700
- pendingLaunch.idempotencyKey);
16297
+ prepared?.pendingLaunch &&
16298
+ this.pendingLaunchMatches(prepared.pendingLaunch, pendingLaunch));
15701
16299
  if (!payload ||
15702
16300
  (!record && !pendingLaunch) ||
15703
16301
  !this.bootstrap ||
@@ -15710,10 +16308,7 @@ class AiLiveAssist {
15710
16308
  !preparedMatches ||
15711
16309
  (record &&
15712
16310
  (!record.tabId ||
15713
- record.protocolVersion !==
15714
- (0, exports.resolveAiGuideProtocolVersion)(this.bootstrap) ||
15715
- record.experienceMode !==
15716
- (this.bootstrap.experienceMode ?? "standard"))) ||
16311
+ !resumeProtocolIsSupported(record, this.bootstrap))) ||
15717
16312
  (pendingLaunch && this.visitorActiveTakeoverOnly)) {
15718
16313
  abandonUnusableCommit();
15719
16314
  return;
@@ -15725,7 +16320,7 @@ class AiLiveAssist {
15725
16320
  return;
15726
16321
  }
15727
16322
  if (this.lastLaunch &&
15728
- this.lastLaunch.idempotencyKey !== stagedPending.idempotencyKey) {
16323
+ !this.pendingLaunchMatches(this.lastLaunch, stagedPending)) {
15729
16324
  this.clearStagedPendingLaunch(stagedPending, authority);
15730
16325
  }
15731
16326
  else {
@@ -15737,6 +16332,12 @@ class AiLiveAssist {
15737
16332
  return;
15738
16333
  }
15739
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
+ }
15740
16341
  const effectivePayload = record &&
15741
16342
  storedResume?.sessionId === record.sessionId &&
15742
16343
  storedResume.resumeCredential !== record.resumeCredential
@@ -15777,11 +16378,11 @@ class AiLiveAssist {
15777
16378
  completeFocusedTabRelinquish(value) {
15778
16379
  const payload = parseFocusedHandoffPayload(value);
15779
16380
  const sameResume = Boolean(payload?.resume &&
15780
- this.focusedHandoffInFlight?.resume?.sessionId ===
15781
- payload.resume.sessionId);
16381
+ this.focusedHandoffInFlight?.resume &&
16382
+ this.sameResumeAuthorityIdentity(this.focusedHandoffInFlight.resume, payload.resume));
15782
16383
  const samePendingLaunch = Boolean(payload?.pendingLaunch &&
15783
- this.focusedHandoffInFlight?.pendingLaunch?.idempotencyKey ===
15784
- payload.pendingLaunch.idempotencyKey);
16384
+ this.focusedHandoffInFlight?.pendingLaunch &&
16385
+ this.pendingLaunchMatches(this.focusedHandoffInFlight.pendingLaunch, payload.pendingLaunch));
15785
16386
  if (!payload || (!sameResume && !samePendingLaunch)) {
15786
16387
  return;
15787
16388
  }
@@ -15812,8 +16413,8 @@ class AiLiveAssist {
15812
16413
  this.clearStagedPendingLaunch(pendingLaunch, authority);
15813
16414
  }
15814
16415
  if (pendingLaunch &&
15815
- this.focusedHandoffInFlight?.pendingLaunch?.idempotencyKey ===
15816
- pendingLaunch.idempotencyKey) {
16416
+ this.focusedHandoffInFlight?.pendingLaunch &&
16417
+ this.pendingLaunchMatches(this.focusedHandoffInFlight.pendingLaunch, pendingLaunch)) {
15817
16418
  this.focusedHandoffInFlight = null;
15818
16419
  if (this.terminalRequestPending ||
15819
16420
  this.ended ||
@@ -15835,7 +16436,7 @@ class AiLiveAssist {
15835
16436
  queueMicrotask(() => {
15836
16437
  const retry = this.lastLaunch;
15837
16438
  if (!retry ||
15838
- retry.idempotencyKey !== pendingLaunch.idempotencyKey ||
16439
+ !this.pendingLaunchMatches(retry, pendingLaunch) ||
15839
16440
  this.terminalRequestPending ||
15840
16441
  this.ended ||
15841
16442
  this.destroyed ||
@@ -15844,7 +16445,9 @@ class AiLiveAssist {
15844
16445
  return;
15845
16446
  }
15846
16447
  this.lastLaunch = null;
15847
- void this.launch(retry.mode, retry.scopes, retry.idempotencyKey);
16448
+ void this.launch(retry.mode, retry.scopes, retry.idempotencyKey, {
16449
+ pendingLaunch: retry,
16450
+ });
15848
16451
  });
15849
16452
  return;
15850
16453
  }
@@ -15910,7 +16513,7 @@ class AiLiveAssist {
15910
16513
  this.view &&
15911
16514
  !this.visitorActiveTakeoverOnly) {
15912
16515
  this.lastLaunch = null;
15913
- await this.launch(deferredLaunch.mode, deferredLaunch.scopes, deferredLaunch.idempotencyKey);
16516
+ await this.launch(deferredLaunch.mode, deferredLaunch.scopes, deferredLaunch.idempotencyKey, { pendingLaunch: deferredLaunch });
15914
16517
  return;
15915
16518
  }
15916
16519
  this.lock.release();