@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.
@@ -551,9 +551,71 @@ const versionAtLeast = (current, minimum) => {
551
551
  };
552
552
  export const resolveAiGuideProtocolVersion = (bootstrap) => bootstrap.experienceMode === "voice_presence" ||
553
553
  (bootstrap.minimumProtocolVersion ?? 1) >= 2 ||
554
- bootstrap.protocol.min >= 2
554
+ bootstrap.protocol.min >= 2 ||
555
+ bootstrap.protocol.max >= 2
555
556
  ? 2
556
557
  : 1;
558
+ const resumeProtocolIsSupported = (record, bootstrap) => {
559
+ const experienceMode = bootstrap.experienceMode ?? "standard";
560
+ if (record.experienceMode !== experienceMode)
561
+ return false;
562
+ if (record.protocolVersion === resolveAiGuideProtocolVersion(bootstrap)) {
563
+ return true;
564
+ }
565
+ // Prefer V2 for fresh standard launches without abandoning a V1 session
566
+ // while the published configuration and runtime still admit V1.
567
+ return (record.experienceMode === "standard" &&
568
+ record.protocolVersion === 1 &&
569
+ (bootstrap.minimumProtocolVersion ?? 1) <= 1 &&
570
+ bootstrap.protocol.min <= 1 &&
571
+ bootstrap.protocol.max >= 1);
572
+ };
573
+ const protocolVersionIsAdmitted = (protocolVersion, bootstrap) => protocolVersion >= (bootstrap.minimumProtocolVersion ?? 1) &&
574
+ protocolVersion >= bootstrap.protocol.min &&
575
+ protocolVersion <= bootstrap.protocol.max;
576
+ const pendingLaunchIsBound = (pendingLaunch) => (pendingLaunch.protocolVersion === 1 ||
577
+ pendingLaunch.protocolVersion === 2) &&
578
+ (pendingLaunch.experienceMode === "standard" ||
579
+ pendingLaunch.experienceMode === "voice_presence");
580
+ const bindPendingLaunchIntent = (pendingLaunch, bootstrap) => {
581
+ const effectiveExperienceMode = bootstrap.experienceMode ?? "standard";
582
+ let protocolVersion = pendingLaunch.protocolVersion;
583
+ let experienceMode = pendingLaunch.experienceMode;
584
+ const hasProtocolVersion = protocolVersion === 1 || protocolVersion === 2;
585
+ const hasExperienceMode = experienceMode === "standard" || experienceMode === "voice_presence";
586
+ if (hasProtocolVersion !== hasExperienceMode)
587
+ return null;
588
+ if (!hasProtocolVersion && !hasExperienceMode) {
589
+ // A legacy record does not preserve its original protocol body. Current
590
+ // configuration can never prove the historical request, even when the
591
+ // present experience is voice presence, so its idempotency key must not be
592
+ // replayed with inferred authority.
593
+ return null;
594
+ }
595
+ if ((protocolVersion !== 1 && protocolVersion !== 2) ||
596
+ (experienceMode !== "standard" && experienceMode !== "voice_presence") ||
597
+ experienceMode !== effectiveExperienceMode ||
598
+ !protocolVersionIsAdmitted(protocolVersion, bootstrap) ||
599
+ (protocolVersion === 1 && experienceMode !== "standard") ||
600
+ (experienceMode === "voice_presence" &&
601
+ (protocolVersion !== 2 || pendingLaunch.mode !== "voice"))) {
602
+ return null;
603
+ }
604
+ return {
605
+ mode: pendingLaunch.mode,
606
+ scopes: [...pendingLaunch.scopes],
607
+ idempotencyKey: pendingLaunch.idempotencyKey,
608
+ protocolVersion,
609
+ experienceMode,
610
+ };
611
+ };
612
+ const freshPendingLaunchIntent = (mode, scopes, idempotencyKey, bootstrap) => ({
613
+ mode,
614
+ scopes: [...scopes],
615
+ idempotencyKey,
616
+ protocolVersion: resolveAiGuideProtocolVersion(bootstrap),
617
+ experienceMode: bootstrap.experienceMode ?? "standard",
618
+ });
557
619
  const parseTerminalEndIntent = (value) => {
558
620
  if (!value || typeof value !== "object" || Array.isArray(value))
559
621
  return null;
@@ -588,9 +650,13 @@ const parseFreshAiReturnProvisional = (value) => {
588
650
  if (!value || typeof value !== "object" || Array.isArray(value))
589
651
  return null;
590
652
  const raw = value;
591
- if (Object.keys(raw).sort().join(",") !==
592
- "conversationDisplayId,expiresAt,launchIdempotencyKey,linkId,mode,phase,sessionId,v" ||
593
- raw.v !== 1 ||
653
+ const recordVersion = raw.v === 1 || raw.v === 2 ? raw.v : null;
654
+ const scopes = Array.isArray(raw.scopes) ? raw.scopes : [];
655
+ const expectedKeys = recordVersion === 1
656
+ ? "conversationDisplayId,expiresAt,launchIdempotencyKey,linkId,mode,phase,sessionId,v"
657
+ : "conversationDisplayId,experienceMode,expiresAt,launchIdempotencyKey,linkId,mode,phase,protocolVersion,scopes,sessionId,v";
658
+ if (recordVersion === null ||
659
+ Object.keys(raw).sort().join(",") !== expectedKeys ||
594
660
  !PUBLIC_REQUEST_ID_PATTERN.test(raw.linkId ?? "") ||
595
661
  !/^[1-9]\d{0,18}$/.test(raw.conversationDisplayId ?? "") ||
596
662
  (raw.mode !== "text" && raw.mode !== "voice") ||
@@ -605,19 +671,42 @@ const parseFreshAiReturnProvisional = (value) => {
605
671
  (raw.launchIdempotencyKey === null || raw.sessionId !== null)) ||
606
672
  (raw.phase === "launched" &&
607
673
  (raw.launchIdempotencyKey === null || raw.sessionId === null)) ||
674
+ (recordVersion === 2 &&
675
+ raw.phase === "released" &&
676
+ (raw.protocolVersion !== null ||
677
+ raw.experienceMode !== null ||
678
+ raw.scopes !== null)) ||
679
+ (recordVersion === 2 &&
680
+ raw.phase !== "released" &&
681
+ ((raw.protocolVersion !== 1 && raw.protocolVersion !== 2) ||
682
+ (raw.experienceMode !== "standard" &&
683
+ raw.experienceMode !== "voice_presence") ||
684
+ (raw.protocolVersion === 1 && raw.experienceMode !== "standard") ||
685
+ (raw.experienceMode === "voice_presence" &&
686
+ (raw.protocolVersion !== 2 || raw.mode !== "voice")) ||
687
+ !Array.isArray(raw.scopes) ||
688
+ scopes.length > CONSENT_SCOPES.size ||
689
+ scopes.some((scope) => typeof scope !== "string" ||
690
+ !CONSENT_SCOPES.has(scope)) ||
691
+ new Set(scopes).size !== scopes.length)) ||
608
692
  !Number.isSafeInteger(raw.expiresAt) ||
609
693
  (raw.expiresAt ?? 0) <= Date.now() ||
610
694
  (raw.expiresAt ?? 0) > Date.now() + STORAGE_TTL_MS) {
611
695
  return null;
612
696
  }
613
697
  return {
614
- v: 1,
698
+ v: recordVersion,
615
699
  linkId: raw.linkId,
616
700
  conversationDisplayId: raw.conversationDisplayId,
617
701
  mode: raw.mode,
618
702
  phase: raw.phase,
619
703
  launchIdempotencyKey: raw.launchIdempotencyKey ?? null,
620
704
  sessionId: raw.sessionId ?? null,
705
+ protocolVersion: recordVersion === 2 ? (raw.protocolVersion ?? null) : null,
706
+ experienceMode: recordVersion === 2 ? (raw.experienceMode ?? null) : null,
707
+ scopes: recordVersion === 2 && raw.phase !== "released"
708
+ ? [...scopes]
709
+ : null,
621
710
  expiresAt: raw.expiresAt,
622
711
  };
623
712
  };
@@ -654,6 +743,16 @@ const parseResumeRecord = (value) => {
654
743
  const record = JSON.parse(value);
655
744
  const scopes = Array.isArray(record.scopes) ? record.scopes : [];
656
745
  const locale = typeof record.locale === "string" ? canonicalLocale(record.locale) : null;
746
+ const protocolFieldsAbsent = record.protocolVersion === undefined &&
747
+ record.experienceMode === undefined;
748
+ const protocolBindingValid = protocolFieldsAbsent ||
749
+ ((record.protocolVersion === 1 || record.protocolVersion === 2) &&
750
+ (record.experienceMode === "standard" ||
751
+ record.experienceMode === "voice_presence") &&
752
+ (record.protocolVersion !== 1 ||
753
+ record.experienceMode === "standard") &&
754
+ (record.experienceMode !== "voice_presence" ||
755
+ (record.protocolVersion === 2 && record.mode === "voice")));
657
756
  if (!PUBLIC_REQUEST_ID_PATTERN.test(record.sessionId ?? "") ||
658
757
  !isValidAssistSessionGrant(record.resumeCredential) ||
659
758
  !["text", "voice"].includes(record.mode ?? "") ||
@@ -662,6 +761,7 @@ const parseResumeRecord = (value) => {
662
761
  scopes.some((scope) => typeof scope !== "string" ||
663
762
  !CONSENT_SCOPES.has(scope)) ||
664
763
  new Set(scopes).size !== scopes.length ||
764
+ !protocolBindingValid ||
665
765
  !locale ||
666
766
  typeof record.expiresAt !== "number" ||
667
767
  !Number.isFinite(record.expiresAt) ||
@@ -895,10 +995,10 @@ const parseResumeRecord = (value) => {
895
995
  ? record.visualConsentGranted
896
996
  : record.scopes.includes("visual_context"),
897
997
  manualTurn: record.manualTurn === true,
898
- protocolVersion: record.protocolVersion === 2 ? 2 : 1,
899
- experienceMode: record.experienceMode === "voice_presence"
900
- ? "voice_presence"
901
- : "standard",
998
+ protocolVersion: protocolFieldsAbsent ? 1 : record.protocolVersion,
999
+ experienceMode: protocolFieldsAbsent
1000
+ ? "standard"
1001
+ : record.experienceMode,
902
1002
  fallbackIntent: record.fallbackIntent === "standard_assist" ||
903
1003
  record.fallbackIntent === "human_handoff"
904
1004
  ? record.fallbackIntent
@@ -977,11 +1077,62 @@ const parseTranscript = (value) => {
977
1077
  }
978
1078
  return entries;
979
1079
  };
1080
+ const parsePendingLaunchIntent = (value, recordVersion) => {
1081
+ if (!value || typeof value !== "object" || Array.isArray(value))
1082
+ return null;
1083
+ const candidate = value;
1084
+ const scopes = Array.isArray(candidate.scopes) ? candidate.scopes : [];
1085
+ const expectedKeys = recordVersion === 1
1086
+ ? ["idempotencyKey", "mode", "scopes"]
1087
+ : [
1088
+ "experienceMode",
1089
+ "idempotencyKey",
1090
+ "mode",
1091
+ "protocolVersion",
1092
+ "scopes",
1093
+ ];
1094
+ if (canonicalJson(Object.keys(candidate).sort()) !==
1095
+ canonicalJson(expectedKeys) ||
1096
+ (candidate.mode !== "text" && candidate.mode !== "voice") ||
1097
+ !Array.isArray(candidate.scopes) ||
1098
+ typeof candidate.idempotencyKey !== "string" ||
1099
+ !PUBLIC_REQUEST_ID_PATTERN.test(candidate.idempotencyKey) ||
1100
+ scopes.length > CONSENT_SCOPES.size ||
1101
+ scopes.some((scope) => typeof scope !== "string" || !CONSENT_SCOPES.has(scope)) ||
1102
+ new Set(scopes).size !== scopes.length) {
1103
+ return null;
1104
+ }
1105
+ const base = {
1106
+ mode: candidate.mode,
1107
+ scopes: [...scopes],
1108
+ idempotencyKey: candidate.idempotencyKey,
1109
+ };
1110
+ if (recordVersion === 1) {
1111
+ return { ...base, protocolVersion: null, experienceMode: null };
1112
+ }
1113
+ const protocolVersion = candidate.protocolVersion;
1114
+ const experienceMode = candidate.experienceMode;
1115
+ if ((protocolVersion !== 1 && protocolVersion !== 2) ||
1116
+ (experienceMode !== "standard" && experienceMode !== "voice_presence") ||
1117
+ (protocolVersion === 1 && experienceMode !== "standard") ||
1118
+ (experienceMode === "voice_presence" &&
1119
+ (protocolVersion !== 2 || candidate.mode !== "voice"))) {
1120
+ return null;
1121
+ }
1122
+ return { ...base, protocolVersion, experienceMode };
1123
+ };
980
1124
  const parseFocusedHandoffPayload = (value) => {
981
1125
  if (!value || typeof value !== "object" || Array.isArray(value))
982
1126
  return null;
983
1127
  const raw = value;
984
- if (raw.v === 1) {
1128
+ if (raw.v === 1 || raw.v === 2) {
1129
+ const expectedKeys = ["resume", "transcript", "v"];
1130
+ if (raw.pendingLaunch !== undefined)
1131
+ expectedKeys.push("pendingLaunch");
1132
+ if (canonicalJson(Object.keys(raw).sort()) !==
1133
+ canonicalJson(expectedKeys.sort())) {
1134
+ return null;
1135
+ }
985
1136
  const resume = raw.resume === null
986
1137
  ? null
987
1138
  : parseResumeRecord(JSON.stringify(raw.resume ?? null));
@@ -989,30 +1140,17 @@ const parseFocusedHandoffPayload = (value) => {
989
1140
  return null;
990
1141
  let pendingLaunch;
991
1142
  if (raw.pendingLaunch !== undefined) {
992
- if (!raw.pendingLaunch ||
993
- typeof raw.pendingLaunch !== "object" ||
994
- Array.isArray(raw.pendingLaunch)) {
1143
+ pendingLaunch =
1144
+ parsePendingLaunchIntent(raw.pendingLaunch, raw.v) ?? undefined;
1145
+ if (resume || !pendingLaunch)
995
1146
  return null;
996
- }
997
- const candidate = raw.pendingLaunch;
998
- const scopes = Array.isArray(candidate.scopes) ? candidate.scopes : [];
999
- if (resume ||
1000
- (candidate.mode !== "text" && candidate.mode !== "voice") ||
1001
- !PUBLIC_REQUEST_ID_PATTERN.test(String(candidate.idempotencyKey ?? "")) ||
1002
- scopes.length > CONSENT_SCOPES.size ||
1003
- scopes.some((scope) => typeof scope !== "string" ||
1004
- !CONSENT_SCOPES.has(scope)) ||
1005
- new Set(scopes).size !== scopes.length) {
1006
- return null;
1007
- }
1008
- pendingLaunch = {
1009
- mode: candidate.mode,
1010
- scopes: [...scopes],
1011
- idempotencyKey: candidate.idempotencyKey,
1012
- };
1147
+ }
1148
+ if (raw.v === 2 &&
1149
+ (!pendingLaunch || !pendingLaunchIsBound(pendingLaunch))) {
1150
+ return null;
1013
1151
  }
1014
1152
  return {
1015
- v: 1,
1153
+ v: raw.v,
1016
1154
  resume,
1017
1155
  transcript: parseTranscript(raw.transcript),
1018
1156
  ...(pendingLaunch ? { pendingLaunch } : {}),
@@ -1060,19 +1198,15 @@ const parseStagedPendingLaunch = (value) => {
1060
1198
  const claimAuthority = raw.claimAuthority === null
1061
1199
  ? null
1062
1200
  : parseTransferAuthority(JSON.stringify(raw.claimAuthority ?? null));
1201
+ const recordVersion = raw.v === 1 || raw.v === 2 ? raw.v : null;
1063
1202
  const payload = parseFocusedHandoffPayload({
1064
- v: 1,
1203
+ v: recordVersion,
1065
1204
  resume: null,
1066
1205
  transcript: [],
1067
1206
  pendingLaunch: raw.pendingLaunch,
1068
1207
  });
1069
1208
  const expiresAt = raw.expiresAt;
1070
1209
  const topLevelKeys = Object.keys(raw).sort();
1071
- const pendingKeys = raw.pendingLaunch &&
1072
- typeof raw.pendingLaunch === "object" &&
1073
- !Array.isArray(raw.pendingLaunch)
1074
- ? Object.keys(raw.pendingLaunch).sort()
1075
- : [];
1076
1210
  const authorityKeys = raw.authority &&
1077
1211
  typeof raw.authority === "object" &&
1078
1212
  !Array.isArray(raw.authority)
@@ -1093,14 +1227,12 @@ const parseStagedPendingLaunch = (value) => {
1093
1227
  "state",
1094
1228
  "v",
1095
1229
  ]) ||
1096
- canonicalJson(pendingKeys) !==
1097
- canonicalJson(["idempotencyKey", "mode", "scopes"]) ||
1098
1230
  canonicalJson(authorityKeys) !==
1099
1231
  canonicalJson(["ownerId", "revision", "term"]) ||
1100
1232
  (raw.claimAuthority !== null &&
1101
1233
  canonicalJson(claimAuthorityKeys) !==
1102
1234
  canonicalJson(["ownerId", "revision", "term"])) ||
1103
- raw.v !== 1 ||
1235
+ recordVersion === null ||
1104
1236
  typeof raw.sourceOwnerId !== "string" ||
1105
1237
  !PUBLIC_REQUEST_ID_PATTERN.test(raw.sourceOwnerId) ||
1106
1238
  !authority ||
@@ -1124,7 +1256,7 @@ const parseStagedPendingLaunch = (value) => {
1124
1256
  return null;
1125
1257
  }
1126
1258
  return {
1127
- v: 1,
1259
+ v: recordVersion,
1128
1260
  sourceOwnerId: raw.sourceOwnerId,
1129
1261
  authority,
1130
1262
  state: raw.state,
@@ -1517,10 +1649,12 @@ export default class AiLiveAssist {
1517
1649
  this.storageKey = `__oxvo_ai_live_assist_resume_${this.siteKey}`;
1518
1650
  this.transcriptStorageKey = `__oxvo_ai_live_assist_transcript_${this.siteKey}`;
1519
1651
  this.transferAuthorityStorageKey = `__oxvo_ai_live_assist_transfer_authority_${this.siteKey}`;
1520
- this.stagedPendingLaunchStorageKey = `__oxvo_ai_live_assist_pending_launch_${this.siteKey}`;
1652
+ this.legacyStagedPendingLaunchStorageKey = `__oxvo_ai_live_assist_pending_launch_${this.siteKey}`;
1653
+ this.stagedPendingLaunchStorageKey = `__oxvo_ai_live_assist_pending_launch_v2_${this.siteKey}`;
1521
1654
  this.terminalEndStorageKey = `__oxvo_ai_live_assist_terminal_end_${this.siteKey}`;
1522
1655
  this.messengerReturnEndStorageKey = `__oxvo_ai_live_assist_handoff_terminal_${this.siteKey}`;
1523
- this.freshAiReturnProvisionalStorageKey = `__oxvo_ai_live_assist_fresh_return_${this.siteKey}`;
1656
+ this.legacyFreshAiReturnProvisionalStorageKey = `__oxvo_ai_live_assist_fresh_return_${this.siteKey}`;
1657
+ this.freshAiReturnProvisionalStorageKey = `__oxvo_ai_live_assist_fresh_return_v2_${this.siteKey}`;
1524
1658
  this.pageAssistanceTrackerIdentityStorageKey = `__oxvo_ai_live_assist_page_assistance_tracker_${this.siteKey}`;
1525
1659
  this.transcript = this.readTranscript();
1526
1660
  this.visitorIdentity = this.resolveVisitorId();
@@ -1719,8 +1853,7 @@ export default class AiLiveAssist {
1719
1853
  pendingLaunch &&
1720
1854
  (!staged ||
1721
1855
  (staged.state === "in_flight" &&
1722
- staged.pendingLaunch.idempotencyKey ===
1723
- pendingLaunch.idempotencyKey))
1856
+ this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)))
1724
1857
  ? { ...pendingLaunch, scopes: [...pendingLaunch.scopes] }
1725
1858
  : null;
1726
1859
  const record = this.readResume();
@@ -1735,8 +1868,12 @@ export default class AiLiveAssist {
1735
1868
  this.persistTerminalEndIntent(intent);
1736
1869
  if (record)
1737
1870
  this.storeTerminalResume(record, intent);
1738
- else if (this.result)
1739
- this.captureTerminalAuthorizedResult(this.result);
1871
+ else if (this.result) {
1872
+ const retainedResult = this.result;
1873
+ if (!this.captureTerminalAuthorizedResult(retainedResult)) {
1874
+ this.rejectTerminalAuthorizedResult(retainedResult);
1875
+ }
1876
+ }
1740
1877
  }
1741
1878
  this.terminalRequestPending = true;
1742
1879
  if (!this.relinquishModeUpgradeForRecovery(false)) {
@@ -1900,8 +2037,7 @@ export default class AiLiveAssist {
1900
2037
  if (resume &&
1901
2038
  !terminalIntent &&
1902
2039
  !resumedFallback &&
1903
- (resume.experienceMode !== experienceMode ||
1904
- resume.protocolVersion !== requestedProtocol ||
2040
+ (!resumeProtocolIsSupported(resume, bootstrap) ||
1905
2041
  resume.fallbackIntent !== null)) {
1906
2042
  this.clearResume();
1907
2043
  resume = null;
@@ -2625,9 +2761,6 @@ export default class AiLiveAssist {
2625
2761
  !this.messengerSupportsConfirmedHandoff(messenger)) {
2626
2762
  return false;
2627
2763
  }
2628
- bridgeActivationAttempted = true;
2629
- if (messenger.setAiGuideHandoffEnabled(true) !== true)
2630
- return false;
2631
2764
  if (messenger.ready) {
2632
2765
  await this.withMessengerTimeout(messenger.ready(), MESSENGER_LOAD_TIMEOUT_MS);
2633
2766
  }
@@ -2637,6 +2770,9 @@ export default class AiLiveAssist {
2637
2770
  this.resolveMessenger() !== messenger) {
2638
2771
  return false;
2639
2772
  }
2773
+ bridgeActivationAttempted = true;
2774
+ if (messenger.setAiGuideHandoffEnabled(true) !== true)
2775
+ return false;
2640
2776
  if (this.messengerInitialLauncherVisible === null) {
2641
2777
  this.messengerInitialLauncherVisible =
2642
2778
  messenger.launcherVisible !== false;
@@ -3865,13 +4001,16 @@ export default class AiLiveAssist {
3865
4001
  if (expiresAt <= Date.now())
3866
4002
  return null;
3867
4003
  return {
3868
- v: 1,
4004
+ v: 2,
3869
4005
  linkId: companion.linkId,
3870
4006
  conversationDisplayId: conversationId,
3871
4007
  mode,
3872
4008
  phase: "released",
3873
4009
  launchIdempotencyKey: null,
3874
4010
  sessionId: null,
4011
+ protocolVersion: null,
4012
+ experienceMode: null,
4013
+ scopes: null,
3875
4014
  expiresAt,
3876
4015
  };
3877
4016
  }
@@ -3888,8 +4027,12 @@ export default class AiLiveAssist {
3888
4027
  }
3889
4028
  this.persistFreshAiReturnProvisional({
3890
4029
  ...provisional,
4030
+ v: 2,
3891
4031
  phase: "released",
3892
4032
  launchIdempotencyKey: null,
4033
+ protocolVersion: null,
4034
+ experienceMode: null,
4035
+ scopes: null,
3893
4036
  });
3894
4037
  }
3895
4038
  quiesceProvisionalFreshAiReturn() {
@@ -3903,22 +4046,59 @@ export default class AiLiveAssist {
3903
4046
  this.view?.setPaused(true);
3904
4047
  this.cleanupTransport();
3905
4048
  }
4049
+ boundFreshAiReturnProvisional(provisional) {
4050
+ if (provisional.phase === "released" ||
4051
+ provisional.launchIdempotencyKey === null ||
4052
+ provisional.protocolVersion === null ||
4053
+ provisional.experienceMode === null ||
4054
+ provisional.scopes === null ||
4055
+ !this.bootstrap) {
4056
+ return null;
4057
+ }
4058
+ const pendingLaunch = {
4059
+ mode: provisional.mode,
4060
+ scopes: [...provisional.scopes],
4061
+ idempotencyKey: provisional.launchIdempotencyKey,
4062
+ protocolVersion: provisional.protocolVersion,
4063
+ experienceMode: provisional.experienceMode,
4064
+ };
4065
+ const bound = bindPendingLaunchIntent(pendingLaunch, this.bootstrap);
4066
+ return bound && this.pendingLaunchMatches(bound, pendingLaunch)
4067
+ ? bound
4068
+ : null;
4069
+ }
4070
+ resumeMatchesFreshAiReturn(resume, pendingLaunch) {
4071
+ return (resume.protocolVersion === pendingLaunch.protocolVersion &&
4072
+ resume.experienceMode === pendingLaunch.experienceMode &&
4073
+ resume.mode === pendingLaunch.mode &&
4074
+ canonicalJson(resume.scopes) === canonicalJson(pendingLaunch.scopes));
4075
+ }
4076
+ resultMatchesFreshAiReturn(result, pendingLaunch) {
4077
+ return (result.schemaVersion === pendingLaunch.protocolVersion &&
4078
+ result.protocolVersion === pendingLaunch.protocolVersion &&
4079
+ result.experienceMode === pendingLaunch.experienceMode &&
4080
+ (result.mode === undefined || result.mode === pendingLaunch.mode));
4081
+ }
3906
4082
  async retireProvisionalFreshAiReturn(provisional) {
3907
4083
  if (provisional.phase === "released")
3908
4084
  return true;
4085
+ const pendingLaunch = this.boundFreshAiReturnProvisional(provisional);
4086
+ if (!pendingLaunch)
4087
+ return false;
3909
4088
  const resume = this.readResume();
3910
4089
  const result = this.result;
3911
4090
  const sessionId = provisional.sessionId ?? resume?.sessionId ?? result?.sessionId ?? null;
3912
4091
  if (!sessionId)
3913
4092
  return false;
3914
- const authority = resume?.sessionId === sessionId
4093
+ const authority = resume?.sessionId === sessionId &&
4094
+ this.resumeMatchesFreshAiReturn(resume, pendingLaunch)
3915
4095
  ? {
3916
4096
  resumeCredential: resume.resumeCredential,
3917
4097
  protocolVersion: resume.protocolVersion,
3918
4098
  }
3919
4099
  : result?.sessionId === sessionId &&
3920
4100
  typeof result.resumeCredential === "string" &&
3921
- (result.protocolVersion === 1 || result.protocolVersion === 2)
4101
+ this.resultMatchesFreshAiReturn(result, pendingLaunch)
3922
4102
  ? {
3923
4103
  resumeCredential: result.resumeCredential,
3924
4104
  protocolVersion: result.protocolVersion,
@@ -3976,9 +4156,13 @@ export default class AiLiveAssist {
3976
4156
  provisional.conversationDisplayId;
3977
4157
  const released = {
3978
4158
  ...provisional,
4159
+ v: 2,
3979
4160
  phase: "released",
3980
4161
  launchIdempotencyKey: null,
3981
4162
  sessionId: null,
4163
+ protocolVersion: null,
4164
+ experienceMode: null,
4165
+ scopes: null,
3982
4166
  };
3983
4167
  return this.persistFreshAiReturnProvisional(released);
3984
4168
  }
@@ -3999,6 +4183,46 @@ export default class AiLiveAssist {
3999
4183
  // interrupted must never be replaced by a second fresh operation.
4000
4184
  return provisional.phase === "launching" ? "launch" : "blocked";
4001
4185
  }
4186
+ if (provisional.protocolVersion === null ||
4187
+ provisional.experienceMode === null ||
4188
+ provisional.scopes === null) {
4189
+ if (provisional.launchIdempotencyKey === null ||
4190
+ !resume ||
4191
+ resume.sessionId !== sessionId) {
4192
+ return "blocked";
4193
+ }
4194
+ const upgraded = {
4195
+ ...provisional,
4196
+ v: 2,
4197
+ sessionId,
4198
+ protocolVersion: resume.protocolVersion,
4199
+ experienceMode: resume.experienceMode,
4200
+ scopes: [...resume.scopes],
4201
+ };
4202
+ if (!this.boundFreshAiReturnProvisional(upgraded) ||
4203
+ !this.persistFreshAiReturnProvisional(upgraded)) {
4204
+ return "blocked";
4205
+ }
4206
+ provisional = upgraded;
4207
+ }
4208
+ const pendingLaunch = this.boundFreshAiReturnProvisional(provisional);
4209
+ if (!pendingLaunch)
4210
+ return "blocked";
4211
+ let hasSessionAuthority = false;
4212
+ if (resume?.sessionId === sessionId) {
4213
+ if (!this.resumeMatchesFreshAiReturn(resume, pendingLaunch)) {
4214
+ return "blocked";
4215
+ }
4216
+ hasSessionAuthority = true;
4217
+ }
4218
+ if (this.result?.sessionId === sessionId) {
4219
+ if (!this.resultMatchesFreshAiReturn(this.result, pendingLaunch)) {
4220
+ return "blocked";
4221
+ }
4222
+ hasSessionAuthority = true;
4223
+ }
4224
+ if (!hasSessionAuthority)
4225
+ return "blocked";
4002
4226
  if (provisional.phase === "launching") {
4003
4227
  provisional = {
4004
4228
  ...provisional,
@@ -4200,17 +4424,61 @@ export default class AiLiveAssist {
4200
4424
  const currentProvisional = this.freshAiReturnProvisionalFor(conversationId);
4201
4425
  if (!currentProvisional)
4202
4426
  return false;
4427
+ if (currentProvisional.phase === "launching" &&
4428
+ currentProvisional.mode !== mode) {
4429
+ return false;
4430
+ }
4431
+ const persistedLaunchScopes = currentProvisional.phase === "launching"
4432
+ ? currentProvisional.scopes
4433
+ : null;
4434
+ if (currentProvisional.phase === "launching" &&
4435
+ (currentProvisional.protocolVersion === null ||
4436
+ currentProvisional.experienceMode === null ||
4437
+ persistedLaunchScopes === null)) {
4438
+ // A legacy Fresh-return record does not contain the complete request
4439
+ // body. Reusing its idempotency key with current bootstrap scopes or
4440
+ // protocol negotiation would create a different operation.
4441
+ return false;
4442
+ }
4203
4443
  const launchIdempotencyKey = currentProvisional.phase === "launching"
4204
4444
  ? currentProvisional.launchIdempotencyKey
4205
4445
  : idempotencyKey("launch");
4206
4446
  if (!launchIdempotencyKey)
4207
4447
  return false;
4448
+ const launchScopes = persistedLaunchScopes
4449
+ ? [...persistedLaunchScopes]
4450
+ : [...scopes];
4451
+ const provisionalPendingLaunch = currentProvisional.protocolVersion !== null &&
4452
+ currentProvisional.experienceMode !== null
4453
+ ? {
4454
+ mode: currentProvisional.mode,
4455
+ scopes: launchScopes,
4456
+ idempotencyKey: launchIdempotencyKey,
4457
+ protocolVersion: currentProvisional.protocolVersion,
4458
+ experienceMode: currentProvisional.experienceMode,
4459
+ }
4460
+ : {
4461
+ mode: currentProvisional.mode,
4462
+ scopes: launchScopes,
4463
+ idempotencyKey: launchIdempotencyKey,
4464
+ protocolVersion: null,
4465
+ experienceMode: null,
4466
+ };
4467
+ const pendingLaunch = currentProvisional.phase === "launching"
4468
+ ? bindPendingLaunchIntent(provisionalPendingLaunch, bootstrap)
4469
+ : freshPendingLaunchIntent(mode, scopes, launchIdempotencyKey, bootstrap);
4470
+ if (!pendingLaunch)
4471
+ return false;
4208
4472
  const launching = {
4209
4473
  ...currentProvisional,
4210
- mode,
4474
+ v: 2,
4475
+ mode: pendingLaunch.mode,
4211
4476
  phase: "launching",
4212
4477
  launchIdempotencyKey,
4213
4478
  sessionId: null,
4479
+ protocolVersion: pendingLaunch.protocolVersion,
4480
+ experienceMode: pendingLaunch.experienceMode,
4481
+ scopes: [...pendingLaunch.scopes],
4214
4482
  };
4215
4483
  if (!this.persistFreshAiReturnProvisional(launching))
4216
4484
  return false;
@@ -4226,7 +4494,7 @@ export default class AiLiveAssist {
4226
4494
  inboxId: messengerDescriptor.inboxId,
4227
4495
  linkId: this.companion.linkId,
4228
4496
  launchIdempotencyKey,
4229
- mode,
4497
+ mode: pendingLaunch.mode,
4230
4498
  previousAiSessionId: this.companion.currentAiSessionId,
4231
4499
  surface: this.messengerTransferredSurface,
4232
4500
  }
@@ -4234,10 +4502,11 @@ export default class AiLiveAssist {
4234
4502
  if (!messengerReturnAuthority)
4235
4503
  return false;
4236
4504
  this.messengerReturnRetryRemaining = 1;
4237
- await this.launch(mode, scopes, launchIdempotencyKey, {
4238
- startMuted: mode === "voice",
4505
+ await this.launch(pendingLaunch.mode, pendingLaunch.scopes, launchIdempotencyKey, {
4506
+ startMuted: pendingLaunch.mode === "voice",
4239
4507
  associateCompanion: false,
4240
4508
  messengerReturnAuthority,
4509
+ pendingLaunch,
4241
4510
  });
4242
4511
  this.messengerReturnRetryRemaining = null;
4243
4512
  const sessionId = this.result?.sessionId;
@@ -4432,6 +4701,7 @@ export default class AiLiveAssist {
4432
4701
  if (this.pushActive)
4433
4702
  this.media?.beginPushToTalk();
4434
4703
  this.closeMessengerBeforeOpen();
4704
+ await this.prepareMessengerIntegration();
4435
4705
  return true;
4436
4706
  }
4437
4707
  async associateFreshCompanionSession(mode, sessionId) {
@@ -4555,11 +4825,6 @@ export default class AiLiveAssist {
4555
4825
  const prepared = await this.withMessengerTimeout(messenger.openHandoffConversation(displayId, options), MESSENGER_LOAD_TIMEOUT_MS);
4556
4826
  if (!prepared || !surfaceCurrent())
4557
4827
  return;
4558
- const returnActivated = await this.createMessengerReturnActivationPromise(messenger, displayId);
4559
- if (!returnActivated || !surfaceCurrent()) {
4560
- this.closeMessengerBeforeOpen();
4561
- return;
4562
- }
4563
4828
  const switched = await this.requestCompanionSurface("messenger", mode, idempotencyKey("companion_switch"), result.sessionId);
4564
4829
  const switchedCurrent = () => generation === this.sessionEffectGeneration &&
4565
4830
  this.result === result &&
@@ -4627,6 +4892,11 @@ export default class AiLiveAssist {
4627
4892
  await this.restoreAiAfterFailedSupportSwitch(mode, result.sessionId, previousMuted, previousPushActive);
4628
4893
  return;
4629
4894
  }
4895
+ const returnActivated = await this.createMessengerReturnActivationPromise(messenger, displayId);
4896
+ if (!returnActivated || !switchedCurrent()) {
4897
+ await this.restoreAiAfterFailedSupportSwitch(mode, result.sessionId, previousMuted, previousPushActive);
4898
+ return;
4899
+ }
4630
4900
  this.messenger = messenger;
4631
4901
  this.messengerSurfaceActive = true;
4632
4902
  this.messengerTransferredConversationId = displayId;
@@ -6189,9 +6459,27 @@ export default class AiLiveAssist {
6189
6459
  releaseMessengerReturnIfPreRuntime();
6190
6460
  return;
6191
6461
  }
6192
- const experienceMode = bootstrap.experienceMode ?? "standard";
6462
+ const retryIntent = options.pendingLaunch;
6463
+ if (retryIntent &&
6464
+ canonicalJson({ mode, scopes, idempotencyKey: launchIdempotencyKey }) !==
6465
+ canonicalJson({
6466
+ mode: retryIntent.mode,
6467
+ scopes: retryIntent.scopes,
6468
+ idempotencyKey: retryIntent.idempotencyKey,
6469
+ })) {
6470
+ releaseMessengerReturnIfPreRuntime();
6471
+ return;
6472
+ }
6473
+ let durableLaunch = retryIntent
6474
+ ? bindPendingLaunchIntent(retryIntent, bootstrap)
6475
+ : freshPendingLaunchIntent(mode, scopes, launchIdempotencyKey, bootstrap);
6476
+ if (!durableLaunch) {
6477
+ this.view.showError(this.view.message("reconnectFailed"), false, false);
6478
+ releaseMessengerReturnIfPreRuntime();
6479
+ return;
6480
+ }
6481
+ const { experienceMode, protocolVersion } = durableLaunch;
6193
6482
  const voicePresence = experienceMode === "voice_presence";
6194
- const protocolVersion = resolveAiGuideProtocolVersion(bootstrap);
6195
6483
  if (voicePresence && mode !== "voice") {
6196
6484
  this.view.showError("Voice assist requires microphone access.", false, false);
6197
6485
  releaseMessengerReturnIfPreRuntime();
@@ -6212,11 +6500,7 @@ export default class AiLiveAssist {
6212
6500
  this.visitorActiveTakeoverOnly = false;
6213
6501
  }
6214
6502
  if (this.visitorActiveTakeoverOnly) {
6215
- const pendingLaunch = {
6216
- mode,
6217
- scopes: [...scopes],
6218
- idempotencyKey: launchIdempotencyKey,
6219
- };
6503
+ const pendingLaunch = durableLaunch;
6220
6504
  this.lastLaunch = pendingLaunch;
6221
6505
  this.view.showActiveElsewhere(false);
6222
6506
  this.view.showConnecting(true, mode);
@@ -6230,11 +6514,7 @@ export default class AiLiveAssist {
6230
6514
  }
6231
6515
  this.pageAssistanceAuthorityGeneration += 1;
6232
6516
  this.rotatePageAssistanceBindingGeneration();
6233
- this.lastLaunch = {
6234
- mode,
6235
- scopes: [...scopes],
6236
- idempotencyKey: launchIdempotencyKey,
6237
- };
6517
+ this.lastLaunch = { ...durableLaunch, scopes: [...durableLaunch.scopes] };
6238
6518
  this.launchInFlight = true;
6239
6519
  if (!this.lock.acquire()) {
6240
6520
  this.view.showActiveElsewhere(false);
@@ -6292,7 +6572,8 @@ export default class AiLiveAssist {
6292
6572
  if (voicePresence ||
6293
6573
  mode !== "voice" ||
6294
6574
  !bootstrap.capabilities?.text ||
6295
- options.messengerReturnAuthority !== undefined) {
6575
+ options.messengerReturnAuthority !== undefined ||
6576
+ retryIntent !== undefined) {
6296
6577
  throw error;
6297
6578
  }
6298
6579
  this.releaseMedia(media);
@@ -6322,22 +6603,20 @@ export default class AiLiveAssist {
6322
6603
  this.navigationConsentResetCandidate = null;
6323
6604
  this.navigationConsentReset = null;
6324
6605
  this.navigationConsentResetReady = null;
6325
- this.lastLaunch = {
6326
- mode: activeMode,
6327
- scopes: [...activeScopes],
6328
- idempotencyKey: launchIdempotencyKey,
6329
- };
6606
+ if (!retryIntent) {
6607
+ durableLaunch = {
6608
+ ...durableLaunch,
6609
+ mode: activeMode,
6610
+ scopes: [...activeScopes],
6611
+ };
6612
+ }
6613
+ this.lastLaunch = { ...durableLaunch, scopes: [...durableLaunch.scopes] };
6330
6614
  this.visualConsentGranted = activeScopes.includes("visual_context");
6331
6615
  assertConnectionCurrent();
6332
6616
  if (this.lock.ownsLatestFocus?.() === false &&
6333
6617
  this.relinquishForNewerFocus()) {
6334
6618
  return;
6335
6619
  }
6336
- const durableLaunch = {
6337
- mode: activeMode,
6338
- scopes: [...activeScopes],
6339
- idempotencyKey: launchIdempotencyKey,
6340
- };
6341
6620
  if (!this.markStagedPendingLaunchInFlight(durableLaunch)) {
6342
6621
  this.releaseMedia(media);
6343
6622
  this.cleanupTransport();
@@ -6382,16 +6661,19 @@ export default class AiLiveAssist {
6382
6661
  }
6383
6662
  }
6384
6663
  }
6664
+ if (result.schemaVersion !== protocolVersion ||
6665
+ result.protocolVersion !== protocolVersion ||
6666
+ result.experienceMode !== experienceMode) {
6667
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
6668
+ }
6385
6669
  if (this.terminalRequestPending) {
6386
- this.captureTerminalAuthorizedResult(result);
6670
+ if (!this.captureTerminalAuthorizedResult(result)) {
6671
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
6672
+ }
6387
6673
  runtimeSessionCreated = true;
6388
6674
  return;
6389
6675
  }
6390
6676
  assertConnectionCurrent();
6391
- if (result.experienceMode !== experienceMode ||
6392
- result.protocolVersion !== protocolVersion) {
6393
- throw new RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
6394
- }
6395
6677
  if (!this.commitAuthorizedRuntimePath(requestedLaunchPath, true)) {
6396
6678
  throw new RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime authorized an invalid page path.", false, null, 502);
6397
6679
  }
@@ -6526,7 +6808,8 @@ export default class AiLiveAssist {
6526
6808
  this.terminalRequestPending ||
6527
6809
  this.ended ||
6528
6810
  this.destroyed ||
6529
- this.lastLaunch?.idempotencyKey !== pendingLaunch.idempotencyKey ||
6811
+ !this.lastLaunch ||
6812
+ !this.pendingLaunchMatches(this.lastLaunch, pendingLaunch) ||
6530
6813
  this.readResume()) {
6531
6814
  return;
6532
6815
  }
@@ -6558,7 +6841,8 @@ export default class AiLiveAssist {
6558
6841
  this.visitorActiveEligibilityRecheckKey = pendingLaunch.idempotencyKey;
6559
6842
  const pendingIntentIsCurrent = () => launchGeneration === this.launchGeneration &&
6560
6843
  this.visitorActiveEligibilityRecheckController === controller &&
6561
- this.lastLaunch?.idempotencyKey === pendingLaunch.idempotencyKey &&
6844
+ Boolean(this.lastLaunch &&
6845
+ this.pendingLaunchMatches(this.lastLaunch, pendingLaunch)) &&
6562
6846
  !this.terminalRequestPending &&
6563
6847
  !this.ended &&
6564
6848
  !this.destroyed &&
@@ -6618,7 +6902,7 @@ export default class AiLiveAssist {
6618
6902
  this.visitorActiveTakeoverOnly = false;
6619
6903
  this.lastLaunch = null;
6620
6904
  this.view?.showActiveElsewhere(false);
6621
- await this.launch(pendingLaunch.mode, pendingLaunch.scopes, pendingLaunch.idempotencyKey);
6905
+ await this.launch(pendingLaunch.mode, pendingLaunch.scopes, pendingLaunch.idempotencyKey, { pendingLaunch });
6622
6906
  return;
6623
6907
  }
6624
6908
  if (status === "visitor_limited") {
@@ -6650,7 +6934,8 @@ export default class AiLiveAssist {
6650
6934
  this.visitorActiveEligibilityRecheck = operation;
6651
6935
  }
6652
6936
  finishVisitorActiveEligibilityRecheck(pendingLaunch, messageKey) {
6653
- if (this.lastLaunch?.idempotencyKey !== pendingLaunch.idempotencyKey) {
6937
+ if (!this.lastLaunch ||
6938
+ !this.pendingLaunchMatches(this.lastLaunch, pendingLaunch)) {
6654
6939
  return;
6655
6940
  }
6656
6941
  this.lastLaunch = null;
@@ -6807,8 +7092,15 @@ export default class AiLiveAssist {
6807
7092
  }
6808
7093
  }
6809
7094
  }
7095
+ if (result.schemaVersion !== record.protocolVersion ||
7096
+ result.protocolVersion !== record.protocolVersion ||
7097
+ result.experienceMode !== record.experienceMode) {
7098
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The saved voice assist session is no longer compatible.", false, null, 409);
7099
+ }
6810
7100
  if (this.terminalRequestPending) {
6811
- this.captureTerminalAuthorizedResult(result, record);
7101
+ if (!this.captureTerminalAuthorizedResult(result, record)) {
7102
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The saved voice assist session is no longer compatible.", false, null, 409);
7103
+ }
6812
7104
  if (modeUpgrade)
6813
7105
  this.modeUpgradeTerminalResult = result;
6814
7106
  attemptedCredentials.add(result.resumeCredential);
@@ -6818,8 +7110,6 @@ export default class AiLiveAssist {
6818
7110
  return;
6819
7111
  assertConnectionCurrent();
6820
7112
  if (result.sessionId !== record.sessionId ||
6821
- result.protocolVersion !== record.protocolVersion ||
6822
- result.experienceMode !== record.experienceMode ||
6823
7113
  (result.mode !== undefined &&
6824
7114
  result.mode !== "text" &&
6825
7115
  result.mode !== "voice") ||
@@ -7350,6 +7640,7 @@ export default class AiLiveAssist {
7350
7640
  ? null
7351
7641
  : (result.assistant?.welcomeMessage ??
7352
7642
  this.bootstrap?.appearance?.welcomeMessage);
7643
+ let renderedPendingTextQuestion = false;
7353
7644
  if (mode === "text" &&
7354
7645
  this.pendingQuestion &&
7355
7646
  !this.pendingQuestion.shown) {
@@ -7357,8 +7648,11 @@ export default class AiLiveAssist {
7357
7648
  this.pendingQuestion.shown = true;
7358
7649
  this.rememberTranscript(this.pendingQuestion.submissionId, "user", this.pendingQuestion.text);
7359
7650
  (this.renderedTranscriptIds ?? (this.renderedTranscriptIds = new Set())).add(this.pendingQuestion.submissionId);
7651
+ renderedPendingTextQuestion = true;
7360
7652
  }
7361
- const visibleWelcome = this.transcriptRestoredFromCache ||
7653
+ const visibleWelcome = resume ||
7654
+ renderedPendingTextQuestion ||
7655
+ this.transcriptRestoredFromCache ||
7362
7656
  (this.view instanceof VoicePresenceView && mode === "voice")
7363
7657
  ? null
7364
7658
  : welcomeMessage;
@@ -13093,7 +13387,7 @@ export default class AiLiveAssist {
13093
13387
  return;
13094
13388
  }
13095
13389
  this.messengerReturnRetryRemaining -= 1;
13096
- const retryAttempt = this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey);
13390
+ const retryAttempt = this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey, { pendingLaunch: this.lastLaunch });
13097
13391
  return retryAttempt.finally(() => {
13098
13392
  if (this.result && !this.ended) {
13099
13393
  this.messengerReturnRetryRemaining = null;
@@ -13113,7 +13407,7 @@ export default class AiLiveAssist {
13113
13407
  return this.resume(resume);
13114
13408
  }
13115
13409
  if (this.lastLaunch) {
13116
- return this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey);
13410
+ return this.launch(this.lastLaunch.mode, this.lastLaunch.scopes, this.lastLaunch.idempotencyKey, { pendingLaunch: this.lastLaunch });
13117
13411
  }
13118
13412
  }
13119
13413
  scheduleReconnect(force = false) {
@@ -13443,6 +13737,18 @@ export default class AiLiveAssist {
13443
13737
  canonicalJson(left.messengerBinding) ===
13444
13738
  canonicalJson(right.messengerBinding));
13445
13739
  }
13740
+ sameResumeAuthorityIdentity(left, right) {
13741
+ const credentialRotated = left.resumeCredential !== right.resumeCredential;
13742
+ if (!credentialRotated && left.expiresAt !== right.expiresAt)
13743
+ return false;
13744
+ const { resumeCredential: _leftCredential, expiresAt: _leftCredentialExpiry, ...leftAuthority } = left;
13745
+ const { resumeCredential: _rightCredential, expiresAt: _rightCredentialExpiry, ...rightAuthority } = right;
13746
+ void _leftCredential;
13747
+ void _rightCredential;
13748
+ void _leftCredentialExpiry;
13749
+ void _rightCredentialExpiry;
13750
+ return canonicalJson(leftAuthority) === canonicalJson(rightAuthority);
13751
+ }
13446
13752
  sameModeUpgradeIntent(left, right) {
13447
13753
  return (left?.requestId === right?.requestId &&
13448
13754
  left?.resumeIdempotencyKey === right?.resumeIdempotencyKey &&
@@ -13811,10 +14117,22 @@ export default class AiLiveAssist {
13811
14117
  this.state("ended");
13812
14118
  }
13813
14119
  captureTerminalAuthorizedResult(result, sourceRecord = null) {
13814
- if (sourceRecord &&
13815
- (result.sessionId !== sourceRecord.sessionId ||
13816
- result.protocolVersion !== sourceRecord.protocolVersion ||
13817
- result.experienceMode !== sourceRecord.experienceMode)) {
14120
+ const pendingLaunch = this.terminalPendingLaunch &&
14121
+ pendingLaunchIsBound(this.terminalPendingLaunch)
14122
+ ? this.terminalPendingLaunch
14123
+ : null;
14124
+ const expectedProtocolVersion = sourceRecord?.protocolVersion ??
14125
+ pendingLaunch?.protocolVersion ??
14126
+ result.protocolVersion;
14127
+ const expectedExperienceMode = sourceRecord?.experienceMode ??
14128
+ pendingLaunch?.experienceMode ??
14129
+ result.experienceMode;
14130
+ if (result.schemaVersion !== expectedProtocolVersion ||
14131
+ result.protocolVersion !== expectedProtocolVersion ||
14132
+ result.experienceMode !== expectedExperienceMode ||
14133
+ (sourceRecord &&
14134
+ (result.sessionId !== sourceRecord.sessionId ||
14135
+ result.protocolVersion !== sourceRecord.protocolVersion))) {
13818
14136
  return false;
13819
14137
  }
13820
14138
  const currentIntent = this.terminalEndIntent;
@@ -13840,6 +14158,22 @@ export default class AiLiveAssist {
13840
14158
  }
13841
14159
  return true;
13842
14160
  }
14161
+ retryRejectedTerminalAuthorizedResult(result) {
14162
+ const error = this.rejectTerminalAuthorizedResult(result);
14163
+ return this.retryTerminalEnd(error);
14164
+ }
14165
+ rejectTerminalAuthorizedResult(result) {
14166
+ if (this.result === result)
14167
+ this.result = null;
14168
+ if (this.modeUpgradeTerminalResult === result) {
14169
+ this.modeUpgradeTerminalResult = null;
14170
+ }
14171
+ this.terminalSessionAuthorization = null;
14172
+ this.terminalSessionAuthorizationController = null;
14173
+ const error = new RuntimeClientError("PROTOCOL_INVALID", "The assist runtime returned an incompatible terminal session.", false, null, 409);
14174
+ this.terminalSessionAuthorizationFailure = error;
14175
+ return error;
14176
+ }
13843
14177
  completeRuntimeEnd() {
13844
14178
  if (this.ended)
13845
14179
  return Promise.resolve();
@@ -13870,8 +14204,9 @@ export default class AiLiveAssist {
13870
14204
  if (terminalAuthorization) {
13871
14205
  return this.waitForTerminalRequestDeadline(terminalAuthorization, this.terminalSessionAuthorizationController).then((authorized) => {
13872
14206
  const result = authorized ?? this.result ?? this.modeUpgradeTerminalResult;
13873
- if (result) {
13874
- this.captureTerminalAuthorizedResult(result, this.terminalResumeRecord);
14207
+ if (result &&
14208
+ !this.captureTerminalAuthorizedResult(result, this.terminalResumeRecord)) {
14209
+ return this.retryRejectedTerminalAuthorizedResult(result);
13875
14210
  }
13876
14211
  return this.finishRuntimeEndAttempt(result);
13877
14212
  }, (error) => {
@@ -13888,7 +14223,9 @@ export default class AiLiveAssist {
13888
14223
  }
13889
14224
  if (this.terminalPendingLaunch) {
13890
14225
  return this.requestTerminalPendingLaunch(this.terminalPendingLaunch).then((result) => {
13891
- this.captureTerminalAuthorizedResult(result);
14226
+ if (!this.captureTerminalAuthorizedResult(result)) {
14227
+ return this.retryRejectedTerminalAuthorizedResult(result);
14228
+ }
13892
14229
  return this.finishRuntimeEndAttempt(result);
13893
14230
  }, (recoveryError) => this.retryTerminalEnd(recoveryError));
13894
14231
  }
@@ -13915,7 +14252,9 @@ export default class AiLiveAssist {
13915
14252
  return this.finishRuntimeEndAttempt(null);
13916
14253
  if (this.terminalPendingLaunch) {
13917
14254
  return this.requestTerminalPendingLaunch(this.terminalPendingLaunch).then((result) => {
13918
- this.captureTerminalAuthorizedResult(result);
14255
+ if (!this.captureTerminalAuthorizedResult(result)) {
14256
+ return this.retryRejectedTerminalAuthorizedResult(result);
14257
+ }
13919
14258
  return this.finishRuntimeEndAttempt(result);
13920
14259
  }, (error) => this.retryTerminalEnd(error));
13921
14260
  }
@@ -14072,13 +14411,21 @@ export default class AiLiveAssist {
14072
14411
  requestTerminalPendingLaunch(pendingLaunch) {
14073
14412
  const intent = this.terminalEndIntent;
14074
14413
  if (!intent ||
14075
- intent.launchIdempotencyKey !== pendingLaunch.idempotencyKey ||
14076
- !this.markStagedPendingLaunchInFlight(pendingLaunch)) {
14414
+ intent.launchIdempotencyKey !== pendingLaunch.idempotencyKey) {
14077
14415
  return Promise.reject(new TerminalRequestDeferredError());
14078
14416
  }
14079
14417
  const controller = new AbortController();
14080
14418
  const request = (async () => {
14081
14419
  const bootstrap = await this.terminalPendingLaunchBootstrap(controller.signal);
14420
+ const boundPendingLaunch = bindPendingLaunchIntent(pendingLaunch, bootstrap);
14421
+ if (!boundPendingLaunch ||
14422
+ !this.markStagedPendingLaunchInFlight(boundPendingLaunch)) {
14423
+ throw new TerminalRequestDeferredError();
14424
+ }
14425
+ this.terminalPendingLaunch = {
14426
+ ...boundPendingLaunch,
14427
+ scopes: [...boundPendingLaunch.scopes],
14428
+ };
14082
14429
  const replay = await this.replayReadiness.wait();
14083
14430
  if (this.destroyed ||
14084
14431
  this.ended ||
@@ -14087,28 +14434,34 @@ export default class AiLiveAssist {
14087
14434
  throw new TerminalRequestDeferredError();
14088
14435
  }
14089
14436
  this.replayReadiness.assertCurrent(replay);
14090
- return this.runtime.launch({
14437
+ const result = await this.runtime.launch({
14091
14438
  siteKey: this.siteKey,
14092
14439
  configRevision: bootstrap.configRevision,
14093
14440
  path: path(),
14094
14441
  tabId: this.tabId,
14095
- mode: pendingLaunch.mode,
14442
+ mode: boundPendingLaunch.mode,
14096
14443
  anonymousId: this.visitorId(),
14097
14444
  replay,
14098
14445
  disclosureChecksum: bootstrap.consent.disclosureChecksum,
14099
- scopes: pendingLaunch.scopes,
14446
+ scopes: boundPendingLaunch.scopes,
14100
14447
  locale: this.locale,
14101
- microphone: pendingLaunch.mode === "voice",
14448
+ microphone: boundPendingLaunch.mode === "voice",
14102
14449
  reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)")
14103
14450
  .matches,
14104
- protocolVersion: resolveAiGuideProtocolVersion(bootstrap),
14105
- experienceMode: bootstrap.experienceMode ?? "standard",
14451
+ protocolVersion: boundPendingLaunch.protocolVersion,
14452
+ experienceMode: boundPendingLaunch.experienceMode,
14106
14453
  ...(this.activeFallback
14107
14454
  ? { fallbackIntent: this.activeFallback }
14108
14455
  : {}),
14109
14456
  lowPower: this.lowPowerMode(),
14110
- idempotencyKey: pendingLaunch.idempotencyKey,
14457
+ idempotencyKey: boundPendingLaunch.idempotencyKey,
14111
14458
  }, controller.signal);
14459
+ if (result.schemaVersion !== boundPendingLaunch.protocolVersion ||
14460
+ result.protocolVersion !== boundPendingLaunch.protocolVersion ||
14461
+ result.experienceMode !== boundPendingLaunch.experienceMode) {
14462
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The voice assist runtime returned an incompatible session.", false, null, 502);
14463
+ }
14464
+ return result;
14112
14465
  })();
14113
14466
  return this.waitForTerminalRequestDeadline(request, controller);
14114
14467
  }
@@ -14272,8 +14625,7 @@ export default class AiLiveAssist {
14272
14625
  const retainTerminalLaunch = Boolean(this.terminalSessionAuthorization &&
14273
14626
  this.terminalPendingLaunch &&
14274
14627
  staged?.state === "in_flight" &&
14275
- staged.pendingLaunch.idempotencyKey ===
14276
- this.terminalPendingLaunch.idempotencyKey);
14628
+ this.pendingLaunchMatches(staged.pendingLaunch, this.terminalPendingLaunch));
14277
14629
  if (!retainTerminalLaunch)
14278
14630
  this.clearOwnedStagedPendingLaunch();
14279
14631
  this.lock?.cancelPendingHandoff?.();
@@ -14697,15 +15049,18 @@ export default class AiLiveAssist {
14697
15049
  focusedHandoffPayload() {
14698
15050
  this.transcript ?? (this.transcript = []);
14699
15051
  const resume = this.readResume();
15052
+ const pendingLaunch = !resume && this.lastLaunch && this.bootstrap
15053
+ ? bindPendingLaunchIntent(this.lastLaunch, this.bootstrap)
15054
+ : null;
14700
15055
  return {
14701
- v: 1,
15056
+ v: pendingLaunch ? 2 : 1,
14702
15057
  resume,
14703
15058
  transcript: this.transcript.map((entry) => ({ ...entry })),
14704
- ...(!resume && this.lastLaunch
15059
+ ...(pendingLaunch
14705
15060
  ? {
14706
15061
  pendingLaunch: {
14707
- ...this.lastLaunch,
14708
- scopes: [...this.lastLaunch.scopes],
15062
+ ...pendingLaunch,
15063
+ scopes: [...pendingLaunch.scopes],
14709
15064
  },
14710
15065
  }
14711
15066
  : {}),
@@ -14720,33 +15075,157 @@ export default class AiLiveAssist {
14720
15075
  }
14721
15076
  }
14722
15077
  readStagedPendingLaunch() {
14723
- let raw = null;
14724
15078
  try {
14725
- raw = localStorage.getItem(this.stagedPendingLaunchStorageKey);
14726
- const staged = parseStagedPendingLaunch(raw);
14727
- if (raw && !staged) {
15079
+ const v2Raw = localStorage.getItem(this.stagedPendingLaunchStorageKey);
15080
+ const parsedV2 = parseStagedPendingLaunch(v2Raw);
15081
+ const v2 = parsedV2?.v === 2 ? parsedV2 : null;
15082
+ if (v2Raw && !v2) {
14728
15083
  localStorage.removeItem(this.stagedPendingLaunchStorageKey);
14729
15084
  }
14730
- return staged;
15085
+ const legacyRaw = localStorage.getItem(this.legacyStagedPendingLaunchStorageKey);
15086
+ const parsedLegacy = parseStagedPendingLaunch(legacyRaw);
15087
+ const legacy = parsedLegacy?.v === 1 ? parsedLegacy : null;
15088
+ if (legacyRaw && !legacy) {
15089
+ localStorage.removeItem(this.legacyStagedPendingLaunchStorageKey);
15090
+ }
15091
+ if (!v2)
15092
+ return legacy;
15093
+ if (!legacy)
15094
+ return v2;
15095
+ const authorityOrder = compareTransferAuthority(v2.authority, legacy.authority);
15096
+ if (authorityOrder !== 0)
15097
+ return authorityOrder > 0 ? v2 : legacy;
15098
+ if (v2.sourceOwnerId !== legacy.sourceOwnerId ||
15099
+ !this.pendingLaunchBaseMatches(v2.pendingLaunch, legacy.pendingLaunch)) {
15100
+ return null;
15101
+ }
15102
+ // A V2 record is the only copy that preserves the exact launch body.
15103
+ // Prefer it for an equal authority; the legacy copy is cleaned only
15104
+ // after a matching V2 write has been verified.
15105
+ return v2;
14731
15106
  }
14732
15107
  catch {
14733
15108
  return null;
14734
15109
  }
14735
15110
  }
14736
15111
  persistStagedPendingLaunch(staged) {
15112
+ let rollback = null;
14737
15113
  try {
14738
- localStorage.setItem(this.stagedPendingLaunchStorageKey, JSON.stringify(staged));
14739
- return (canonicalJson(this.readStagedPendingLaunch()) === canonicalJson(staged));
15114
+ const serializedPendingLaunch = staged.v === 1
15115
+ ? {
15116
+ mode: staged.pendingLaunch.mode,
15117
+ scopes: [...staged.pendingLaunch.scopes],
15118
+ idempotencyKey: staged.pendingLaunch.idempotencyKey,
15119
+ }
15120
+ : staged.pendingLaunch;
15121
+ const storageKey = staged.v === 2
15122
+ ? this.stagedPendingLaunchStorageKey
15123
+ : this.legacyStagedPendingLaunchStorageKey;
15124
+ const serialized = JSON.stringify({
15125
+ ...staged,
15126
+ pendingLaunch: serializedPendingLaunch,
15127
+ });
15128
+ const previousSerialized = localStorage.getItem(storageKey);
15129
+ const previousLegacySerialized = localStorage.getItem(this.legacyStagedPendingLaunchStorageKey);
15130
+ let removedMatchingLegacy = false;
15131
+ rollback = () => {
15132
+ if (localStorage.getItem(storageKey) === serialized) {
15133
+ if (previousSerialized === null)
15134
+ localStorage.removeItem(storageKey);
15135
+ else
15136
+ localStorage.setItem(storageKey, previousSerialized);
15137
+ }
15138
+ if (removedMatchingLegacy &&
15139
+ previousLegacySerialized !== null &&
15140
+ localStorage.getItem(this.legacyStagedPendingLaunchStorageKey) ===
15141
+ null) {
15142
+ localStorage.setItem(this.legacyStagedPendingLaunchStorageKey, previousLegacySerialized);
15143
+ }
15144
+ };
15145
+ localStorage.setItem(storageKey, serialized);
15146
+ const normalizedPendingLaunch = staged.v === 1
15147
+ ? {
15148
+ ...serializedPendingLaunch,
15149
+ protocolVersion: null,
15150
+ experienceMode: null,
15151
+ }
15152
+ : staged.pendingLaunch;
15153
+ const expected = {
15154
+ ...staged,
15155
+ pendingLaunch: normalizedPendingLaunch,
15156
+ };
15157
+ const directReadback = parseStagedPendingLaunch(localStorage.getItem(storageKey));
15158
+ if (canonicalJson(directReadback) !== canonicalJson(expected)) {
15159
+ rollback();
15160
+ return false;
15161
+ }
15162
+ if (staged.v === 2) {
15163
+ const legacySerialized = localStorage.getItem(this.legacyStagedPendingLaunchStorageKey);
15164
+ const legacy = parseStagedPendingLaunch(legacySerialized);
15165
+ const equalAuthority = legacy?.v === 1 &&
15166
+ compareTransferAuthority(legacy.authority, staged.authority) === 0;
15167
+ if (equalAuthority &&
15168
+ (legacy.sourceOwnerId !== staged.sourceOwnerId ||
15169
+ !this.pendingLaunchBaseMatches(legacy.pendingLaunch, staged.pendingLaunch))) {
15170
+ rollback();
15171
+ return false;
15172
+ }
15173
+ if (legacy?.v === 1 &&
15174
+ legacy.sourceOwnerId === staged.sourceOwnerId &&
15175
+ equalAuthority &&
15176
+ this.pendingLaunchBaseMatches(legacy.pendingLaunch, staged.pendingLaunch) &&
15177
+ legacySerialized !== null &&
15178
+ localStorage.getItem(this.legacyStagedPendingLaunchStorageKey) ===
15179
+ legacySerialized) {
15180
+ localStorage.removeItem(this.legacyStagedPendingLaunchStorageKey);
15181
+ removedMatchingLegacy = true;
15182
+ }
15183
+ }
15184
+ const persisted = canonicalJson(this.readStagedPendingLaunch()) ===
15185
+ canonicalJson(expected);
15186
+ if (!persisted)
15187
+ rollback();
15188
+ return persisted;
14740
15189
  }
14741
15190
  catch {
15191
+ try {
15192
+ rollback?.();
15193
+ }
15194
+ catch {
15195
+ // Storage is already unavailable. Leave the launch unclaimed and
15196
+ // fail closed rather than trusting a partially observed write.
15197
+ }
14742
15198
  return false;
14743
15199
  }
14744
15200
  }
15201
+ pendingLaunchBaseMatches(left, right) {
15202
+ return (canonicalJson({
15203
+ mode: left.mode,
15204
+ scopes: left.scopes,
15205
+ idempotencyKey: left.idempotencyKey,
15206
+ }) ===
15207
+ canonicalJson({
15208
+ mode: right.mode,
15209
+ scopes: right.scopes,
15210
+ idempotencyKey: right.idempotencyKey,
15211
+ }));
15212
+ }
14745
15213
  pendingLaunchMatches(left, right) {
14746
- return canonicalJson(left) === canonicalJson(right);
15214
+ const leftBound = pendingLaunchIsBound(left);
15215
+ const rightBound = pendingLaunchIsBound(right);
15216
+ if (leftBound !== rightBound)
15217
+ return false;
15218
+ if (leftBound && rightBound) {
15219
+ return canonicalJson(left) === canonicalJson(right);
15220
+ }
15221
+ return this.pendingLaunchBaseMatches(left, right);
14747
15222
  }
14748
15223
  stagePendingLaunch(pendingLaunch, authority, sourceOwnerId) {
14749
- if (!PUBLIC_REQUEST_ID_PATTERN.test(sourceOwnerId) ||
15224
+ const boundPendingLaunch = this.bootstrap
15225
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15226
+ : null;
15227
+ if (!boundPendingLaunch ||
15228
+ !PUBLIC_REQUEST_ID_PATTERN.test(sourceOwnerId) ||
14750
15229
  sourceOwnerId === authority.ownerId) {
14751
15230
  return false;
14752
15231
  }
@@ -14757,35 +15236,82 @@ export default class AiLiveAssist {
14757
15236
  if (authorityOrder < 0 ||
14758
15237
  (authorityOrder === 0 &&
14759
15238
  (existing?.sourceOwnerId !== sourceOwnerId ||
14760
- !this.pendingLaunchMatches(existing.pendingLaunch, pendingLaunch) ||
15239
+ !this.pendingLaunchMatches(!pendingLaunchIsBound(existing.pendingLaunch) && this.bootstrap
15240
+ ? (bindPendingLaunchIntent(existing.pendingLaunch, this.bootstrap) ?? existing.pendingLaunch)
15241
+ : existing.pendingLaunch, boundPendingLaunch) ||
14761
15242
  existing.state !== "prepared"))) {
14762
15243
  return false;
14763
15244
  }
14764
15245
  return this.persistStagedPendingLaunch({
14765
- v: 1,
15246
+ v: 2,
14766
15247
  sourceOwnerId,
14767
15248
  authority: { ...authority },
14768
15249
  state: "prepared",
14769
15250
  claimAuthority: null,
14770
15251
  expiresAt: Date.now() + STAGED_PENDING_LAUNCH_TTL_MS,
14771
15252
  pendingLaunch: {
14772
- ...pendingLaunch,
14773
- scopes: [...pendingLaunch.scopes],
15253
+ ...boundPendingLaunch,
15254
+ scopes: [...boundPendingLaunch.scopes],
14774
15255
  },
14775
15256
  });
14776
15257
  }
14777
15258
  clearStagedPendingLaunch(pendingLaunch, authority) {
14778
15259
  const staged = this.readStagedPendingLaunch();
15260
+ const comparablePending = this.bootstrap && !pendingLaunchIsBound(pendingLaunch)
15261
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15262
+ : pendingLaunch;
15263
+ const comparableStaged = this.bootstrap && staged && !pendingLaunchIsBound(staged.pendingLaunch)
15264
+ ? bindPendingLaunchIntent(staged.pendingLaunch, this.bootstrap)
15265
+ : staged?.pendingLaunch;
14779
15266
  if (!staged ||
15267
+ !comparablePending ||
15268
+ !comparableStaged ||
14780
15269
  compareTransferAuthority(staged.authority, authority) !== 0 ||
14781
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
14782
- return;
15270
+ !this.pendingLaunchMatches(comparableStaged, comparablePending)) {
15271
+ return false;
14783
15272
  }
14784
15273
  try {
14785
- localStorage.removeItem(this.stagedPendingLaunchStorageKey);
15274
+ const snapshots = [
15275
+ {
15276
+ key: this.stagedPendingLaunchStorageKey,
15277
+ raw: localStorage.getItem(this.stagedPendingLaunchStorageKey),
15278
+ version: 2,
15279
+ },
15280
+ {
15281
+ key: this.legacyStagedPendingLaunchStorageKey,
15282
+ raw: localStorage.getItem(this.legacyStagedPendingLaunchStorageKey),
15283
+ version: 1,
15284
+ },
15285
+ ];
15286
+ const removable = [];
15287
+ for (const snapshot of snapshots) {
15288
+ if (snapshot.raw === null)
15289
+ continue;
15290
+ const shadow = parseStagedPendingLaunch(snapshot.raw);
15291
+ if (!shadow || shadow.v !== snapshot.version)
15292
+ return false;
15293
+ const authorityOrder = compareTransferAuthority(shadow.authority, staged.authority);
15294
+ if (authorityOrder > 0)
15295
+ continue;
15296
+ if (authorityOrder === 0 &&
15297
+ (shadow.sourceOwnerId !== staged.sourceOwnerId ||
15298
+ !this.pendingLaunchBaseMatches(shadow.pendingLaunch, staged.pendingLaunch))) {
15299
+ return false;
15300
+ }
15301
+ removable.push(snapshot);
15302
+ }
15303
+ if (removable.some((snapshot) => localStorage.getItem(snapshot.key) !== snapshot.raw)) {
15304
+ return false;
15305
+ }
15306
+ for (const snapshot of removable) {
15307
+ if (localStorage.getItem(snapshot.key) !== snapshot.raw)
15308
+ return false;
15309
+ localStorage.removeItem(snapshot.key);
15310
+ }
15311
+ return removable.every((snapshot) => localStorage.getItem(snapshot.key) === null);
14786
15312
  }
14787
15313
  catch {
14788
- /* noop */
15314
+ return false;
14789
15315
  }
14790
15316
  }
14791
15317
  clearOwnedStagedPendingLaunch() {
@@ -14802,7 +15328,7 @@ export default class AiLiveAssist {
14802
15328
  const staged = this.readStagedPendingLaunch();
14803
15329
  if (!pendingLaunch ||
14804
15330
  !staged ||
14805
- staged.pendingLaunch.idempotencyKey !== pendingLaunch.idempotencyKey ||
15331
+ !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch) ||
14806
15332
  this.terminalEndIntent?.launchIdempotencyKey !==
14807
15333
  pendingLaunch.idempotencyKey) {
14808
15334
  return;
@@ -14813,11 +15339,15 @@ export default class AiLiveAssist {
14813
15339
  const pendingLaunch = parseFocusedHandoffPayload(value)?.pendingLaunch;
14814
15340
  if (!pendingLaunch)
14815
15341
  return true;
15342
+ const boundPendingLaunch = this.bootstrap
15343
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15344
+ : null;
14816
15345
  const staged = this.readStagedPendingLaunch();
14817
- if (!staged ||
15346
+ if (!boundPendingLaunch ||
15347
+ !staged ||
14818
15348
  staged.state !== "prepared" ||
14819
15349
  compareTransferAuthority(staged.authority, authority) !== 0 ||
14820
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15350
+ !this.pendingLaunchMatches(staged.pendingLaunch, boundPendingLaunch)) {
14821
15351
  return false;
14822
15352
  }
14823
15353
  return this.persistStagedPendingLaunch({
@@ -14861,6 +15391,12 @@ export default class AiLiveAssist {
14861
15391
  }
14862
15392
  markStagedPendingLaunchInFlight(pendingLaunch) {
14863
15393
  const staged = this.readStagedPendingLaunch();
15394
+ const boundPendingLaunch = pendingLaunchIsBound(pendingLaunch)
15395
+ ? pendingLaunch
15396
+ : this.bootstrap
15397
+ ? bindPendingLaunchIntent(pendingLaunch, this.bootstrap)
15398
+ : null;
15399
+ const durablePendingLaunch = boundPendingLaunch ?? pendingLaunch;
14864
15400
  // AiLiveAssist always installs a TabActivityLock with durable authority.
14865
15401
  // Keep prototype-level test harnesses that intentionally omit the lock
14866
15402
  // implementation isolated from the persistence contract.
@@ -14872,40 +15408,54 @@ export default class AiLiveAssist {
14872
15408
  }
14873
15409
  if (!staged) {
14874
15410
  return this.persistStagedPendingLaunch({
14875
- v: 1,
15411
+ v: boundPendingLaunch ? 2 : 1,
14876
15412
  sourceOwnerId: randomId("launch_source"),
14877
15413
  authority: { ...ownedAuthority },
14878
15414
  state: "in_flight",
14879
15415
  claimAuthority: { ...ownedAuthority },
14880
15416
  expiresAt: Date.now() + STAGED_PENDING_LAUNCH_TTL_MS,
14881
15417
  pendingLaunch: {
14882
- ...pendingLaunch,
14883
- scopes: [...pendingLaunch.scopes],
15418
+ ...durablePendingLaunch,
15419
+ scopes: [...durablePendingLaunch.scopes],
14884
15420
  },
14885
15421
  });
14886
15422
  }
15423
+ const comparableStagedPendingLaunch = pendingLaunchIsBound(staged.pendingLaunch)
15424
+ ? staged.pendingLaunch
15425
+ : this.bootstrap
15426
+ ? bindPendingLaunchIntent(staged.pendingLaunch, this.bootstrap)
15427
+ : null;
14887
15428
  if (staged.pendingLaunch.idempotencyKey !== pendingLaunch.idempotencyKey ||
14888
15429
  !staged.claimAuthority ||
14889
15430
  compareTransferAuthority(staged.claimAuthority, ownedAuthority) !== 0 ||
14890
- (staged.state !== "claimed" && staged.state !== "in_flight")) {
15431
+ (staged.state !== "claimed" && staged.state !== "in_flight") ||
15432
+ (boundPendingLaunch
15433
+ ? !comparableStagedPendingLaunch ||
15434
+ !this.pendingLaunchMatches(comparableStagedPendingLaunch, boundPendingLaunch)
15435
+ : !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch))) {
14891
15436
  return false;
14892
15437
  }
14893
- if (staged.state === "in_flight")
15438
+ if (staged.state === "in_flight" &&
15439
+ staged.v === (boundPendingLaunch ? 2 : 1) &&
15440
+ this.pendingLaunchMatches(staged.pendingLaunch, durablePendingLaunch)) {
14894
15441
  return true;
15442
+ }
14895
15443
  return this.persistStagedPendingLaunch({
14896
15444
  ...staged,
15445
+ v: boundPendingLaunch ? 2 : 1,
14897
15446
  state: "in_flight",
14898
15447
  pendingLaunch: {
14899
- ...pendingLaunch,
14900
- scopes: [...pendingLaunch.scopes],
15448
+ ...durablePendingLaunch,
15449
+ scopes: [...durablePendingLaunch.scopes],
14901
15450
  },
14902
15451
  });
14903
15452
  }
14904
15453
  acceptStagedPendingLaunch(pendingLaunch, result) {
14905
15454
  const staged = this.readStagedPendingLaunch();
14906
- if (!staged ||
14907
- !this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15455
+ if (!staged)
14908
15456
  return true;
15457
+ if (!this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch)) {
15458
+ return false;
14909
15459
  }
14910
15460
  const ownedAuthority = this.lock.ownedAuthority?.() ?? null;
14911
15461
  const stored = this.readResume();
@@ -14929,17 +15479,11 @@ export default class AiLiveAssist {
14929
15479
  })) {
14930
15480
  return false;
14931
15481
  }
14932
- try {
14933
- localStorage.removeItem(this.stagedPendingLaunchStorageKey);
14934
- return localStorage.getItem(this.stagedPendingLaunchStorageKey) === null;
14935
- }
14936
- catch {
14937
- return false;
14938
- }
15482
+ return this.clearStagedPendingLaunch(staged.pendingLaunch, staged.authority);
14939
15483
  }
14940
15484
  hasStagedPendingLaunch(pendingLaunch) {
14941
- return (this.readStagedPendingLaunch()?.pendingLaunch.idempotencyKey ===
14942
- pendingLaunch.idempotencyKey);
15485
+ const staged = this.readStagedPendingLaunch();
15486
+ return Boolean(staged && this.pendingLaunchMatches(staged.pendingLaunch, pendingLaunch));
14943
15487
  }
14944
15488
  storeTransferAuthority(authority) {
14945
15489
  const current = this.readTransferAuthority();
@@ -15017,12 +15561,21 @@ export default class AiLiveAssist {
15017
15561
  if (!this.freshAiReturnProvisionalStorageKey)
15018
15562
  return null;
15019
15563
  try {
15020
- const serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15021
- const provisional = parseFreshAiReturnProvisional(serialized ? JSON.parse(serialized) : null);
15022
- if (serialized && !provisional) {
15564
+ const v2Serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15565
+ const parsedV2 = parseFreshAiReturnProvisional(v2Serialized ? JSON.parse(v2Serialized) : null);
15566
+ const v2 = parsedV2?.v === 2 ? parsedV2 : null;
15567
+ if (v2Serialized && !v2) {
15023
15568
  sessionStorage.removeItem(this.freshAiReturnProvisionalStorageKey);
15024
15569
  }
15025
- return provisional;
15570
+ if (v2)
15571
+ return v2;
15572
+ const legacySerialized = sessionStorage.getItem(this.legacyFreshAiReturnProvisionalStorageKey);
15573
+ const parsedLegacy = parseFreshAiReturnProvisional(legacySerialized ? JSON.parse(legacySerialized) : null);
15574
+ const legacy = parsedLegacy?.v === 1 ? parsedLegacy : null;
15575
+ if (legacySerialized && !legacy) {
15576
+ sessionStorage.removeItem(this.legacyFreshAiReturnProvisionalStorageKey);
15577
+ }
15578
+ return legacy;
15026
15579
  }
15027
15580
  catch {
15028
15581
  return null;
@@ -15032,9 +15585,54 @@ export default class AiLiveAssist {
15032
15585
  if (!this.freshAiReturnProvisionalStorageKey)
15033
15586
  return false;
15034
15587
  try {
15035
- sessionStorage.setItem(this.freshAiReturnProvisionalStorageKey, JSON.stringify(provisional));
15588
+ const serialized = provisional.v === 1
15589
+ ? {
15590
+ v: 1,
15591
+ linkId: provisional.linkId,
15592
+ conversationDisplayId: provisional.conversationDisplayId,
15593
+ mode: provisional.mode,
15594
+ phase: provisional.phase,
15595
+ launchIdempotencyKey: provisional.launchIdempotencyKey,
15596
+ sessionId: provisional.sessionId,
15597
+ expiresAt: provisional.expiresAt,
15598
+ }
15599
+ : provisional;
15600
+ const storageKey = provisional.v === 2
15601
+ ? this.freshAiReturnProvisionalStorageKey
15602
+ : this.legacyFreshAiReturnProvisionalStorageKey;
15603
+ sessionStorage.setItem(storageKey, JSON.stringify(serialized));
15604
+ const normalized = provisional.v === 1
15605
+ ? {
15606
+ ...provisional,
15607
+ protocolVersion: null,
15608
+ experienceMode: null,
15609
+ scopes: null,
15610
+ }
15611
+ : provisional;
15612
+ const directReadback = parseFreshAiReturnProvisional(JSON.parse(sessionStorage.getItem(storageKey) ?? "null"));
15613
+ if (canonicalJson(directReadback) !== canonicalJson(normalized)) {
15614
+ return false;
15615
+ }
15616
+ if (provisional.v === 2) {
15617
+ const legacySerialized = sessionStorage.getItem(this.legacyFreshAiReturnProvisionalStorageKey);
15618
+ const legacy = parseFreshAiReturnProvisional(legacySerialized ? JSON.parse(legacySerialized) : null);
15619
+ const sameReturnLineage = legacy?.v === 1 &&
15620
+ legacy.linkId === provisional.linkId &&
15621
+ legacy.conversationDisplayId === provisional.conversationDisplayId &&
15622
+ legacy.mode === provisional.mode &&
15623
+ legacy.expiresAt === provisional.expiresAt;
15624
+ if (sameReturnLineage &&
15625
+ (legacy.phase === "released" ||
15626
+ provisional.phase === "released" ||
15627
+ (legacy.phase === provisional.phase &&
15628
+ legacy.launchIdempotencyKey ===
15629
+ provisional.launchIdempotencyKey &&
15630
+ legacy.sessionId === provisional.sessionId))) {
15631
+ sessionStorage.removeItem(this.legacyFreshAiReturnProvisionalStorageKey);
15632
+ }
15633
+ }
15036
15634
  return (canonicalJson(this.readFreshAiReturnProvisional()) ===
15037
- canonicalJson(provisional));
15635
+ canonicalJson(normalized));
15038
15636
  }
15039
15637
  catch {
15040
15638
  return false;
@@ -15044,19 +15642,21 @@ export default class AiLiveAssist {
15044
15642
  if (!this.freshAiReturnProvisionalStorageKey)
15045
15643
  return;
15046
15644
  try {
15047
- const serialized = sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey);
15645
+ const storageKey = expected?.v === 1
15646
+ ? this.legacyFreshAiReturnProvisionalStorageKey
15647
+ : this.freshAiReturnProvisionalStorageKey;
15648
+ const serialized = sessionStorage.getItem(storageKey);
15048
15649
  if (!serialized)
15049
15650
  return;
15050
15651
  if (expected) {
15051
15652
  const stored = parseFreshAiReturnProvisional(JSON.parse(serialized));
15052
15653
  if (canonicalJson(stored) !== canonicalJson(expected))
15053
15654
  return;
15054
- if (sessionStorage.getItem(this.freshAiReturnProvisionalStorageKey) !==
15055
- serialized) {
15655
+ if (sessionStorage.getItem(storageKey) !== serialized) {
15056
15656
  return;
15057
15657
  }
15058
15658
  }
15059
- sessionStorage.removeItem(this.freshAiReturnProvisionalStorageKey);
15659
+ sessionStorage.removeItem(storageKey);
15060
15660
  }
15061
15661
  catch {
15062
15662
  /* noop */
@@ -15647,10 +16247,7 @@ export default class AiLiveAssist {
15647
16247
  !this.view ||
15648
16248
  (record &&
15649
16249
  (!record.tabId ||
15650
- record.protocolVersion !==
15651
- resolveAiGuideProtocolVersion(this.bootstrap) ||
15652
- record.experienceMode !==
15653
- (this.bootstrap.experienceMode ?? "standard"))) ||
16250
+ !resumeProtocolIsSupported(record, this.bootstrap))) ||
15654
16251
  (pendingLaunch &&
15655
16252
  (this.visitorActiveTakeoverOnly ||
15656
16253
  ((this.bootstrap.experienceMode ?? "standard") === "voice_presence" &&
@@ -15686,10 +16283,11 @@ export default class AiLiveAssist {
15686
16283
  this.lock.release();
15687
16284
  };
15688
16285
  const preparedMatches = record
15689
- ? prepared?.resume?.sessionId === record.sessionId
16286
+ ? Boolean(prepared?.resume &&
16287
+ this.sameResumeAuthorityIdentity(prepared.resume, record))
15690
16288
  : Boolean(pendingLaunch &&
15691
- prepared?.pendingLaunch?.idempotencyKey ===
15692
- pendingLaunch.idempotencyKey);
16289
+ prepared?.pendingLaunch &&
16290
+ this.pendingLaunchMatches(prepared.pendingLaunch, pendingLaunch));
15693
16291
  if (!payload ||
15694
16292
  (!record && !pendingLaunch) ||
15695
16293
  !this.bootstrap ||
@@ -15702,10 +16300,7 @@ export default class AiLiveAssist {
15702
16300
  !preparedMatches ||
15703
16301
  (record &&
15704
16302
  (!record.tabId ||
15705
- record.protocolVersion !==
15706
- resolveAiGuideProtocolVersion(this.bootstrap) ||
15707
- record.experienceMode !==
15708
- (this.bootstrap.experienceMode ?? "standard"))) ||
16303
+ !resumeProtocolIsSupported(record, this.bootstrap))) ||
15709
16304
  (pendingLaunch && this.visitorActiveTakeoverOnly)) {
15710
16305
  abandonUnusableCommit();
15711
16306
  return;
@@ -15717,7 +16312,7 @@ export default class AiLiveAssist {
15717
16312
  return;
15718
16313
  }
15719
16314
  if (this.lastLaunch &&
15720
- this.lastLaunch.idempotencyKey !== stagedPending.idempotencyKey) {
16315
+ !this.pendingLaunchMatches(this.lastLaunch, stagedPending)) {
15721
16316
  this.clearStagedPendingLaunch(stagedPending, authority);
15722
16317
  }
15723
16318
  else {
@@ -15729,6 +16324,12 @@ export default class AiLiveAssist {
15729
16324
  return;
15730
16325
  }
15731
16326
  const storedResume = record ? this.readResume() : null;
16327
+ if (record &&
16328
+ storedResume?.sessionId === record.sessionId &&
16329
+ !this.sameResumeAuthorityIdentity(record, storedResume)) {
16330
+ abandonUnusableCommit();
16331
+ return;
16332
+ }
15732
16333
  const effectivePayload = record &&
15733
16334
  storedResume?.sessionId === record.sessionId &&
15734
16335
  storedResume.resumeCredential !== record.resumeCredential
@@ -15769,11 +16370,11 @@ export default class AiLiveAssist {
15769
16370
  completeFocusedTabRelinquish(value) {
15770
16371
  const payload = parseFocusedHandoffPayload(value);
15771
16372
  const sameResume = Boolean(payload?.resume &&
15772
- this.focusedHandoffInFlight?.resume?.sessionId ===
15773
- payload.resume.sessionId);
16373
+ this.focusedHandoffInFlight?.resume &&
16374
+ this.sameResumeAuthorityIdentity(this.focusedHandoffInFlight.resume, payload.resume));
15774
16375
  const samePendingLaunch = Boolean(payload?.pendingLaunch &&
15775
- this.focusedHandoffInFlight?.pendingLaunch?.idempotencyKey ===
15776
- payload.pendingLaunch.idempotencyKey);
16376
+ this.focusedHandoffInFlight?.pendingLaunch &&
16377
+ this.pendingLaunchMatches(this.focusedHandoffInFlight.pendingLaunch, payload.pendingLaunch));
15777
16378
  if (!payload || (!sameResume && !samePendingLaunch)) {
15778
16379
  return;
15779
16380
  }
@@ -15804,8 +16405,8 @@ export default class AiLiveAssist {
15804
16405
  this.clearStagedPendingLaunch(pendingLaunch, authority);
15805
16406
  }
15806
16407
  if (pendingLaunch &&
15807
- this.focusedHandoffInFlight?.pendingLaunch?.idempotencyKey ===
15808
- pendingLaunch.idempotencyKey) {
16408
+ this.focusedHandoffInFlight?.pendingLaunch &&
16409
+ this.pendingLaunchMatches(this.focusedHandoffInFlight.pendingLaunch, pendingLaunch)) {
15809
16410
  this.focusedHandoffInFlight = null;
15810
16411
  if (this.terminalRequestPending ||
15811
16412
  this.ended ||
@@ -15827,7 +16428,7 @@ export default class AiLiveAssist {
15827
16428
  queueMicrotask(() => {
15828
16429
  const retry = this.lastLaunch;
15829
16430
  if (!retry ||
15830
- retry.idempotencyKey !== pendingLaunch.idempotencyKey ||
16431
+ !this.pendingLaunchMatches(retry, pendingLaunch) ||
15831
16432
  this.terminalRequestPending ||
15832
16433
  this.ended ||
15833
16434
  this.destroyed ||
@@ -15836,7 +16437,9 @@ export default class AiLiveAssist {
15836
16437
  return;
15837
16438
  }
15838
16439
  this.lastLaunch = null;
15839
- void this.launch(retry.mode, retry.scopes, retry.idempotencyKey);
16440
+ void this.launch(retry.mode, retry.scopes, retry.idempotencyKey, {
16441
+ pendingLaunch: retry,
16442
+ });
15840
16443
  });
15841
16444
  return;
15842
16445
  }
@@ -15902,7 +16505,7 @@ export default class AiLiveAssist {
15902
16505
  this.view &&
15903
16506
  !this.visitorActiveTakeoverOnly) {
15904
16507
  this.lastLaunch = null;
15905
- await this.launch(deferredLaunch.mode, deferredLaunch.scopes, deferredLaunch.idempotencyKey);
16508
+ await this.launch(deferredLaunch.mode, deferredLaunch.scopes, deferredLaunch.idempotencyKey, { pendingLaunch: deferredLaunch });
15906
16509
  return;
15907
16510
  }
15908
16511
  this.lock.release();