@openclaw/facetime 2026.9.6 → 2026.9.8
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/assets/icon.png +0 -0
- package/dist/.setup/{setup-DUgC9yE7.mjs → setup-CoxvlB5L.mjs} +91 -128
- package/dist/doctor-contract-api.js +189 -7
- package/dist/index.js +1 -1
- package/dist/runtime-api.js +197 -238
- package/node_modules/typebox/build/compile/code.mjs +1 -2
- package/node_modules/typebox/build/compile/validator.mjs +5 -1
- package/node_modules/typebox/build/format/iri.mjs +16 -9
- package/node_modules/typebox/build/guard/emit.mjs +2 -2
- package/node_modules/typebox/build/guard/guard.d.mts +7 -5
- package/node_modules/typebox/build/guard/guard.mjs +14 -9
- package/node_modules/typebox/build/guard/index.mjs +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
- package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
- package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
- package/node_modules/typebox/build/schema/build.mjs +2 -3
- package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
- package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
- package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
- package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
- package/node_modules/typebox/build/schema/parse.mjs +3 -0
- package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
- package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
- package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
- package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
- package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
- package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
- package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
- package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
- package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
- package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
- package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +3 -1
- package/package.json +5 -5
- package/src/audio-pump.ts +9 -16
- package/src/call-events.ts +12 -26
- package/src/call-lifecycle.ts +3 -19
- package/src/runtime-call-control.ts +3 -12
- package/src/runtime-call-events.ts +4 -9
- package/src/runtime-state.ts +0 -10
- package/src/setup.ts +96 -173
- package/src/talk-driver.ts +217 -242
- /package/node_modules/typebox/build/guard/{string.d.mts → unicode/unicode_segment.d.mts} +0 -0
package/src/talk-driver.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
|
2
2
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
3
|
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
4
4
|
import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveRealtimeVoiceAgentContextInstructions } from "openclaw/plugin-sdk/realtime-bootstrap-context";
|
|
6
6
|
import {
|
|
7
7
|
createRealtimeVoiceBridgeSession,
|
|
8
8
|
createTalkSessionController,
|
|
@@ -103,11 +103,9 @@ export async function startFaceTimeTalkDriver(params: {
|
|
|
103
103
|
let lastInputAudioStatusAt = 0;
|
|
104
104
|
let callMediaTimestampMs = 0;
|
|
105
105
|
let modelMediaGeneration = 1;
|
|
106
|
-
let responseGeneration = 0;
|
|
107
106
|
let response:
|
|
108
107
|
| {
|
|
109
108
|
id?: string;
|
|
110
|
-
generation: number;
|
|
111
109
|
startTimestampMs: number;
|
|
112
110
|
playbackStartFrame: number;
|
|
113
111
|
outcome?: RealtimeVoiceResponseOutcome;
|
|
@@ -120,7 +118,6 @@ export async function startFaceTimeTalkDriver(params: {
|
|
|
120
118
|
let activated = false;
|
|
121
119
|
let providerReady = false;
|
|
122
120
|
const providerReadyDeferred = createDeferred<void>();
|
|
123
|
-
let providerConnectPromise: Promise<void> | undefined;
|
|
124
121
|
let audioReadyPromise: Promise<void> | undefined;
|
|
125
122
|
let interruptProviderConnect: (() => void) | undefined;
|
|
126
123
|
let mediaSuspensionError: Error | undefined;
|
|
@@ -221,17 +218,11 @@ export async function startFaceTimeTalkDriver(params: {
|
|
|
221
218
|
};
|
|
222
219
|
|
|
223
220
|
const remember = (input: TalkEventInput) => talk.emit(input);
|
|
224
|
-
const ensureTurn = () => {
|
|
225
|
-
const turn = talk.ensureTurn({ payload: { callUUID: params.callUUID } });
|
|
226
|
-
return turn.turnId;
|
|
227
|
-
};
|
|
221
|
+
const ensureTurn = () => talk.ensureTurn({ payload: { callUUID: params.callUUID } }).turnId;
|
|
228
222
|
const finishOutputAudio = (reason: string) => {
|
|
229
223
|
talk.finishOutputAudio({ payload: { reason } });
|
|
230
224
|
};
|
|
231
|
-
const endTurn = (reason: string) => {
|
|
232
|
-
const ended = talk.endTurn({ payload: { reason } });
|
|
233
|
-
return ended.ok;
|
|
234
|
-
};
|
|
225
|
+
const endTurn = (reason: string) => talk.endTurn({ payload: { reason } }).ok;
|
|
235
226
|
const playedCurrentResponseMs = () =>
|
|
236
227
|
response === undefined
|
|
237
228
|
? 0
|
|
@@ -281,7 +272,6 @@ export async function startFaceTimeTalkDriver(params: {
|
|
|
281
272
|
}
|
|
282
273
|
response = {
|
|
283
274
|
id: responseId,
|
|
284
|
-
generation: ++responseGeneration,
|
|
285
275
|
startTimestampMs: callMediaTimestampMs,
|
|
286
276
|
playbackStartFrame: pump?.playedAudioFrames() ?? 0,
|
|
287
277
|
};
|
|
@@ -396,257 +386,242 @@ export async function startFaceTimeTalkDriver(params: {
|
|
|
396
386
|
finishDrainedResponse();
|
|
397
387
|
},
|
|
398
388
|
});
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
389
|
+
const providerConnect = (async () => {
|
|
390
|
+
let removeAbortListener: (() => void) | undefined;
|
|
391
|
+
let readinessTimer: NodeJS.Timeout | undefined;
|
|
392
|
+
const interrupted = new Promise<never>((_resolve, reject) => {
|
|
393
|
+
const interrupt = () => reject(new Error("FaceTime talk startup aborted"));
|
|
394
|
+
interruptProviderConnect = interrupt;
|
|
395
|
+
params.signal?.addEventListener("abort", interrupt, { once: true });
|
|
396
|
+
removeAbortListener = () => params.signal?.removeEventListener("abort", interrupt);
|
|
397
|
+
if (params.signal?.aborted || stopped || mediaSuspended) {
|
|
398
|
+
interrupt();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
const readinessTimedOut = new Promise<never>((_resolve, reject) => {
|
|
402
|
+
readinessTimer = setTimeout(() => {
|
|
403
|
+
reject(new Error("Realtime provider was not ready within 15 seconds"));
|
|
404
|
+
}, REALTIME_READY_TIMEOUT_MS);
|
|
405
|
+
readinessTimer.unref?.();
|
|
406
|
+
});
|
|
407
|
+
const prepareAndConnect = async () => {
|
|
408
|
+
const providerResolution = resolveFaceTimeRealtimeProvider({
|
|
409
|
+
config: params.config,
|
|
410
|
+
fullConfig: params.fullConfig,
|
|
411
|
+
agentId: consultAgentId,
|
|
414
412
|
});
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
413
|
+
const agentContextResolution = resolveRealtimeVoiceAgentContextInstructions({
|
|
414
|
+
config: params.fullConfig,
|
|
415
|
+
agentId: consultAgentId,
|
|
416
|
+
sessionKey: requesterSessionKey,
|
|
417
|
+
warn: (message) => params.logger.warn?.(`[facetime] realtime agent context: ${message}`),
|
|
420
418
|
});
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
audioFormat: REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ,
|
|
453
|
-
// Configured voice/personality instructions remain customizable, but the
|
|
454
|
-
// authoritative-agent boundary must not disappear when they are replaced.
|
|
455
|
-
instructions: buildRealtimeInstructions({
|
|
456
|
-
instructions: params.config.realtime.instructions,
|
|
457
|
-
bootstrapContext,
|
|
458
|
-
toolPolicy: params.config.realtime.toolPolicy,
|
|
459
|
-
}),
|
|
460
|
-
autoRespondToAudio: true,
|
|
461
|
-
triggerGreetingOnReady: false,
|
|
462
|
-
initialGreetingInstructions: initialGreeting.instructions,
|
|
463
|
-
markStrategy: "ack-immediately",
|
|
464
|
-
tools: resolveRealtimeVoiceAgentConsultTools(params.config.realtime.toolPolicy, [
|
|
465
|
-
FACETIME_END_CALL_TOOL,
|
|
466
|
-
]),
|
|
467
|
-
audioSink: {
|
|
468
|
-
isOpen: () => !stopped && !mediaSuspended,
|
|
469
|
-
sendAudio(audio, metadata) {
|
|
470
|
-
if (stopped || mediaSuspended) {
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
const turnId = ensureTurn();
|
|
474
|
-
if (!response) {
|
|
475
|
-
startResponse();
|
|
476
|
-
}
|
|
477
|
-
talk.startOutputAudio({ turnId, payload: { callUUID: params.callUUID } });
|
|
478
|
-
remember({
|
|
479
|
-
type: "output.audio.delta",
|
|
480
|
-
turnId,
|
|
481
|
-
payload: { byteLength: audio.byteLength },
|
|
482
|
-
});
|
|
483
|
-
pump?.writeOutputAudio(audio, metadata);
|
|
484
|
-
},
|
|
485
|
-
getPlaybackState: () => pump.getPlaybackState(),
|
|
486
|
-
clearAudio() {
|
|
487
|
-
if (stopped || mediaSuspended) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
pump?.clearOutputAudio();
|
|
491
|
-
resetResponsePlayback();
|
|
492
|
-
finishOutputAudio("clear");
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
onTranscript(role, text, final) {
|
|
419
|
+
const [resolved, bootstrapContext] = await Promise.all([
|
|
420
|
+
providerResolution,
|
|
421
|
+
agentContextResolution,
|
|
422
|
+
]);
|
|
423
|
+
if (params.signal?.aborted || stopped || mediaSuspended) {
|
|
424
|
+
throw new Error("FaceTime talk startup aborted");
|
|
425
|
+
}
|
|
426
|
+
bridge = createRealtimeVoiceBridgeSession({
|
|
427
|
+
provider: resolved.provider,
|
|
428
|
+
capabilities: resolved.capabilities,
|
|
429
|
+
cfg: params.fullConfig,
|
|
430
|
+
agentId: consultAgentId,
|
|
431
|
+
providerConfig: resolved.providerConfig,
|
|
432
|
+
audioFormat: REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ,
|
|
433
|
+
// Configured voice/personality instructions remain customizable, but the
|
|
434
|
+
// authoritative-agent boundary must not disappear when they are replaced.
|
|
435
|
+
instructions: buildRealtimeInstructions({
|
|
436
|
+
instructions: params.config.realtime.instructions,
|
|
437
|
+
bootstrapContext,
|
|
438
|
+
toolPolicy: params.config.realtime.toolPolicy,
|
|
439
|
+
}),
|
|
440
|
+
autoRespondToAudio: true,
|
|
441
|
+
triggerGreetingOnReady: false,
|
|
442
|
+
initialGreetingInstructions: initialGreeting.instructions,
|
|
443
|
+
markStrategy: "ack-immediately",
|
|
444
|
+
tools: resolveRealtimeVoiceAgentConsultTools(params.config.realtime.toolPolicy, [
|
|
445
|
+
FACETIME_END_CALL_TOOL,
|
|
446
|
+
]),
|
|
447
|
+
audioSink: {
|
|
448
|
+
isOpen: () => !stopped && !mediaSuspended,
|
|
449
|
+
sendAudio(audio, metadata) {
|
|
496
450
|
if (stopped || mediaSuspended) {
|
|
497
451
|
return;
|
|
498
452
|
}
|
|
499
453
|
const turnId = ensureTurn();
|
|
454
|
+
if (!response) {
|
|
455
|
+
startResponse();
|
|
456
|
+
}
|
|
457
|
+
talk.startOutputAudio({ turnId, payload: { callUUID: params.callUUID } });
|
|
500
458
|
remember({
|
|
501
|
-
type:
|
|
502
|
-
role === "assistant"
|
|
503
|
-
? final
|
|
504
|
-
? "output.text.done"
|
|
505
|
-
: "output.text.delta"
|
|
506
|
-
: final
|
|
507
|
-
? "transcript.done"
|
|
508
|
-
: "transcript.delta",
|
|
459
|
+
type: "output.audio.delta",
|
|
509
460
|
turnId,
|
|
510
|
-
payload:
|
|
511
|
-
final,
|
|
461
|
+
payload: { byteLength: audio.byteLength },
|
|
512
462
|
});
|
|
513
|
-
|
|
514
|
-
if (text.trim()) {
|
|
515
|
-
initialGreeting.cancel();
|
|
516
|
-
} else {
|
|
517
|
-
initialGreeting.schedule();
|
|
518
|
-
}
|
|
519
|
-
remember({
|
|
520
|
-
type: "input.audio.committed",
|
|
521
|
-
turnId,
|
|
522
|
-
payload: { callUUID: params.callUUID },
|
|
523
|
-
final: true,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
if (final) {
|
|
527
|
-
appendTranscript({ role, text });
|
|
528
|
-
}
|
|
463
|
+
pump?.writeOutputAudio(audio, metadata);
|
|
529
464
|
},
|
|
530
|
-
|
|
465
|
+
getPlaybackState: () => pump.getPlaybackState(),
|
|
466
|
+
clearAudio() {
|
|
531
467
|
if (stopped || mediaSuspended) {
|
|
532
468
|
return;
|
|
533
469
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
payload: {
|
|
538
|
-
name: `${event.direction}:${event.type}`,
|
|
539
|
-
message: event.detail,
|
|
540
|
-
},
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
if (event.type === "response.created") {
|
|
544
|
-
startResponse(event.responseId);
|
|
545
|
-
} else if (event.type === "session.continuity.reset") {
|
|
546
|
-
resetProviderContinuity();
|
|
547
|
-
} else if (event.type === "response.done") {
|
|
548
|
-
if (!event.responseId && suppressNextUnkeyedLegacyTerminal) {
|
|
549
|
-
suppressNextUnkeyedLegacyTerminal = false;
|
|
550
|
-
} else {
|
|
551
|
-
finishResponseOutcome(
|
|
552
|
-
{
|
|
553
|
-
status: "completed",
|
|
554
|
-
...(event.responseId ? { responseId: event.responseId } : {}),
|
|
555
|
-
},
|
|
556
|
-
false,
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
} else if (event.type === "error") {
|
|
560
|
-
remember({
|
|
561
|
-
type: "session.error",
|
|
562
|
-
payload: { message: event.detail ?? "Realtime provider error" },
|
|
563
|
-
final: true,
|
|
564
|
-
});
|
|
565
|
-
}
|
|
470
|
+
pump?.clearOutputAudio();
|
|
471
|
+
resetResponsePlayback();
|
|
472
|
+
finishOutputAudio("clear");
|
|
566
473
|
},
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
474
|
+
},
|
|
475
|
+
onTranscript(role, text, final) {
|
|
476
|
+
if (stopped || mediaSuspended) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const turnId = ensureTurn();
|
|
480
|
+
remember({
|
|
481
|
+
type:
|
|
482
|
+
role === "assistant"
|
|
483
|
+
? final
|
|
484
|
+
? "output.text.done"
|
|
485
|
+
: "output.text.delta"
|
|
486
|
+
: final
|
|
487
|
+
? "transcript.done"
|
|
488
|
+
: "transcript.delta",
|
|
489
|
+
turnId,
|
|
490
|
+
payload: role === "assistant" ? { text } : { role, text },
|
|
491
|
+
final,
|
|
492
|
+
});
|
|
493
|
+
if (role === "user" && final) {
|
|
494
|
+
if (text.trim()) {
|
|
495
|
+
initialGreeting.cancel();
|
|
496
|
+
} else {
|
|
497
|
+
initialGreeting.schedule();
|
|
575
498
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
499
|
+
remember({
|
|
500
|
+
type: "input.audio.committed",
|
|
501
|
+
turnId,
|
|
502
|
+
payload: { callUUID: params.callUUID },
|
|
503
|
+
final: true,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (final) {
|
|
507
|
+
appendTranscript({ role, text });
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
onEvent(event) {
|
|
511
|
+
if (stopped || mediaSuspended) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!(event.direction === "client" && event.type === "input_audio_buffer.append")) {
|
|
515
|
+
remember({
|
|
516
|
+
type: "health.changed",
|
|
517
|
+
payload: {
|
|
518
|
+
name: `${event.direction}:${event.type}`,
|
|
519
|
+
message: event.detail,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (event.type === "response.created") {
|
|
524
|
+
startResponse(event.responseId);
|
|
525
|
+
} else if (event.type === "session.continuity.reset") {
|
|
526
|
+
resetProviderContinuity();
|
|
527
|
+
} else if (event.type === "response.done") {
|
|
528
|
+
if (!event.responseId && suppressNextUnkeyedLegacyTerminal) {
|
|
529
|
+
suppressNextUnkeyedLegacyTerminal = false;
|
|
530
|
+
} else {
|
|
531
|
+
finishResponseOutcome(
|
|
532
|
+
{
|
|
533
|
+
status: "completed",
|
|
534
|
+
...(event.responseId ? { responseId: event.responseId } : {}),
|
|
535
|
+
},
|
|
536
|
+
false,
|
|
537
|
+
);
|
|
580
538
|
}
|
|
539
|
+
} else if (event.type === "error") {
|
|
581
540
|
remember({
|
|
582
541
|
type: "session.error",
|
|
583
|
-
payload: { message:
|
|
542
|
+
payload: { message: event.detail ?? "Realtime provider error" },
|
|
584
543
|
final: true,
|
|
585
544
|
});
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
onResponseDone(outcome) {
|
|
548
|
+
finishResponseOutcome(outcome, true);
|
|
549
|
+
},
|
|
550
|
+
onToolCall: consultController.handleToolCall,
|
|
551
|
+
onReady() {
|
|
552
|
+
if (!stopped && !mediaSuspended) {
|
|
553
|
+
remember({ type: "session.ready", payload: { callUUID: params.callUUID } });
|
|
554
|
+
providerReadyDeferred.resolve();
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
onError(error) {
|
|
558
|
+
if (stopped || mediaSuspended) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
remember({
|
|
562
|
+
type: "session.error",
|
|
563
|
+
payload: { message: formatErrorMessage(error) },
|
|
564
|
+
final: true,
|
|
565
|
+
});
|
|
566
|
+
params.logger.warn(`[facetime] realtime bridge failed: ${formatErrorMessage(error)}`);
|
|
567
|
+
if (!providerReady && !startupSettled) {
|
|
598
568
|
signalStartupFailure(error);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
},
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
);
|
|
632
|
-
// Lifecycle cancellation starts carrier shutdown; it does not confirm
|
|
633
|
-
// closure. Retain process-tap suppression until its owner confirms safety.
|
|
634
|
-
const safeToClose = stopped || (await reportFailure(normalized));
|
|
635
|
-
if (safeToClose) {
|
|
636
|
-
await close(params.signal?.aborted || stopped ? "startup-aborted" : "connect-failed");
|
|
637
|
-
}
|
|
638
|
-
throw normalized;
|
|
639
|
-
} finally {
|
|
640
|
-
if (readinessTimer) {
|
|
641
|
-
clearTimeout(readinessTimer);
|
|
642
|
-
}
|
|
643
|
-
interruptProviderConnect = undefined;
|
|
644
|
-
removeAbortListener?.();
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
onClose(reason) {
|
|
572
|
+
if (stopped || mediaSuspended) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
finishOutputAudio(reason);
|
|
576
|
+
remember({ type: "session.closed", payload: { reason }, final: true });
|
|
577
|
+
const error = new Error(`Realtime bridge closed unexpectedly: ${reason}`);
|
|
578
|
+
signalStartupFailure(error);
|
|
579
|
+
void (async () => {
|
|
580
|
+
await suspendMedia("provider-closed");
|
|
581
|
+
const safeToClose = await reportFailure(error);
|
|
582
|
+
if (safeToClose) {
|
|
583
|
+
await close("provider-closed");
|
|
584
|
+
}
|
|
585
|
+
})();
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
await bridge.connect();
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
// Provider connect() may return before the server's setup-complete
|
|
592
|
+
// event. onReady is the contract that the session can accept audio.
|
|
593
|
+
await Promise.race([
|
|
594
|
+
Promise.all([prepareAndConnect(), providerReadyDeferred.promise]),
|
|
595
|
+
startupFailurePromise,
|
|
596
|
+
interrupted,
|
|
597
|
+
readinessTimedOut,
|
|
598
|
+
]);
|
|
599
|
+
if (startupFailure) {
|
|
600
|
+
throw startupFailure;
|
|
645
601
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
602
|
+
if (params.signal?.aborted || stopped || mediaSuspended) {
|
|
603
|
+
throw new Error("FaceTime talk startup aborted");
|
|
604
|
+
}
|
|
605
|
+
startupSettled = true;
|
|
606
|
+
providerReady = true;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
609
|
+
await suspendMedia(params.signal?.aborted || stopped ? "startup-aborted" : "connect-failed");
|
|
610
|
+
// Lifecycle cancellation starts carrier shutdown; it does not confirm
|
|
611
|
+
// closure. Retain process-tap suppression until its owner confirms safety.
|
|
612
|
+
const safeToClose = stopped || (await reportFailure(normalized));
|
|
613
|
+
if (safeToClose) {
|
|
614
|
+
await close(params.signal?.aborted || stopped ? "startup-aborted" : "connect-failed");
|
|
615
|
+
}
|
|
616
|
+
throw normalized;
|
|
617
|
+
} finally {
|
|
618
|
+
if (readinessTimer) {
|
|
619
|
+
clearTimeout(readinessTimer);
|
|
620
|
+
}
|
|
621
|
+
interruptProviderConnect = undefined;
|
|
622
|
+
removeAbortListener?.();
|
|
623
|
+
}
|
|
624
|
+
})();
|
|
650
625
|
void providerConnect.catch(() => {});
|
|
651
626
|
try {
|
|
652
627
|
await Promise.race([pump.suppressionReady(), startupFailurePromise]);
|
|
File without changes
|