@oxvo/ai-live-assist 7.3.4 → 7.4.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/cjs/AiLiveAssist.d.ts +588 -6
  2. package/cjs/AiLiveAssist.js +14892 -1240
  3. package/cjs/client.d.ts +71 -10
  4. package/cjs/client.js +316 -58
  5. package/cjs/context.d.ts +15 -5
  6. package/cjs/context.js +568 -73
  7. package/cjs/control.d.ts +72 -7
  8. package/cjs/control.js +594 -71
  9. package/cjs/dialogFocusScope.d.ts +37 -0
  10. package/cjs/dialogFocusScope.js +227 -0
  11. package/cjs/experienceState.js +1 -1
  12. package/cjs/index.d.ts +1 -1
  13. package/cjs/index.js +2 -2
  14. package/cjs/media.d.ts +14 -3
  15. package/cjs/media.js +128 -19
  16. package/cjs/messages.js +26 -3
  17. package/cjs/messengerLoader.d.ts +3 -0
  18. package/cjs/messengerLoader.js +156 -0
  19. package/cjs/pageAssistancePreference.d.ts +78 -0
  20. package/cjs/pageAssistancePreference.js +488 -0
  21. package/cjs/preview.d.ts +33 -0
  22. package/cjs/preview.js +567 -0
  23. package/cjs/tabLock.d.ts +54 -3
  24. package/cjs/tabLock.js +752 -72
  25. package/cjs/types.d.ts +152 -4
  26. package/cjs/ui.d.ts +200 -10
  27. package/cjs/ui.js +6946 -523
  28. package/cjs/version.d.ts +1 -1
  29. package/cjs/version.js +1 -1
  30. package/cjs/voicePresenceUi.d.ts +48 -9
  31. package/cjs/voicePresenceUi.js +863 -125
  32. package/lib/AiLiveAssist.d.ts +588 -6
  33. package/lib/AiLiveAssist.js +14897 -1248
  34. package/lib/client.d.ts +71 -10
  35. package/lib/client.js +313 -57
  36. package/lib/context.d.ts +15 -5
  37. package/lib/context.js +566 -72
  38. package/lib/control.d.ts +72 -7
  39. package/lib/control.js +594 -71
  40. package/lib/dialogFocusScope.d.ts +37 -0
  41. package/lib/dialogFocusScope.js +223 -0
  42. package/lib/experienceState.js +1 -1
  43. package/lib/index.d.ts +1 -1
  44. package/lib/index.js +2 -2
  45. package/lib/media.d.ts +14 -3
  46. package/lib/media.js +128 -19
  47. package/lib/messages.js +26 -3
  48. package/lib/messengerLoader.d.ts +3 -0
  49. package/lib/messengerLoader.js +151 -0
  50. package/lib/pageAssistancePreference.d.ts +78 -0
  51. package/lib/pageAssistancePreference.js +482 -0
  52. package/lib/preview.d.ts +33 -0
  53. package/lib/preview.js +561 -0
  54. package/lib/tabLock.d.ts +54 -3
  55. package/lib/tabLock.js +752 -72
  56. package/lib/types.d.ts +152 -4
  57. package/lib/ui.d.ts +200 -10
  58. package/lib/ui.js +6947 -524
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/lib/voicePresenceUi.d.ts +48 -9
  62. package/lib/voicePresenceUi.js +863 -125
  63. package/package.json +8 -3
  64. package/cjs/recordingPolicy.d.ts +0 -2
  65. package/cjs/recordingPolicy.js +0 -12
  66. package/lib/recordingPolicy.d.ts +0 -2
  67. package/lib/recordingPolicy.js +0 -8
package/lib/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AssistMode, BootstrapConfig, ConsentScope, LaunchResult, ExperienceMode } from './types.js';
1
+ import type { AssistMode, AiGuideCompanionState, BootstrapConfig, ConsentScope, HandoffRecoveryResult, LaunchResult, ExperienceMode, MessengerConversationState } from "./types.js";
2
2
  export declare class RuntimeClientError extends Error {
3
3
  readonly code: string;
4
4
  readonly retryable: boolean;
@@ -6,17 +6,22 @@ export declare class RuntimeClientError extends Error {
6
6
  readonly status: number;
7
7
  constructor(code: string, message: string, retryable: boolean, retryAfterMs: number | null, status: number);
8
8
  }
9
- type LaunchInput = {
9
+ export declare const MAX_ASSIST_SESSION_GRANT_LENGTH = 4096;
10
+ export declare const isValidReplaySessionId: (value: unknown) => value is string;
11
+ export declare const isValidAssistSessionGrant: (value: unknown) => value is string;
12
+ export type ReplayLaunchContext = {
13
+ sessionId: string;
14
+ tabId: string;
15
+ grant: string;
16
+ };
17
+ export type LaunchInput = {
10
18
  siteKey: string;
11
19
  configRevision: number;
12
20
  path: string;
13
21
  tabId: string;
14
22
  mode: AssistMode;
15
23
  anonymousId: string;
16
- replay?: {
17
- sessionId: string;
18
- tabId: string;
19
- };
24
+ replay: ReplayLaunchContext;
20
25
  disclosureChecksum: string;
21
26
  scopes: ConsentScope[];
22
27
  locale: string;
@@ -26,6 +31,55 @@ type LaunchInput = {
26
31
  experienceMode: ExperienceMode;
27
32
  fallbackIntent?: "standard_assist" | "human_handoff";
28
33
  lowPower: boolean;
34
+ idempotencyKey: string;
35
+ };
36
+ export type ModeUpgradeResumeInput = {
37
+ sessionId: string;
38
+ resumeCredential: string;
39
+ path: string;
40
+ protocolVersion: 1 | 2;
41
+ requestId: string;
42
+ idempotencyKey: string;
43
+ };
44
+ export type ModeUpgradeRecoveryResult = {
45
+ schemaVersion: 1;
46
+ status: "voice_committed";
47
+ resume: LaunchResult;
48
+ } | {
49
+ schemaVersion: 1;
50
+ status: "terminalized";
51
+ sessionId: string;
52
+ state: "ended" | "failed" | "revoked";
53
+ };
54
+ export type RuntimeSessionState = "created" | "authorizing" | "connecting" | "active_text" | "active_voice" | "paused" | "reconnecting" | "handoff_pending" | "human_controlled" | "ending" | "ended" | "failed" | "revoked";
55
+ export type PauseCommandIdentity = {
56
+ operationId: string;
57
+ clientSequence: number;
58
+ };
59
+ export type PauseCommandResult = {
60
+ schemaVersion: 1;
61
+ operationId: string;
62
+ clientSequence: number;
63
+ fencingToken: number;
64
+ action: "pause" | "resume";
65
+ status: "applied" | "replayed" | "superseded";
66
+ state: RuntimeSessionState;
67
+ };
68
+ export type CompanionSurfaceSwitchInput = {
69
+ linkId: string;
70
+ switchGrant: string;
71
+ visitorId: string;
72
+ expectedOwnerEpoch: number;
73
+ targetSurface: "ai" | "messenger";
74
+ lastAiMode: AssistMode;
75
+ currentAiSessionId?: string;
76
+ idempotencyKey: string;
77
+ };
78
+ export type CompanionSurfaceSwitchResult = {
79
+ schemaVersion: 1;
80
+ status: "switched" | "replayed" | "stale";
81
+ companion: AiGuideCompanionState;
82
+ messengerConversation: MessengerConversationState;
29
83
  };
30
84
  export declare const idempotencyKey: (prefix: string) => string;
31
85
  export declare class RuntimeClient {
@@ -35,19 +89,26 @@ export declare class RuntimeClient {
35
89
  bootstrap(siteKey: string, path: string, anonymousId: string, signal: AbortSignal): Promise<BootstrapConfig>;
36
90
  launch(input: LaunchInput, signal: AbortSignal): Promise<LaunchResult>;
37
91
  resume(sessionId: string, credential: string, path: string, protocolVersion: 1 | 2, signal: AbortSignal): Promise<LaunchResult>;
38
- offer(result: LaunchResult, offerSdp: string, signal: AbortSignal): Promise<{
92
+ resumePendingHandoff(sessionId: string, credential: string, path: string, protocolVersion: 1 | 2, messengerBindingChallengeId: string, signal: AbortSignal): Promise<HandoffRecoveryResult>;
93
+ resumeModeUpgrade(input: ModeUpgradeResumeInput, signal: AbortSignal): Promise<LaunchResult>;
94
+ reconcileModeUpgrade(input: ModeUpgradeResumeInput, signal?: AbortSignal, keepalive?: boolean): Promise<ModeUpgradeRecoveryResult>;
95
+ offer(result: LaunchResult, offerSdp: string, signal: AbortSignal, offerIdempotencyKey?: string): Promise<{
39
96
  answerSdp: string;
40
97
  fencingToken: number;
98
+ sessionState: "active_text" | "active_voice" | null;
41
99
  }>;
42
100
  end(result: LaunchResult, fencingToken: number, signal?: AbortSignal): Promise<void>;
43
- pauseOrResume(result: LaunchResult, fencingToken: number, operation: 'pause' | 'resume', signal?: AbortSignal): Promise<void>;
101
+ recoverAndEnd(sessionId: string, resumeCredential: string, protocolVersion: 1 | 2, signal?: AbortSignal): Promise<void>;
102
+ pauseOrResume(result: LaunchResult, fencingToken: number, operation: "pause" | "resume", identity: PauseCommandIdentity, signal?: AbortSignal): Promise<PauseCommandResult>;
103
+ switchCompanionSurface(input: CompanionSurfaceSwitchInput, signal?: AbortSignal): Promise<CompanionSurfaceSwitchResult>;
44
104
  feedback(result: LaunchResult, payload: {
45
105
  rating: number | null;
46
- outcome: 'resolved' | 'partially_resolved' | 'not_resolved' | 'handoff';
106
+ outcome: "resolved" | "partially_resolved" | "not_resolved" | "handoff";
47
107
  }, signal?: AbortSignal): Promise<void>;
48
108
  private postEmpty;
109
+ private parseCompanionState;
110
+ private parseMessengerConversation;
49
111
  private postJson;
50
112
  private parseJson;
51
113
  private throwResponse;
52
114
  }
53
- export {};
package/lib/client.js CHANGED
@@ -5,40 +5,90 @@ export class RuntimeClientError extends Error {
5
5
  this.retryable = retryable;
6
6
  this.retryAfterMs = retryAfterMs;
7
7
  this.status = status;
8
- this.name = 'RuntimeClientError';
8
+ this.name = "RuntimeClientError";
9
9
  }
10
10
  }
11
+ const MAX_SIGNED_INT64 = "9223372036854775807";
12
+ const CONTROL_OPERATION_ID_PATTERN = /^[A-Za-z0-9_.:-]{8,128}$/;
13
+ export const MAX_ASSIST_SESSION_GRANT_LENGTH = 4096;
14
+ export const isValidReplaySessionId = (value) => {
15
+ if (typeof value !== "string" || !/^[1-9]\d{0,18}$/.test(value)) {
16
+ return false;
17
+ }
18
+ return value.length < MAX_SIGNED_INT64.length || value <= MAX_SIGNED_INT64;
19
+ };
20
+ export const isValidAssistSessionGrant = (value) => typeof value === "string" &&
21
+ value.length > 0 &&
22
+ value.length <= MAX_ASSIST_SESSION_GRANT_LENGTH &&
23
+ value.trim() === value;
24
+ const RUNTIME_SESSION_STATES = new Set([
25
+ "created",
26
+ "authorizing",
27
+ "connecting",
28
+ "active_text",
29
+ "active_voice",
30
+ "paused",
31
+ "reconnecting",
32
+ "handoff_pending",
33
+ "human_controlled",
34
+ "ending",
35
+ "ended",
36
+ "failed",
37
+ "revoked",
38
+ ]);
11
39
  const requestId = () => `req_${crypto.randomUUID()}`;
12
40
  export const idempotencyKey = (prefix) => `${prefix}_${crypto.randomUUID()}`;
13
41
  export class RuntimeClient {
14
42
  constructor(runtimeUrl, clientVersion) {
15
43
  this.clientVersion = clientVersion;
16
44
  const url = new URL(runtimeUrl, window.location.href);
17
- if (!['http:', 'https:'].includes(url.protocol)) {
18
- throw new Error('AI Guide runtime URL must use HTTP or HTTPS.');
45
+ if (!["http:", "https:"].includes(url.protocol)) {
46
+ throw new Error("AI Guide runtime URL must use HTTP or HTTPS.");
19
47
  }
20
- this.baseUrl = url.origin + url.pathname.replace(/\/$/, '');
48
+ this.baseUrl = url.origin + url.pathname.replace(/\/$/, "");
21
49
  }
22
50
  async bootstrap(siteKey, path, anonymousId, signal) {
23
51
  const url = new URL(`${this.baseUrl}/ai-live-assist/v1/public/bootstrap`);
24
- url.searchParams.set('siteKey', siteKey);
25
- url.searchParams.set('path', path);
52
+ url.searchParams.set("siteKey", siteKey);
53
+ url.searchParams.set("path", path);
26
54
  const response = await fetch(url, {
27
- method: 'GET',
28
- mode: 'cors',
29
- credentials: 'omit',
30
- cache: 'no-store',
55
+ method: "GET",
56
+ mode: "cors",
57
+ credentials: "omit",
58
+ cache: "no-store",
31
59
  headers: {
32
- Accept: 'application/json',
33
- 'X-Oxvo-Ai-Client-Version': this.clientVersion,
34
- 'X-Oxvo-Ai-Visitor-Id': anonymousId,
35
- 'X-Request-Id': requestId(),
60
+ Accept: "application/json",
61
+ "X-Oxvo-Ai-Client-Version": this.clientVersion,
62
+ "X-Oxvo-Ai-Visitor-Id": anonymousId,
63
+ "X-Request-Id": requestId(),
36
64
  },
37
65
  signal,
38
66
  });
39
67
  return this.parseJson(response);
40
68
  }
41
69
  async launch(input, signal) {
70
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(input.idempotencyKey)) {
71
+ throw new Error("Invalid AI Guide launch idempotency key.");
72
+ }
73
+ const replaySessionId = input.replay?.sessionId;
74
+ const replayTabId = input.replay?.tabId;
75
+ const replayGrant = input.replay?.grant;
76
+ if (!isValidReplaySessionId(replaySessionId)) {
77
+ throw new Error("Invalid AI Guide replay session ID.");
78
+ }
79
+ if (typeof replayTabId !== "string" ||
80
+ replayTabId !== input.tabId ||
81
+ !/^[A-Za-z0-9_.:-]{1,128}$/.test(replayTabId)) {
82
+ throw new Error("Invalid AI Guide replay tab ID.");
83
+ }
84
+ if (!isValidAssistSessionGrant(replayGrant)) {
85
+ throw new Error("Invalid AI Guide replay grant.");
86
+ }
87
+ const replay = {
88
+ sessionId: replaySessionId,
89
+ tabId: replayTabId,
90
+ grant: replayGrant,
91
+ };
42
92
  const shared = {
43
93
  clientVersion: this.clientVersion,
44
94
  siteKey: input.siteKey,
@@ -47,7 +97,7 @@ export class RuntimeClient {
47
97
  tabId: input.tabId,
48
98
  mode: input.mode,
49
99
  visitor: { anonymousId: input.anonymousId },
50
- ...(input.replay ? { replay: input.replay } : {}),
100
+ replay,
51
101
  consent: {
52
102
  disclosureChecksum: input.disclosureChecksum,
53
103
  scopes: input.scopes,
@@ -65,13 +115,12 @@ export class RuntimeClient {
65
115
  ...shared,
66
116
  capabilitySummary: {
67
117
  microphone: input.microphone,
68
- webrtc: typeof RTCPeerConnection !== 'undefined',
69
- audioPlayback: typeof HTMLAudioElement !== 'undefined',
70
- webAudio: typeof AudioContext !== 'undefined' ||
71
- typeof window
72
- .webkitAudioContext !== 'undefined',
73
- svgOverlay: typeof SVGElement !== 'undefined',
74
- visualViewport: typeof window.visualViewport !== 'undefined',
118
+ webrtc: typeof RTCPeerConnection !== "undefined",
119
+ audioPlayback: typeof HTMLAudioElement !== "undefined",
120
+ webAudio: typeof AudioContext !== "undefined" ||
121
+ typeof window.webkitAudioContext !== "undefined",
122
+ svgOverlay: typeof SVGElement !== "undefined",
123
+ visualViewport: typeof window.visualViewport !== "undefined",
75
124
  reducedMotion: input.reducedMotion,
76
125
  lowPower: input.lowPower,
77
126
  },
@@ -82,83 +131,290 @@ export class RuntimeClient {
82
131
  ...shared,
83
132
  capabilitySummary: {
84
133
  microphone: input.microphone,
85
- webrtc: typeof RTCPeerConnection !== 'undefined',
134
+ webrtc: typeof RTCPeerConnection !== "undefined",
86
135
  reducedMotion: input.reducedMotion,
87
136
  },
88
137
  };
89
- return this.postJson('/ai-live-assist/v1/public/launch', body, {
90
- 'Idempotency-Key': idempotencyKey('launch'),
138
+ return this.postJson("/ai-live-assist/v1/public/launch", body, {
139
+ "Idempotency-Key": input.idempotencyKey,
91
140
  }, signal);
92
141
  }
93
142
  async resume(sessionId, credential, path, protocolVersion, signal) {
94
- return this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/resume`, {
143
+ const request = this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/resume`, {
95
144
  schemaVersion: protocolVersion,
96
145
  protocolVersion,
97
146
  path,
98
- reason: 'navigation',
147
+ reason: "navigation",
99
148
  }, { Authorization: `Bearer ${credential}` }, signal);
149
+ try {
150
+ return await request;
151
+ }
152
+ catch (error) {
153
+ if (error instanceof TypeError) {
154
+ throw new RuntimeClientError("RUNTIME_UNAVAILABLE", "AI Guide is temporarily unavailable.", true, null, 0);
155
+ }
156
+ throw error;
157
+ }
158
+ }
159
+ async resumePendingHandoff(sessionId, credential, path, protocolVersion, messengerBindingChallengeId, signal) {
160
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(messengerBindingChallengeId)) {
161
+ throw new Error("Invalid Messenger handoff recovery identity.");
162
+ }
163
+ try {
164
+ return await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/resume`, {
165
+ schemaVersion: protocolVersion,
166
+ protocolVersion,
167
+ path,
168
+ reason: "handoff_recovery",
169
+ messengerBindingChallengeId,
170
+ }, { Authorization: `Bearer ${credential}` }, signal);
171
+ }
172
+ catch (error) {
173
+ if (error instanceof TypeError) {
174
+ throw new RuntimeClientError("RUNTIME_UNAVAILABLE", "AI Guide is temporarily unavailable.", true, null, 0);
175
+ }
176
+ throw error;
177
+ }
178
+ }
179
+ async resumeModeUpgrade(input, signal) {
180
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(input.requestId) ||
181
+ !/^[A-Za-z0-9_.:-]{8,128}$/.test(input.idempotencyKey)) {
182
+ throw new Error("Invalid AI Guide mode-upgrade identity.");
183
+ }
184
+ return this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(input.sessionId)}/resume`, {
185
+ schemaVersion: input.protocolVersion,
186
+ protocolVersion: input.protocolVersion,
187
+ path: input.path,
188
+ reason: "mode_upgrade",
189
+ modeUpgradeRequestId: input.requestId,
190
+ }, {
191
+ Authorization: `Bearer ${input.resumeCredential}`,
192
+ "Idempotency-Key": input.idempotencyKey,
193
+ }, signal);
194
+ }
195
+ async reconcileModeUpgrade(input, signal, keepalive = false) {
196
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(input.requestId) ||
197
+ !/^[A-Za-z0-9_.:-]{8,128}$/.test(input.idempotencyKey)) {
198
+ throw new Error("Invalid AI Guide mode-upgrade recovery identity.");
199
+ }
200
+ return this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(input.sessionId)}/mode-upgrade/reconcile`, {
201
+ schemaVersion: input.protocolVersion,
202
+ protocolVersion: input.protocolVersion,
203
+ path: input.path,
204
+ modeUpgradeRequestId: input.requestId,
205
+ }, {
206
+ Authorization: `Bearer ${input.resumeCredential}`,
207
+ "Idempotency-Key": input.idempotencyKey,
208
+ }, signal, keepalive);
100
209
  }
101
- async offer(result, offerSdp, signal) {
210
+ async offer(result, offerSdp, signal, offerIdempotencyKey = idempotencyKey("offer")) {
211
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(offerIdempotencyKey)) {
212
+ throw new Error("Invalid AI Guide realtime-offer idempotency key.");
213
+ }
102
214
  const response = await fetch(result.realtimeOfferUrl, {
103
- method: 'POST',
104
- mode: 'cors',
105
- credentials: 'omit',
106
- cache: 'no-store',
215
+ method: "POST",
216
+ mode: "cors",
217
+ credentials: "omit",
218
+ cache: "no-store",
107
219
  headers: {
108
- Accept: 'application/sdp',
220
+ Accept: "application/sdp",
109
221
  Authorization: `Bearer ${result.launchCredential}`,
110
- 'Content-Type': 'application/sdp',
111
- 'Idempotency-Key': idempotencyKey('offer'),
112
- 'X-Oxvo-Ai-Client-Version': this.clientVersion,
113
- 'X-Request-Id': requestId(),
222
+ "Content-Type": "application/sdp",
223
+ "Idempotency-Key": offerIdempotencyKey,
224
+ "X-Oxvo-Ai-Client-Version": this.clientVersion,
225
+ "X-Request-Id": requestId(),
114
226
  },
115
227
  body: offerSdp,
116
228
  signal,
117
229
  });
118
230
  if (!response.ok)
119
231
  await this.throwResponse(response);
120
- const fencingToken = Number(response.headers.get('X-Oxvo-Fencing-Token'));
232
+ const fencingToken = Number(response.headers.get("X-Oxvo-Fencing-Token"));
121
233
  if (!Number.isSafeInteger(fencingToken) || fencingToken < 1) {
122
- throw new RuntimeClientError('PROTOCOL_INVALID', 'The runtime returned an invalid control lease.', false, null, 502);
234
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid control lease.", false, null, 502);
123
235
  }
124
- return { answerSdp: await response.text(), fencingToken };
236
+ const sessionStateHeader = response.headers.get("X-Oxvo-Session-State");
237
+ const sessionState = sessionStateHeader === "active_text" ||
238
+ sessionStateHeader === "active_voice"
239
+ ? sessionStateHeader
240
+ : null;
241
+ return {
242
+ answerSdp: await response.text(),
243
+ fencingToken,
244
+ sessionState,
245
+ };
125
246
  }
126
247
  async end(result, fencingToken, signal) {
127
- await this.postEmpty(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/end`, result.controlCredential, fencingToken, result.protocolVersion, signal);
248
+ await this.postEmpty(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/end`, result.controlCredential, fencingToken, result.protocolVersion, signal, true);
249
+ }
250
+ async recoverAndEnd(sessionId, resumeCredential, protocolVersion, signal) {
251
+ await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(sessionId)}/end`, {
252
+ schemaVersion: protocolVersion,
253
+ protocolVersion,
254
+ fencingToken: 0,
255
+ terminalRecovery: true,
256
+ }, { Authorization: `Bearer ${resumeCredential}` }, signal, true, true);
128
257
  }
129
- async pauseOrResume(result, fencingToken, operation, signal) {
130
- await this.postEmpty(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/${operation}`, result.controlCredential, fencingToken, result.protocolVersion, signal);
258
+ async pauseOrResume(result, fencingToken, operation, identity, signal) {
259
+ if (!CONTROL_OPERATION_ID_PATTERN.test(identity.operationId) ||
260
+ !Number.isSafeInteger(identity.clientSequence) ||
261
+ identity.clientSequence < 1) {
262
+ throw new Error("Invalid AI Guide pause command identity.");
263
+ }
264
+ const response = await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/actions/${operation}`, {
265
+ schemaVersion: result.protocolVersion,
266
+ protocolVersion: result.protocolVersion,
267
+ fencingToken,
268
+ operationId: identity.operationId,
269
+ clientSequence: identity.clientSequence,
270
+ }, { Authorization: `Bearer ${result.controlCredential}` }, signal);
271
+ if (!response ||
272
+ typeof response !== "object" ||
273
+ response.schemaVersion !== 1 ||
274
+ response.operationId !== identity.operationId ||
275
+ response.clientSequence !== identity.clientSequence ||
276
+ response.fencingToken !== fencingToken ||
277
+ response.action !== operation ||
278
+ !["applied", "replayed", "superseded"].includes(response.status) ||
279
+ !RUNTIME_SESSION_STATES.has(response.state)) {
280
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid pause command result.", false, null, 502);
281
+ }
282
+ return response;
283
+ }
284
+ async switchCompanionSurface(input, signal) {
285
+ if (!CONTROL_OPERATION_ID_PATTERN.test(input.linkId) ||
286
+ !isValidAssistSessionGrant(input.switchGrant) ||
287
+ !CONTROL_OPERATION_ID_PATTERN.test(input.visitorId) ||
288
+ !Number.isSafeInteger(input.expectedOwnerEpoch) ||
289
+ input.expectedOwnerEpoch < 0 ||
290
+ !["ai", "messenger"].includes(input.targetSurface) ||
291
+ !["text", "voice"].includes(input.lastAiMode) ||
292
+ !CONTROL_OPERATION_ID_PATTERN.test(input.idempotencyKey) ||
293
+ (input.currentAiSessionId !== undefined &&
294
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.currentAiSessionId))) {
295
+ throw new Error("Invalid AI Guide companion switch request.");
296
+ }
297
+ const response = await this.postJson(`/ai-live-assist/v1/public/companion-links/${encodeURIComponent(input.linkId)}/surface`, {
298
+ schemaVersion: 1,
299
+ expectedOwnerEpoch: input.expectedOwnerEpoch,
300
+ targetSurface: input.targetSurface,
301
+ lastAiMode: input.lastAiMode,
302
+ ...(input.currentAiSessionId
303
+ ? { currentAiSessionId: input.currentAiSessionId }
304
+ : {}),
305
+ idempotencyKey: input.idempotencyKey,
306
+ }, {
307
+ Authorization: `Bearer ${input.switchGrant}`,
308
+ "X-Oxvo-Ai-Visitor-Id": input.visitorId,
309
+ }, signal);
310
+ const companion = this.parseCompanionState(response.companion, response.switchGrant);
311
+ const messengerConversation = this.parseMessengerConversation(response.messengerConversation);
312
+ if (response.schemaVersion !== 1 ||
313
+ (response.status !== "switched" &&
314
+ response.status !== "replayed" &&
315
+ response.status !== "stale") ||
316
+ !companion ||
317
+ !messengerConversation ||
318
+ companion.linkId !== input.linkId) {
319
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid companion switch result.", false, null, 502);
320
+ }
321
+ return {
322
+ schemaVersion: 1,
323
+ status: response.status,
324
+ companion,
325
+ messengerConversation,
326
+ };
131
327
  }
132
328
  async feedback(result, payload, signal) {
133
329
  await this.postJson(`/ai-live-assist/v1/public/sessions/${encodeURIComponent(result.sessionId)}/feedback`, payload, {
134
330
  Authorization: `Bearer ${result.feedbackCredential}`,
135
331
  }, signal);
136
332
  }
137
- async postEmpty(path, credential, fencingToken, protocolVersion, signal) {
333
+ async postEmpty(path, credential, fencingToken, protocolVersion, signal, keepalive = false) {
138
334
  await this.postJson(path, {
139
335
  schemaVersion: protocolVersion,
140
336
  protocolVersion,
141
337
  fencingToken,
142
- }, { Authorization: `Bearer ${credential}` }, signal);
338
+ }, { Authorization: `Bearer ${credential}` }, signal, keepalive);
339
+ }
340
+ parseCompanionState(value, switchGrant) {
341
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
342
+ return null;
343
+ }
344
+ const candidate = value;
345
+ const currentAiSessionId = candidate.currentAiSessionId;
346
+ const expiresAt = typeof candidate.expiresAt === "string"
347
+ ? Date.parse(candidate.expiresAt)
348
+ : Number.NaN;
349
+ if (candidate.schemaVersion !== 1 ||
350
+ typeof candidate.linkId !== "string" ||
351
+ !CONTROL_OPERATION_ID_PATTERN.test(candidate.linkId) ||
352
+ (candidate.activeSurface !== "ai" &&
353
+ candidate.activeSurface !== "messenger") ||
354
+ (candidate.lastAiMode !== "text" && candidate.lastAiMode !== "voice") ||
355
+ !Number.isSafeInteger(candidate.ownerEpoch) ||
356
+ candidate.ownerEpoch < 0 ||
357
+ (currentAiSessionId !== null &&
358
+ (typeof currentAiSessionId !== "string" ||
359
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(currentAiSessionId))) ||
360
+ !Number.isFinite(expiresAt) ||
361
+ expiresAt <= Date.now() ||
362
+ !isValidAssistSessionGrant(switchGrant)) {
363
+ return null;
364
+ }
365
+ return {
366
+ schemaVersion: 1,
367
+ linkId: candidate.linkId,
368
+ activeSurface: candidate.activeSurface,
369
+ lastAiMode: candidate.lastAiMode,
370
+ ownerEpoch: candidate.ownerEpoch,
371
+ currentAiSessionId: currentAiSessionId,
372
+ expiresAt: candidate.expiresAt,
373
+ switchGrant,
374
+ };
143
375
  }
144
- async postJson(path, body, extraHeaders, signal) {
376
+ parseMessengerConversation(value) {
377
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
378
+ return null;
379
+ }
380
+ const candidate = value;
381
+ if (candidate.state !== "connected" ||
382
+ typeof candidate.conversationDisplayId !== "string" ||
383
+ !/^[1-9]\d{0,18}$/.test(candidate.conversationDisplayId) ||
384
+ typeof candidate.inboxId !== "string" ||
385
+ !/^[1-9]\d{0,18}$/.test(candidate.inboxId)) {
386
+ return null;
387
+ }
388
+ return {
389
+ state: "connected",
390
+ conversationDisplayId: candidate.conversationDisplayId,
391
+ inboxId: candidate.inboxId,
392
+ };
393
+ }
394
+ async postJson(path, body, extraHeaders, signal, keepalive = false, requireNoContent = false) {
145
395
  const response = await fetch(`${this.baseUrl}${path}`, {
146
- method: 'POST',
147
- mode: 'cors',
148
- credentials: 'omit',
149
- cache: 'no-store',
396
+ method: "POST",
397
+ mode: "cors",
398
+ credentials: "omit",
399
+ cache: "no-store",
150
400
  headers: {
151
- Accept: 'application/json',
152
- 'Content-Type': 'application/json',
153
- 'X-Oxvo-Ai-Client-Version': this.clientVersion,
154
- 'X-Request-Id': requestId(),
401
+ Accept: "application/json",
402
+ "Content-Type": "application/json",
403
+ "X-Oxvo-Ai-Client-Version": this.clientVersion,
404
+ "X-Request-Id": requestId(),
155
405
  ...extraHeaders,
156
406
  },
157
407
  body: JSON.stringify(body),
408
+ ...(keepalive ? { keepalive: true } : {}),
158
409
  ...(signal ? { signal } : {}),
159
410
  });
160
411
  if (response.status === 204)
161
412
  return undefined;
413
+ if (requireNoContent) {
414
+ if (!response.ok)
415
+ await this.throwResponse(response);
416
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid terminal response.", false, null, 502);
417
+ }
162
418
  return this.parseJson(response);
163
419
  }
164
420
  async parseJson(response) {
@@ -168,7 +424,7 @@ export class RuntimeClient {
168
424
  return (await response.json());
169
425
  }
170
426
  catch {
171
- throw new RuntimeClientError('PROTOCOL_INVALID', 'The runtime returned an invalid response.', false, null, response.status);
427
+ throw new RuntimeClientError("PROTOCOL_INVALID", "The runtime returned an invalid response.", false, null, response.status);
172
428
  }
173
429
  }
174
430
  async throwResponse(response) {
@@ -180,8 +436,8 @@ export class RuntimeClient {
180
436
  catch {
181
437
  // Keep the public fallback generic; response bodies may come from a proxy.
182
438
  }
183
- throw new RuntimeClientError(typeof error.code === 'string' ? error.code : 'RUNTIME_UNAVAILABLE', typeof error.message === 'string'
439
+ throw new RuntimeClientError(typeof error.code === "string" ? error.code : "RUNTIME_UNAVAILABLE", typeof error.message === "string"
184
440
  ? error.message
185
- : 'AI Guide is temporarily unavailable.', error.retryable === true, typeof error.retryAfterMs === 'number' ? error.retryAfterMs : null, response.status);
441
+ : "AI Guide is temporarily unavailable.", error.retryable === true, typeof error.retryAfterMs === "number" ? error.retryAfterMs : null, response.status);
186
442
  }
187
443
  }
package/lib/context.d.ts CHANGED
@@ -1,17 +1,22 @@
1
- import type { ActionExecutionResult, ActionGrant, PageContextSnapshot, PresentationTarget, VisualContextCapture } from './types.js';
1
+ import type { ActionExecutionResult, ActionGrant, PageContextSnapshot, SanitizedTarget, PresentationTarget, VisualContextCapture } from './types.js';
2
+ export declare const actionTargetFingerprint: (target: SanitizedTarget, subtle?: Pick<SubtleCrypto, "digest"> | undefined) => Promise<string | null>;
2
3
  type SelectorRegionRule = {
3
4
  digest: string;
4
5
  selector: string;
5
6
  };
6
7
  type ContextCallbacks = {
7
8
  onSnapshot: (snapshot: PageContextSnapshot) => void | Promise<void>;
8
- onNavigation: (pageId: string, revision: number, url: string) => void | Promise<void>;
9
+ onActionMutationApplied?: (actionId: string, revision: number, url: string) => void;
10
+ onCausalNavigationObserved?: (pageId: string, revision: number, url: string, causalActionId: string) => void;
11
+ onNavigation: (pageId: string, revision: number, url: string, causalActionId?: string) => boolean | void | Promise<boolean | void>;
9
12
  onHighlight: (element: Element | null, status: 'focus' | 'success' | 'failure') => void;
10
13
  };
11
14
  export declare class PageContextCollector {
12
15
  private readonly callbacks;
13
16
  private readonly selectorRegionRules;
14
17
  private readonly policyVersion;
18
+ private readonly trustedGuideRoot;
19
+ private readonly statefulButtonActionProof;
15
20
  private pageId;
16
21
  private revision;
17
22
  private running;
@@ -28,19 +33,21 @@ export declare class PageContextCollector {
28
33
  private pendingNavigation;
29
34
  private pendingCapture;
30
35
  private deferredNavigation;
36
+ private navigationSettlementGeneration;
37
+ private navigationSettlement;
31
38
  private recentSelection;
32
39
  private latestSemanticSignature;
33
- constructor(callbacks: ContextCallbacks, selectorRegionRules?: SelectorRegionRule[], policyVersion?: number);
40
+ constructor(callbacks: ContextCallbacks, selectorRegionRules?: SelectorRegionRule[], policyVersion?: number, trustedGuideRoot?: Element | null, statefulButtonActionProof?: boolean);
34
41
  get currentRevision(): number;
35
42
  get currentPageId(): string;
36
43
  presentationTarget(targetId: string, revision: number): PresentationTarget | null;
37
44
  start(): void;
38
45
  stop(): void;
39
- captureAndPublish(): Promise<PageContextSnapshot>;
46
+ captureAndPublish(): Promise<PageContextSnapshot | null>;
40
47
  capture(): PageContextSnapshot;
41
48
  private captureSnapshot;
42
49
  captureVisualContext(): VisualContextCapture | null;
43
- execute(grant: ActionGrant): Promise<ActionExecutionResult>;
50
+ execute(grant: ActionGrant, authorityIsCurrent?: () => boolean): Promise<ActionExecutionResult>;
44
51
  completeActionExecution(actionId: string): Promise<string | null>;
45
52
  private click;
46
53
  private type;
@@ -59,6 +66,9 @@ export declare class PageContextCollector {
59
66
  private handleControlStateChange;
60
67
  private readSafeSelection;
61
68
  private handleNavigation;
69
+ private beginNavigationSettlement;
70
+ private observeCausalNavigationAfterMutation;
71
+ private reportCausalNavigationObservation;
62
72
  private installNavigationHooks;
63
73
  private restoreNavigationHooks;
64
74
  private reducedMotion;