@oxvo/ai-live-assist 7.5.0 → 7.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/AiLiveAssist.d.ts +2 -1
- package/cjs/AiLiveAssist.js +56 -8
- package/cjs/context.d.ts +2 -0
- package/cjs/context.js +125 -59
- package/cjs/control.d.ts +9 -1
- package/cjs/control.js +81 -3
- package/cjs/liveControlChannel.d.ts +1 -1
- package/cjs/media.d.ts +1 -0
- package/cjs/media.js +3 -0
- package/cjs/types.d.ts +2 -0
- package/cjs/ui.js +2 -0
- package/cjs/version.d.ts +1 -1
- package/cjs/version.js +1 -1
- package/lib/AiLiveAssist.d.ts +2 -1
- package/lib/AiLiveAssist.js +56 -8
- package/lib/context.d.ts +2 -0
- package/lib/context.js +125 -59
- package/lib/control.d.ts +9 -1
- package/lib/control.js +81 -3
- package/lib/liveControlChannel.d.ts +1 -1
- package/lib/media.d.ts +1 -0
- package/lib/media.js +3 -0
- package/lib/types.d.ts +2 -0
- package/lib/ui.js +2 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/control.js
CHANGED
|
@@ -3,6 +3,7 @@ const PROTOCOL_VIOLATION_CLOSE_CODE = 4008;
|
|
|
3
3
|
const SERVER_SEQUENCE_GAP_CLOSE_CODE = 4009;
|
|
4
4
|
const HANDLER_FAILURE_CLOSE_CODE = 4010;
|
|
5
5
|
const HANDLER_TIMEOUT_MS = 15000;
|
|
6
|
+
const TRANSPORT_RECOVERY_TIMEOUT_MS = 5000;
|
|
6
7
|
const MAX_PENDING_HANDLER_MESSAGES = 64;
|
|
7
8
|
const MAX_PENDING_STATEFUL_OPERATIONS = 64;
|
|
8
9
|
const MAX_SETTLED_OPERATION_HISTORY = 100;
|
|
@@ -35,13 +36,14 @@ const STATEFUL_CONTROL_TYPES = new Set([
|
|
|
35
36
|
]);
|
|
36
37
|
const outcomeKey = (messageId, sequence) => `${messageId}\u0000${sequence}`;
|
|
37
38
|
export class ControlChannel {
|
|
38
|
-
constructor(result, tabId, clientVersion, onDisconnect, initialClientSequence = 0, onClientSequence = () => undefined, onUnsafeDisconnect = () => undefined) {
|
|
39
|
+
constructor(result, tabId, clientVersion, onDisconnect, initialClientSequence = 0, onClientSequence = () => undefined, onUnsafeDisconnect = () => undefined, transportRecovery) {
|
|
39
40
|
this.result = result;
|
|
40
41
|
this.tabId = tabId;
|
|
41
42
|
this.clientVersion = clientVersion;
|
|
42
43
|
this.onDisconnect = onDisconnect;
|
|
43
44
|
this.onClientSequence = onClientSequence;
|
|
44
45
|
this.onUnsafeDisconnect = onUnsafeDisconnect;
|
|
46
|
+
this.transportRecovery = transportRecovery;
|
|
45
47
|
this.socket = null;
|
|
46
48
|
this.serverSequence = 0;
|
|
47
49
|
this.serverBaselineEstablished = false;
|
|
@@ -64,6 +66,8 @@ export class ControlChannel {
|
|
|
64
66
|
this.pendingConnection = null;
|
|
65
67
|
this.helloOperation = null;
|
|
66
68
|
this.fencedAuthorityConfirmed = false;
|
|
69
|
+
this.transportRecoveryAttempted = false;
|
|
70
|
+
this.recoveringTransport = false;
|
|
67
71
|
this.clientSequence =
|
|
68
72
|
Number.isSafeInteger(initialClientSequence) && initialClientSequence >= 0
|
|
69
73
|
? initialClientSequence
|
|
@@ -76,7 +80,7 @@ export class ControlChannel {
|
|
|
76
80
|
return this.clientSequence;
|
|
77
81
|
}
|
|
78
82
|
get isOpen() {
|
|
79
|
-
return this.socket?.readyState === WebSocket.OPEN;
|
|
83
|
+
return (!this.recoveringTransport && this.socket?.readyState === WebSocket.OPEN);
|
|
80
84
|
}
|
|
81
85
|
get connectionHelloOperation() {
|
|
82
86
|
return this.helloOperation ? { ...this.helloOperation } : null;
|
|
@@ -177,10 +181,19 @@ export class ControlChannel {
|
|
|
177
181
|
this.rejectPendingConnection(socket, generation, new Error("The AI Guide control channel could not connect."));
|
|
178
182
|
}
|
|
179
183
|
}, { once: true });
|
|
180
|
-
socket.addEventListener("close", () => {
|
|
184
|
+
socket.addEventListener("close", (event) => {
|
|
181
185
|
if (!this.isCurrentSocket(socket, generation))
|
|
182
186
|
return;
|
|
183
187
|
const wasIntentionallyClosed = this.intentionallyClosed;
|
|
188
|
+
// A settled transport loss is not a new session. Keep its provider
|
|
189
|
+
// connection alive for one bounded retry, but never carry ambiguous
|
|
190
|
+
// browser writes, an active handler, or a protocol failure across it.
|
|
191
|
+
const canRecover = event.code === 1006 &&
|
|
192
|
+
this.fencedAuthorityConfirmed &&
|
|
193
|
+
!this.transportRecoveryAttempted &&
|
|
194
|
+
!this.sequenceGapDetected &&
|
|
195
|
+
this.pendingHandlerMessages === 0 &&
|
|
196
|
+
this.unsettledStatefulOperations.size === 0;
|
|
184
197
|
this.socket = null;
|
|
185
198
|
this.socketGeneration += 1;
|
|
186
199
|
this.stopHeartbeat();
|
|
@@ -194,12 +207,24 @@ export class ControlChannel {
|
|
|
194
207
|
this.pendingHandlerMessages = 0;
|
|
195
208
|
this.rejectPendingConnection(socket, generation, new Error("The AI Guide control channel closed before connecting."));
|
|
196
209
|
if (!wasIntentionallyClosed && !signal.aborted) {
|
|
210
|
+
try {
|
|
211
|
+
if (canRecover && this.transportRecovery?.canRecover()) {
|
|
212
|
+
void this.recoverTransport(signal);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
/* A failed eligibility check uses full recovery. */
|
|
218
|
+
}
|
|
197
219
|
this.notifyUnsafeDisconnect();
|
|
198
220
|
}
|
|
199
221
|
});
|
|
200
222
|
});
|
|
201
223
|
}
|
|
202
224
|
send(type, payload, stableMessageId) {
|
|
225
|
+
if (this.recoveringTransport && type !== "hello" && type !== "heartbeat") {
|
|
226
|
+
throw new Error("The AI Guide control channel is reconnecting.");
|
|
227
|
+
}
|
|
203
228
|
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
204
229
|
throw new Error("The AI Guide control channel is not connected.");
|
|
205
230
|
}
|
|
@@ -510,6 +535,59 @@ export class ControlChannel {
|
|
|
510
535
|
this.onDisconnect();
|
|
511
536
|
}
|
|
512
537
|
}
|
|
538
|
+
async recoverTransport(signal) {
|
|
539
|
+
this.transportRecoveryAttempted = true;
|
|
540
|
+
this.recoveringTransport = true;
|
|
541
|
+
const controller = new AbortController();
|
|
542
|
+
const abort = () => controller.abort();
|
|
543
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
544
|
+
let timeout;
|
|
545
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
546
|
+
timeout = setTimeout(() => {
|
|
547
|
+
controller.abort();
|
|
548
|
+
reject(new Error("The control reconnect timed out."));
|
|
549
|
+
}, TRANSPORT_RECOVERY_TIMEOUT_MS);
|
|
550
|
+
});
|
|
551
|
+
const fencingToken = this.fencingToken;
|
|
552
|
+
let recovered = false;
|
|
553
|
+
try {
|
|
554
|
+
await Promise.race([
|
|
555
|
+
deadline,
|
|
556
|
+
(async () => {
|
|
557
|
+
if (signal.aborted)
|
|
558
|
+
throw new Error("The control connection was retired.");
|
|
559
|
+
await this.connect(controller.signal, true);
|
|
560
|
+
const hello = await this.waitForHelloOutcome(2000);
|
|
561
|
+
if (hello.status !== "ack" || controller.signal.aborted) {
|
|
562
|
+
throw new Error("The control reconnect was not acknowledged.");
|
|
563
|
+
}
|
|
564
|
+
const authority = await this.proveFencedAuthority(2000);
|
|
565
|
+
if (authority.status !== "ack" ||
|
|
566
|
+
controller.signal.aborted ||
|
|
567
|
+
this.fencingToken !== fencingToken ||
|
|
568
|
+
!this.transportRecovery?.canRecover()) {
|
|
569
|
+
throw new Error("The control reconnect no longer owns this session.");
|
|
570
|
+
}
|
|
571
|
+
this.recoveringTransport = false;
|
|
572
|
+
await this.transportRecovery.onRecovered();
|
|
573
|
+
recovered = !controller.signal.aborted && this.hasFencedAuthority;
|
|
574
|
+
})(),
|
|
575
|
+
]);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// The existing full-resume path revokes local executable authority and
|
|
579
|
+
// handles expired credentials, changed ownership, and longer outages.
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
clearTimeout(timeout);
|
|
583
|
+
signal.removeEventListener("abort", abort);
|
|
584
|
+
this.recoveringTransport = false;
|
|
585
|
+
if (!recovered && !signal.aborted && !this.intentionallyClosed) {
|
|
586
|
+
this.retireSocket("control reconnect failed");
|
|
587
|
+
this.notifyUnsafeDisconnect();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
513
591
|
enqueueHandlers(message, socket, generation) {
|
|
514
592
|
const isCurrent = () => this.isMessageSourceCurrent(socket, generation);
|
|
515
593
|
if (!isCurrent())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LiveDuplexMediaOptions, type LiveDuplexMediaDependencies } from './liveDuplexMedia.js';
|
|
2
|
-
export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.
|
|
2
|
+
export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.6";
|
|
3
3
|
export declare const LIVE_CONTROL_CLIENT_CAPABILITIES: readonly ["live_duplex_v1", "live_local_wake_v1", "live_fragment_captions_v1"];
|
|
4
4
|
declare const phases: readonly ["voice_off", "local_listening", "wake_buffering", "starting", "active", "tentative_idle", "closing", "uncertain", "muted", "suspended", "budget_blocked", "ended"];
|
|
5
5
|
export type LiveControlState = {
|
package/lib/media.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export declare class RealtimeMedia {
|
|
|
36
36
|
connect(result: LaunchResult, mode: AssistMode, signal: AbortSignal, offerIdempotencyKey?: string, expectedSessionState?: "active_text" | "active_voice"): Promise<number>;
|
|
37
37
|
setMuted(muted: boolean): void;
|
|
38
38
|
get isMuted(): boolean;
|
|
39
|
+
get isConnected(): boolean;
|
|
39
40
|
setAssistantPlaybackAudible(audible: boolean): void;
|
|
40
41
|
setManualTurn(manual: boolean): void;
|
|
41
42
|
beginPushToTalk(): void;
|
package/lib/media.js
CHANGED
|
@@ -134,6 +134,9 @@ export class RealtimeMedia {
|
|
|
134
134
|
get isMuted() {
|
|
135
135
|
return this.muted;
|
|
136
136
|
}
|
|
137
|
+
get isConnected() {
|
|
138
|
+
return !this.closed && this.connection?.connectionState === "connected";
|
|
139
|
+
}
|
|
137
140
|
setAssistantPlaybackAudible(audible) {
|
|
138
141
|
if (this.closed)
|
|
139
142
|
return;
|
package/lib/types.d.ts
CHANGED
|
@@ -331,6 +331,7 @@ export type SanitizedTarget = {
|
|
|
331
331
|
inputType?: string;
|
|
332
332
|
autocomplete?: string;
|
|
333
333
|
semanticRegion?: string;
|
|
334
|
+
navigationMenu?: true;
|
|
334
335
|
destinationPath?: string;
|
|
335
336
|
sensitivity: Sensitivity;
|
|
336
337
|
protectedRegion?: boolean;
|
|
@@ -356,6 +357,7 @@ export type PageContextSnapshot = {
|
|
|
356
357
|
pageId: string;
|
|
357
358
|
revision: number;
|
|
358
359
|
url: string;
|
|
360
|
+
locationHasQuery?: boolean;
|
|
359
361
|
title: string;
|
|
360
362
|
locale: string;
|
|
361
363
|
viewport: {
|
package/lib/ui.js
CHANGED
|
@@ -6790,7 +6790,9 @@ export class WidgetView {
|
|
|
6790
6790
|
this.setActionStateRunning(status === "running");
|
|
6791
6791
|
if (this.actionStateRunning) {
|
|
6792
6792
|
this.clearAssistantReadyVisualTimer();
|
|
6793
|
+
const previousState = this.assistantState;
|
|
6793
6794
|
this.assistantState = "thinking";
|
|
6795
|
+
this.updateRemoteAudioEvidenceExpectation(previousState, "thinking");
|
|
6794
6796
|
const assistantCaption = this.captions.querySelector('[data-caption="assistant-partial"]');
|
|
6795
6797
|
if (!assistantCaption || assistantCaption.classList.contains("pending")) {
|
|
6796
6798
|
this.ensurePendingCaption("assistant", this.messages.thinking);
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "7.5.
|
|
1
|
+
export declare const VERSION = "7.5.6";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '7.5.
|
|
1
|
+
export const VERSION = '7.5.6';
|