@voxevolv/client 0.3.1 → 0.3.2
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/CHANGELOG.md +7 -0
- package/README.md +32 -3
- package/dist/browser/index.js +6 -6
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/index.js.sha384 +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/context-queue.d.ts +19 -0
- package/dist/context-queue.d.ts.map +1 -0
- package/dist/context-queue.js +112 -0
- package/dist/context-queue.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +145 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384-
|
|
1
|
+
sha384-0D7tdN1boqxnaeeziL5QCBMckXnVRKf0kS1e1c7P7aDc+MC7TcXTJTOkHOl7G3Ys
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ContextUpdate, ContextUpdatedPayload } from "./protocol.js";
|
|
2
|
+
/** Only replace updates can be coalesced or restored without duplicating data. */
|
|
3
|
+
export declare class ReplacementContextQueue {
|
|
4
|
+
private send;
|
|
5
|
+
private paused;
|
|
6
|
+
private generation;
|
|
7
|
+
private latest;
|
|
8
|
+
private pending;
|
|
9
|
+
private running;
|
|
10
|
+
private failure;
|
|
11
|
+
constructor(send: (update: ContextUpdate, timeoutMs: number) => Promise<ContextUpdatedPayload>);
|
|
12
|
+
submit(update: ContextUpdate, timeoutMs: number): Promise<ContextUpdatedPayload>;
|
|
13
|
+
pause(): void;
|
|
14
|
+
resume(restore?: boolean): Promise<void>;
|
|
15
|
+
clear(error: Error): void;
|
|
16
|
+
private start;
|
|
17
|
+
private drain;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=context-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-queue.d.ts","sourceRoot":"","sources":["../src/context-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAS1E,kFAAkF;AAClF,qBAAa,uBAAuB;IAQtB,OAAO,CAAC,IAAI;IAPxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,MAAM,CAAoC;IAClD,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,OAAO,CAAsB;IAErC,YAAoB,IAAI,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,EAAI;IAE1G,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAqB/E;IAED,KAAK,IAAI,IAAI,CAGZ;IAEK,MAAM,CAAC,OAAO,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB3C;IAED,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAKxB;IAED,OAAO,CAAC,KAAK;YASC,KAAK;CAsBpB"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Only replace updates can be coalesced or restored without duplicating data. */
|
|
2
|
+
export class ReplacementContextQueue {
|
|
3
|
+
send;
|
|
4
|
+
paused = false;
|
|
5
|
+
generation = 0;
|
|
6
|
+
latest = new Map();
|
|
7
|
+
pending = new Map();
|
|
8
|
+
running = new Map();
|
|
9
|
+
failure = null;
|
|
10
|
+
constructor(send) {
|
|
11
|
+
this.send = send;
|
|
12
|
+
}
|
|
13
|
+
submit(update, timeoutMs) {
|
|
14
|
+
const activeKeys = new Set([...this.pending.keys(), ...this.running.keys()]);
|
|
15
|
+
if (activeKeys.size >= 64 && !activeKeys.has(update.key)) {
|
|
16
|
+
return Promise.reject(new Error("Too many pending context keys"));
|
|
17
|
+
}
|
|
18
|
+
// Bound retained image/text memory even for applications using dynamic keys.
|
|
19
|
+
this.latest.delete(update.key);
|
|
20
|
+
this.latest.set(update.key, update);
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
let count = 0;
|
|
23
|
+
for (const [key, value] of [...this.latest].reverse()) {
|
|
24
|
+
bytes += "image" in value ? value.image.size : new TextEncoder().encode(value.text).byteLength;
|
|
25
|
+
count += 1;
|
|
26
|
+
if (bytes > 16 * 1024 * 1024 || count > 64)
|
|
27
|
+
this.latest.delete(key);
|
|
28
|
+
}
|
|
29
|
+
const response = new Promise((resolve, reject) => {
|
|
30
|
+
this.pending.get(update.key)?.reject(new Error("Context update superseded by a newer value"));
|
|
31
|
+
this.pending.set(update.key, { update, timeoutMs, resolve, reject });
|
|
32
|
+
});
|
|
33
|
+
this.start(update.key);
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
pause() {
|
|
37
|
+
this.generation += 1;
|
|
38
|
+
this.paused = true;
|
|
39
|
+
}
|
|
40
|
+
async resume(restore = false) {
|
|
41
|
+
const generation = this.generation;
|
|
42
|
+
// The SDK cancels tracked commands before restoring the new worker.
|
|
43
|
+
await Promise.all(this.running.values());
|
|
44
|
+
if (generation !== this.generation)
|
|
45
|
+
throw new Error("Context restoration interrupted");
|
|
46
|
+
this.failure = null;
|
|
47
|
+
if (restore) {
|
|
48
|
+
for (const [key, update] of this.latest) {
|
|
49
|
+
if (this.pending.has(key))
|
|
50
|
+
continue;
|
|
51
|
+
this.pending.set(key, {
|
|
52
|
+
update: { ...update, respond: false },
|
|
53
|
+
timeoutMs: 20_000,
|
|
54
|
+
resolve: () => { },
|
|
55
|
+
reject: () => { },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.paused = false;
|
|
60
|
+
for (const key of this.pending.keys())
|
|
61
|
+
this.start(key);
|
|
62
|
+
await Promise.all(this.running.values());
|
|
63
|
+
if (generation !== this.generation)
|
|
64
|
+
throw new Error("Context restoration interrupted");
|
|
65
|
+
if (this.failure)
|
|
66
|
+
throw this.failure;
|
|
67
|
+
}
|
|
68
|
+
clear(error) {
|
|
69
|
+
this.pause();
|
|
70
|
+
this.latest.clear();
|
|
71
|
+
for (const request of this.pending.values())
|
|
72
|
+
request.reject(error);
|
|
73
|
+
this.pending.clear();
|
|
74
|
+
}
|
|
75
|
+
start(key) {
|
|
76
|
+
if (this.paused || this.running.has(key))
|
|
77
|
+
return;
|
|
78
|
+
const task = this.drain(key).finally(() => {
|
|
79
|
+
this.running.delete(key);
|
|
80
|
+
if (this.pending.has(key))
|
|
81
|
+
this.start(key);
|
|
82
|
+
});
|
|
83
|
+
this.running.set(key, task);
|
|
84
|
+
}
|
|
85
|
+
async drain(key) {
|
|
86
|
+
while (!this.paused) {
|
|
87
|
+
const request = this.pending.get(key);
|
|
88
|
+
if (!request)
|
|
89
|
+
return;
|
|
90
|
+
this.pending.delete(key);
|
|
91
|
+
try {
|
|
92
|
+
request.resolve(await this.send(request.update, request.timeoutMs));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (this.paused && !this.pending.has(key) && this.latest.get(key) === request.update) {
|
|
96
|
+
this.pending.set(key, request);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
100
|
+
}
|
|
101
|
+
if (!this.paused) {
|
|
102
|
+
// Invalid/rejected values must not poison the next restoration.
|
|
103
|
+
if (this.latest.get(key) === request.update)
|
|
104
|
+
this.latest.delete(key);
|
|
105
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=context-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-queue.js","sourceRoot":"","sources":["../src/context-queue.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface SessionCredentials {
|
|
|
16
16
|
access_token: string;
|
|
17
17
|
access_token_expires_at: string;
|
|
18
18
|
}
|
|
19
|
-
export declare const SDK_VERSION = "0.3.
|
|
19
|
+
export declare const SDK_VERSION = "0.3.2";
|
|
20
20
|
type Listener = (event: RealtimeEvent) => void;
|
|
21
21
|
type StateListener = (state: ConnectionState) => void;
|
|
22
22
|
type GapListener = (gap: EventGap) => void;
|
|
@@ -46,6 +46,7 @@ export interface AutoRejoinOptions {
|
|
|
46
46
|
maxAttempts?: number;
|
|
47
47
|
baseDelayMs?: number;
|
|
48
48
|
maxDelayMs?: number;
|
|
49
|
+
timeoutMs?: number;
|
|
49
50
|
}
|
|
50
51
|
export interface RealtimeSessionOptions {
|
|
51
52
|
connect?: ConnectOptions;
|
|
@@ -129,6 +130,9 @@ export declare class RealtimeSession {
|
|
|
129
130
|
private reconnectStartedAt;
|
|
130
131
|
private pendingReconnect;
|
|
131
132
|
private agentReady;
|
|
133
|
+
private agentStreamId;
|
|
134
|
+
private contextGeneration;
|
|
135
|
+
private readonly replacementContext;
|
|
132
136
|
private readyResolve;
|
|
133
137
|
private replayPromise;
|
|
134
138
|
private pendingCommands;
|
|
@@ -184,6 +188,7 @@ export declare class RealtimeSession {
|
|
|
184
188
|
setMicrophoneEnabled(enabled: boolean): Promise<void>;
|
|
185
189
|
sendCommand<T extends Record<string, unknown>>(type: string, payload: T, timeoutMs?: number): Promise<CommandAckPayload>;
|
|
186
190
|
updateContext(update: ContextUpdate, timeoutMs?: number): Promise<ContextUpdatedPayload>;
|
|
191
|
+
private sendContextUpdate;
|
|
187
192
|
private uploadTextContext;
|
|
188
193
|
private uploadImageContext;
|
|
189
194
|
disconnect(): Promise<void>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,IAAI,EAIL,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAUL,KAAK,iBAAiB,EAEtB,KAAK,aAAa,EAElB,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ConnectionState as LiveKitConnectionState, DataPacket_Kind, DisconnectReason, Room, RoomEvent, RpcError, Track, } from "livekit-client";
|
|
2
2
|
import { createClientMessage, CommandError, EventCursor, isCommandAckPayload, isCommandErrorPayload, isContextRejectedPayload, isContextUpdatePayload, isContextUpdatedPayload, isRealtimeEvent, } from "./protocol.js";
|
|
3
|
+
import { ReplacementContextQueue } from "./context-queue.js";
|
|
3
4
|
export { CommandError, EventCursor, createClientMessage, isCommandAckPayload, isCommandErrorPayload, isContextRejectedPayload, isContextUpdatePayload, isContextUpdatedPayload, isRealtimeEvent, } from "./protocol.js";
|
|
4
5
|
export { decodeAgentSnapshotPayload, decodeClientHelloPayload, decodeClientMessage, decodeCommandAckPayload, decodeCommandErrorPayload, decodeContextRejectedPayload, decodeContextUpdatePayload, decodeContextUpdatedPayload, decodeRealtimeEvent, } from "./generated/protocol-codecs.js";
|
|
5
|
-
export const SDK_VERSION = "0.3.
|
|
6
|
+
export const SDK_VERSION = "0.3.2";
|
|
7
|
+
const CLIENT_HELLO_TIMEOUT_MS = 30_000;
|
|
6
8
|
export class RealtimeSessionHttpError extends Error {
|
|
7
9
|
status;
|
|
8
10
|
code;
|
|
@@ -47,6 +49,9 @@ export class RealtimeSession {
|
|
|
47
49
|
reconnectStartedAt = null;
|
|
48
50
|
pendingReconnect = null;
|
|
49
51
|
agentReady = false;
|
|
52
|
+
agentStreamId = null;
|
|
53
|
+
contextGeneration = 0;
|
|
54
|
+
replacementContext = new ReplacementContextQueue((update, timeoutMs) => this.sendContextUpdate(update, timeoutMs));
|
|
50
55
|
readyResolve = null;
|
|
51
56
|
replayPromise = null;
|
|
52
57
|
pendingCommands = new Map();
|
|
@@ -290,6 +295,7 @@ export class RealtimeSession {
|
|
|
290
295
|
if (blockedStates.includes(this.stateValue)) {
|
|
291
296
|
throw new Error(`Cannot connect while session is ${this.stateValue}`);
|
|
292
297
|
}
|
|
298
|
+
const generation = this.contextGeneration;
|
|
293
299
|
const resolved = { ...this.connectDefaults, ...options };
|
|
294
300
|
const audioTrack = resolved.audioTrack ?? null;
|
|
295
301
|
const microphone = resolved.microphone ?? audioTrack === null;
|
|
@@ -303,6 +309,8 @@ export class RealtimeSession {
|
|
|
303
309
|
this.applicationAudioTrack = audioTrack;
|
|
304
310
|
if (!recovering) {
|
|
305
311
|
this.sessionEnded = false;
|
|
312
|
+
this.agentStreamId = null;
|
|
313
|
+
this.reconnectStartedAt = null;
|
|
306
314
|
this.pendingEndEvent = null;
|
|
307
315
|
this.endNotificationDelivered = false;
|
|
308
316
|
this.setState("connecting");
|
|
@@ -329,6 +337,14 @@ export class RealtimeSession {
|
|
|
329
337
|
if (waitForRemoteAudio)
|
|
330
338
|
await this.waitForRemoteAudioTrack(readyDeadline, signal);
|
|
331
339
|
throwIfAborted(signal);
|
|
340
|
+
if (this.sessionEnded || generation !== this.contextGeneration) {
|
|
341
|
+
throw new Error("Connection restoration interrupted");
|
|
342
|
+
}
|
|
343
|
+
await this.replacementContext.resume(recovering);
|
|
344
|
+
throwIfAborted(signal);
|
|
345
|
+
if (this.sessionEnded || generation !== this.contextGeneration) {
|
|
346
|
+
throw new Error("Connection restoration interrupted");
|
|
347
|
+
}
|
|
332
348
|
this.hasConnectedValue = true;
|
|
333
349
|
this.setState("connected");
|
|
334
350
|
}
|
|
@@ -373,6 +389,9 @@ export class RealtimeSession {
|
|
|
373
389
|
}
|
|
374
390
|
}
|
|
375
391
|
async performRejoin(apiBase, signal, recovering = true) {
|
|
392
|
+
this.contextGeneration += 1;
|
|
393
|
+
this.replacementContext.pause();
|
|
394
|
+
this.rejectPendingCommands(new Error("Restoring the session"));
|
|
376
395
|
let response = await this.fetchRoomToken(apiBase, signal);
|
|
377
396
|
if (response.status === 401 && this.refreshCredentialsCallback) {
|
|
378
397
|
await this.refreshCredentials();
|
|
@@ -393,6 +412,13 @@ export class RealtimeSession {
|
|
|
393
412
|
}
|
|
394
413
|
}
|
|
395
414
|
throwIfAborted(signal);
|
|
415
|
+
if (recovering && this.reconnectStartedAt !== null) {
|
|
416
|
+
this.pendingReconnect = {
|
|
417
|
+
started_at: new Date(this.reconnectStartedAt).toISOString(),
|
|
418
|
+
recovered_at: new Date().toISOString(),
|
|
419
|
+
duration_ms: Date.now() - this.reconnectStartedAt,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
396
422
|
await this.connectWithMode({
|
|
397
423
|
apiBase,
|
|
398
424
|
microphone: this.useManagedMicrophone,
|
|
@@ -408,9 +434,31 @@ export class RealtimeSession {
|
|
|
408
434
|
return this.sendTrackedCommand(message, timeoutMs);
|
|
409
435
|
}
|
|
410
436
|
async updateContext(update, timeoutMs = 20_000) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
437
|
+
if (this.sessionEnded || ["failed", "disconnected"].includes(this.stateValue)) {
|
|
438
|
+
throw new Error("Cannot update context while the session is inactive");
|
|
439
|
+
}
|
|
440
|
+
if (update.mode === "replace")
|
|
441
|
+
return this.replacementContext.submit(update, timeoutMs);
|
|
442
|
+
if (this.activeRecovery)
|
|
443
|
+
throw new Error("Cannot append context during recovery");
|
|
444
|
+
return this.sendContextUpdate(update, timeoutMs);
|
|
445
|
+
}
|
|
446
|
+
async sendContextUpdate(update, timeoutMs) {
|
|
447
|
+
const generation = this.contextGeneration;
|
|
448
|
+
let transferTimeout;
|
|
449
|
+
const transferExpired = new Promise((_resolve, reject) => {
|
|
450
|
+
transferTimeout = globalThis.setTimeout(() => {
|
|
451
|
+
const error = new Error(`Context transfer timed out after ${timeoutMs}ms`);
|
|
452
|
+
this.startAutoRejoin(error);
|
|
453
|
+
reject(error);
|
|
454
|
+
}, timeoutMs);
|
|
455
|
+
});
|
|
456
|
+
const transfer = await Promise.race([
|
|
457
|
+
"image" in update ? this.uploadImageContext(update) : this.uploadTextContext(update),
|
|
458
|
+
transferExpired,
|
|
459
|
+
]).finally(() => globalThis.clearTimeout(transferTimeout));
|
|
460
|
+
if (generation !== this.contextGeneration)
|
|
461
|
+
throw new Error("Context transfer interrupted by recovery");
|
|
414
462
|
const { payload } = transfer;
|
|
415
463
|
if (!isContextUpdatePayload(payload)) {
|
|
416
464
|
throw new TypeError("Invalid context update payload");
|
|
@@ -499,6 +547,8 @@ export class RealtimeSession {
|
|
|
499
547
|
}
|
|
500
548
|
async disconnect() {
|
|
501
549
|
this.cancelAutoRejoin();
|
|
550
|
+
this.contextGeneration += 1;
|
|
551
|
+
this.replacementContext.clear(new Error("Session disconnected"));
|
|
502
552
|
if (this.stateValue === "disconnected")
|
|
503
553
|
return;
|
|
504
554
|
this.rejectPendingCommands(new Error("Session disconnected before command completion"));
|
|
@@ -559,6 +609,11 @@ export class RealtimeSession {
|
|
|
559
609
|
this.notifyListeners(this.stateListeners, state);
|
|
560
610
|
}
|
|
561
611
|
bindRoomEvents() {
|
|
612
|
+
this.room.on(RoomEvent.ParticipantDisconnected, (participant) => {
|
|
613
|
+
if (participant.isAgent && !this.disconnectMode) {
|
|
614
|
+
this.startAutoRejoin(new Error("Agent disconnected from the room"));
|
|
615
|
+
}
|
|
616
|
+
});
|
|
562
617
|
this.room.on(RoomEvent.SignalReconnecting, () => this.handleLiveKitReconnecting());
|
|
563
618
|
this.room.on(RoomEvent.Reconnecting, () => this.handleLiveKitReconnecting());
|
|
564
619
|
this.room.on(RoomEvent.Reconnected, () => {
|
|
@@ -648,6 +703,8 @@ export class RealtimeSession {
|
|
|
648
703
|
});
|
|
649
704
|
}
|
|
650
705
|
handleLiveKitReconnecting() {
|
|
706
|
+
if (this.sessionEnded || this.disconnectMode === "intentional")
|
|
707
|
+
return;
|
|
651
708
|
this.agentReady = false;
|
|
652
709
|
// Once full rejoin owns recovery, late LiveKit reconnect notifications
|
|
653
710
|
// from the old transport must not move the wrapper back to reconnecting.
|
|
@@ -658,6 +715,8 @@ export class RealtimeSession {
|
|
|
658
715
|
this.setState("resuming");
|
|
659
716
|
return;
|
|
660
717
|
}
|
|
718
|
+
this.contextGeneration += 1;
|
|
719
|
+
this.replacementContext.pause();
|
|
661
720
|
this.reconnectStartedAt ??= Date.now();
|
|
662
721
|
this.rejectPendingCommands(new Error("Connection interrupted before command completion"), true);
|
|
663
722
|
if (this.activeRecovery === null) {
|
|
@@ -666,6 +725,9 @@ export class RealtimeSession {
|
|
|
666
725
|
this.setState("reconnecting");
|
|
667
726
|
}
|
|
668
727
|
async handleLiveKitReconnected() {
|
|
728
|
+
if (this.sessionEnded || this.disconnectMode || this.autoRejoinPromise)
|
|
729
|
+
return;
|
|
730
|
+
const generation = this.contextGeneration;
|
|
669
731
|
const recoveredAt = Date.now();
|
|
670
732
|
if (this.reconnectStartedAt !== null) {
|
|
671
733
|
this.pendingReconnect = {
|
|
@@ -683,8 +745,21 @@ export class RealtimeSession {
|
|
|
683
745
|
this.setState(this.hasConnectedValue ? "resuming" : "connected_not_ready");
|
|
684
746
|
try {
|
|
685
747
|
await this.sendHello();
|
|
748
|
+
const deadline = Date.now() + (this.connectDefaults.readyTimeoutMs ?? 15_000);
|
|
749
|
+
await this.waitForAgentReady(Math.max(deadline - Date.now(), 0));
|
|
750
|
+
if (this.connectDefaults.waitForRemoteAudio !== false)
|
|
751
|
+
await this.waitForRemoteAudioTrack(deadline);
|
|
752
|
+
if (this.sessionEnded || generation !== this.contextGeneration)
|
|
753
|
+
return;
|
|
754
|
+
await this.replacementContext.resume(true);
|
|
755
|
+
if (this.sessionEnded || generation !== this.contextGeneration)
|
|
756
|
+
return;
|
|
757
|
+
this.setState("connected");
|
|
758
|
+
this.completeRecovery();
|
|
686
759
|
}
|
|
687
760
|
catch (error) {
|
|
761
|
+
if (this.sessionEnded || generation !== this.contextGeneration)
|
|
762
|
+
return;
|
|
688
763
|
const normalizedError = toError(error);
|
|
689
764
|
if (this.shouldAutoRejoin()) {
|
|
690
765
|
this.startAutoRejoin(normalizedError);
|
|
@@ -697,8 +772,9 @@ export class RealtimeSession {
|
|
|
697
772
|
}
|
|
698
773
|
handleLiveKitDisconnected(reason) {
|
|
699
774
|
this.agentReady = false;
|
|
700
|
-
this.
|
|
701
|
-
|
|
775
|
+
if (this.disconnectMode !== "rejoin") {
|
|
776
|
+
this.pendingReconnect = null;
|
|
777
|
+
}
|
|
702
778
|
// Drop any remaining remote tracks so a stale, ended track cannot
|
|
703
779
|
// satisfy the next connect()'s wait for agent audio.
|
|
704
780
|
this.outputMediaStream?.getTracks().forEach((track) => {
|
|
@@ -775,15 +851,33 @@ export class RealtimeSession {
|
|
|
775
851
|
return (this.autoRejoinOptions !== null
|
|
776
852
|
&& this.hasConnectedValue
|
|
777
853
|
&& !this.sessionEnded
|
|
854
|
+
&& !["disconnected", "failed"].includes(this.stateValue)
|
|
778
855
|
&& !isTerminalDisconnectReason(reason));
|
|
779
856
|
}
|
|
780
857
|
startAutoRejoin(initialError) {
|
|
781
858
|
if (this.autoRejoinPromise || !this.shouldAutoRejoin())
|
|
782
859
|
return;
|
|
860
|
+
this.agentReady = false;
|
|
861
|
+
this.contextGeneration += 1;
|
|
862
|
+
this.replacementContext.pause();
|
|
863
|
+
this.rejectPendingCommands(initialError);
|
|
864
|
+
this.reconnectStartedAt ??= Date.now();
|
|
783
865
|
const controller = new AbortController();
|
|
784
866
|
this.autoRejoinController = controller;
|
|
867
|
+
const timeout = globalThis.setTimeout(() => {
|
|
868
|
+
if (controller.signal.aborted)
|
|
869
|
+
return;
|
|
870
|
+
const error = new Error("Session recovery timed out");
|
|
871
|
+
controller.abort(error);
|
|
872
|
+
this.contextGeneration += 1;
|
|
873
|
+
this.rejectPendingCommands(error);
|
|
874
|
+
this.replacementContext.clear(error);
|
|
875
|
+
this.failRecovery(error);
|
|
876
|
+
this.setState("failed");
|
|
877
|
+
}, this.autoRejoinOptions.timeoutMs);
|
|
785
878
|
const promise = this.runAutoRejoin(initialError, controller.signal)
|
|
786
879
|
.finally(() => {
|
|
880
|
+
globalThis.clearTimeout(timeout);
|
|
787
881
|
if (this.autoRejoinController === controller) {
|
|
788
882
|
this.autoRejoinController = null;
|
|
789
883
|
this.autoRejoinPromise = null;
|
|
@@ -813,6 +907,7 @@ export class RealtimeSession {
|
|
|
813
907
|
this.beginRecovery("rejoin", attempt);
|
|
814
908
|
await this.performRejoin(this.apiBase, signal);
|
|
815
909
|
throwIfAborted(signal);
|
|
910
|
+
this.reconnectStartedAt = null;
|
|
816
911
|
this.completeRecovery();
|
|
817
912
|
return;
|
|
818
913
|
}
|
|
@@ -825,6 +920,7 @@ export class RealtimeSession {
|
|
|
825
920
|
}
|
|
826
921
|
}
|
|
827
922
|
this.failRecovery(lastError);
|
|
923
|
+
this.replacementContext.clear(lastError);
|
|
828
924
|
this.setState("failed");
|
|
829
925
|
}
|
|
830
926
|
cancelAutoRejoin() {
|
|
@@ -867,12 +963,23 @@ export class RealtimeSession {
|
|
|
867
963
|
}
|
|
868
964
|
}
|
|
869
965
|
async fetchRoomToken(apiBase, signal) {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
966
|
+
const controller = new AbortController();
|
|
967
|
+
const cancel = () => controller.abort(signal?.reason);
|
|
968
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
969
|
+
const timeout = globalThis.setTimeout(() => controller.abort(), 10_000);
|
|
970
|
+
try {
|
|
971
|
+
throwIfAborted(signal);
|
|
972
|
+
return await this.fetcher(`${apiBase}/sessions/${this.id}/room-tokens`, {
|
|
973
|
+
method: "POST",
|
|
974
|
+
headers: { Authorization: `Bearer ${this.accessToken}` },
|
|
975
|
+
credentials: this.lifecycleCredentials,
|
|
976
|
+
signal: controller.signal,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
finally {
|
|
980
|
+
globalThis.clearTimeout(timeout);
|
|
981
|
+
signal?.removeEventListener("abort", cancel);
|
|
982
|
+
}
|
|
876
983
|
}
|
|
877
984
|
async refreshCredentials() {
|
|
878
985
|
if (!this.refreshCredentialsCallback)
|
|
@@ -886,11 +993,16 @@ export class RealtimeSession {
|
|
|
886
993
|
this.credentials = refreshed.connection;
|
|
887
994
|
}
|
|
888
995
|
observeConvenienceEvents(event) {
|
|
996
|
+
if (event.type === "session.ending" || event.type === "session.ended") {
|
|
997
|
+
this.contextGeneration += 1;
|
|
998
|
+
this.rejectPendingCommands(new Error("Session ended"));
|
|
999
|
+
}
|
|
889
1000
|
if (event.type === "session.ending") {
|
|
890
1001
|
// The server announced an intentional hang-up before tearing down the
|
|
891
1002
|
// room, so the coming disconnect must not fail recovery or auto-rejoin.
|
|
892
1003
|
this.sessionEnded = true;
|
|
893
1004
|
this.cancelAutoRejoin();
|
|
1005
|
+
this.replacementContext.clear(new Error("Session ended"));
|
|
894
1006
|
this.pendingEndEvent = event;
|
|
895
1007
|
}
|
|
896
1008
|
if (event.type === "session.ended") {
|
|
@@ -900,6 +1012,7 @@ export class RealtimeSession {
|
|
|
900
1012
|
|| this.stateValue === "reconnecting"
|
|
901
1013
|
|| this.stateValue === "resuming");
|
|
902
1014
|
this.sessionEnded = true;
|
|
1015
|
+
this.replacementContext.clear(new Error("Session ended"));
|
|
903
1016
|
if (this.activeRecovery !== null) {
|
|
904
1017
|
this.failRecovery(new Error("Session ended during recovery"));
|
|
905
1018
|
}
|
|
@@ -937,7 +1050,7 @@ export class RealtimeSession {
|
|
|
937
1050
|
...(reconnect ? { reconnect } : {}),
|
|
938
1051
|
});
|
|
939
1052
|
this.lastHelloMessageId = message.message_id;
|
|
940
|
-
await this.sendTrackedCommand(message,
|
|
1053
|
+
await this.sendTrackedCommand(message, CLIENT_HELLO_TIMEOUT_MS);
|
|
941
1054
|
if (this.pendingReconnect === reconnect)
|
|
942
1055
|
this.pendingReconnect = null;
|
|
943
1056
|
}
|
|
@@ -948,7 +1061,10 @@ export class RealtimeSession {
|
|
|
948
1061
|
if (pending)
|
|
949
1062
|
globalThis.clearInterval(pending.retry);
|
|
950
1063
|
this.pendingCommands.delete(message.message_id);
|
|
951
|
-
|
|
1064
|
+
const error = new Error(`Command ${message.type} timed out after ${timeoutMs}ms`);
|
|
1065
|
+
reject(error);
|
|
1066
|
+
if (message.type === "context.update")
|
|
1067
|
+
this.startAutoRejoin(error);
|
|
952
1068
|
}, timeoutMs);
|
|
953
1069
|
const retry = globalThis.setInterval(() => {
|
|
954
1070
|
void this.publishCommand(message).catch(() => {
|
|
@@ -1035,6 +1151,9 @@ export class RealtimeSession {
|
|
|
1035
1151
|
});
|
|
1036
1152
|
}
|
|
1037
1153
|
observeReadiness(event) {
|
|
1154
|
+
// Replayed readiness describes an old worker, not the current transport.
|
|
1155
|
+
if (event.stream_id === "durable-replay")
|
|
1156
|
+
return;
|
|
1038
1157
|
const payload = event.payload;
|
|
1039
1158
|
const snapshotReady = event.type === "agent.snapshot"
|
|
1040
1159
|
&& payload.status === "ready"
|
|
@@ -1042,14 +1161,15 @@ export class RealtimeSession {
|
|
|
1042
1161
|
&& payload.ack_message_id === this.lastHelloMessageId;
|
|
1043
1162
|
if (event.type !== "agent.ready" && !snapshotReady)
|
|
1044
1163
|
return;
|
|
1164
|
+
if (this.agentStreamId && this.agentStreamId !== event.stream_id && !this.activeRecovery) {
|
|
1165
|
+
this.startAutoRejoin(new Error("Agent worker was replaced"));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
this.agentStreamId = event.stream_id;
|
|
1045
1169
|
const wasReady = this.agentReady;
|
|
1046
1170
|
this.agentReady = true;
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
this.setState("connected");
|
|
1050
|
-
if (this.activeRecovery?.strategy === "livekit")
|
|
1051
|
-
this.completeRecovery();
|
|
1052
|
-
}
|
|
1171
|
+
// connect/reconnect owns the transition after hello, audio and context
|
|
1172
|
+
// restoration all finish. Readiness alone is not successful recovery.
|
|
1053
1173
|
this.readyResolve?.();
|
|
1054
1174
|
if (!wasReady)
|
|
1055
1175
|
this.notifyListeners(this.readyListeners, event);
|
|
@@ -1152,10 +1272,14 @@ function resolveAutoRejoinOptions(value) {
|
|
|
1152
1272
|
return null;
|
|
1153
1273
|
const options = value === true ? {} : value;
|
|
1154
1274
|
const resolved = {
|
|
1155
|
-
maxAttempts: options.maxAttempts ??
|
|
1275
|
+
maxAttempts: options.maxAttempts ?? Number.MAX_SAFE_INTEGER,
|
|
1156
1276
|
baseDelayMs: options.baseDelayMs ?? 750,
|
|
1157
1277
|
maxDelayMs: options.maxDelayMs ?? 5_000,
|
|
1278
|
+
timeoutMs: options.timeoutMs ?? 180_000,
|
|
1158
1279
|
};
|
|
1280
|
+
if (!Number.isFinite(resolved.timeoutMs) || resolved.timeoutMs <= 0) {
|
|
1281
|
+
throw new TypeError("autoRejoin.timeoutMs must be a positive finite number");
|
|
1282
|
+
}
|
|
1159
1283
|
if (!Number.isInteger(resolved.maxAttempts) || resolved.maxAttempts < 1) {
|
|
1160
1284
|
throw new TypeError("autoRejoin.maxAttempts must be a positive integer");
|
|
1161
1285
|
}
|