@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.
Files changed (57) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/.setup/{setup-DUgC9yE7.mjs → setup-CoxvlB5L.mjs} +91 -128
  3. package/dist/doctor-contract-api.js +189 -7
  4. package/dist/index.js +1 -1
  5. package/dist/runtime-api.js +197 -238
  6. package/node_modules/typebox/build/compile/code.mjs +1 -2
  7. package/node_modules/typebox/build/compile/validator.mjs +5 -1
  8. package/node_modules/typebox/build/format/iri.mjs +16 -9
  9. package/node_modules/typebox/build/guard/emit.mjs +2 -2
  10. package/node_modules/typebox/build/guard/guard.d.mts +7 -5
  11. package/node_modules/typebox/build/guard/guard.mjs +14 -9
  12. package/node_modules/typebox/build/guard/index.mjs +6 -0
  13. package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
  14. package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
  15. package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
  16. package/node_modules/typebox/build/schema/build.mjs +2 -3
  17. package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
  18. package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
  19. package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
  20. package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
  21. package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
  22. package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
  23. package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
  24. package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
  25. package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
  26. package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
  27. package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
  28. package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
  29. package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
  30. package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
  31. package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
  32. package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
  33. package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
  34. package/node_modules/typebox/build/schema/parse.mjs +3 -0
  35. package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
  36. package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
  37. package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
  38. package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
  39. package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
  40. package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
  41. package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
  42. package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
  43. package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
  44. package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
  45. package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
  46. package/node_modules/typebox/package.json +1 -1
  47. package/node_modules/typebox/readme.md +3 -1
  48. package/package.json +5 -5
  49. package/src/audio-pump.ts +9 -16
  50. package/src/call-events.ts +12 -26
  51. package/src/call-lifecycle.ts +3 -19
  52. package/src/runtime-call-control.ts +3 -12
  53. package/src/runtime-call-events.ts +4 -9
  54. package/src/runtime-state.ts +0 -10
  55. package/src/setup.ts +96 -173
  56. package/src/talk-driver.ts +217 -242
  57. /package/node_modules/typebox/build/guard/{string.d.mts → unicode/unicode_segment.d.mts} +0 -0
@@ -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 { resolveRealtimeBootstrapContextInstructions } from "openclaw/plugin-sdk/realtime-bootstrap-context";
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 connectProvider = async () => {
400
- if (providerConnectPromise) {
401
- return await providerConnectPromise;
402
- }
403
- providerConnectPromise = (async () => {
404
- let removeAbortListener: (() => void) | undefined;
405
- let readinessTimer: NodeJS.Timeout | undefined;
406
- const interrupted = new Promise<never>((_resolve, reject) => {
407
- const interrupt = () => reject(new Error("FaceTime talk startup aborted"));
408
- interruptProviderConnect = interrupt;
409
- params.signal?.addEventListener("abort", interrupt, { once: true });
410
- removeAbortListener = () => params.signal?.removeEventListener("abort", interrupt);
411
- if (params.signal?.aborted || stopped || mediaSuspended) {
412
- interrupt();
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 readinessTimedOut = new Promise<never>((_resolve, reject) => {
416
- readinessTimer = setTimeout(() => {
417
- reject(new Error("Realtime provider was not ready within 15 seconds"));
418
- }, REALTIME_READY_TIMEOUT_MS);
419
- readinessTimer.unref?.();
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 prepareAndConnect = async () => {
422
- const providerResolution = resolveFaceTimeRealtimeProvider({
423
- config: params.config,
424
- fullConfig: params.fullConfig,
425
- agentId: consultAgentId,
426
- });
427
- const bootstrapContextResolution = resolveRealtimeBootstrapContextInstructions({
428
- config: params.fullConfig,
429
- agentId: consultAgentId,
430
- sessionKey: requesterSessionKey,
431
- warn: (message) =>
432
- params.logger.warn?.(`[facetime] realtime bootstrap context: ${message}`),
433
- }).catch((error: unknown) => {
434
- params.logger.warn?.(
435
- `[facetime] realtime bootstrap context unavailable: ${formatErrorMessage(error)}`,
436
- );
437
- return undefined;
438
- });
439
- const [resolved, bootstrapContext] = await Promise.all([
440
- providerResolution,
441
- bootstrapContextResolution,
442
- ]);
443
- if (params.signal?.aborted || stopped || mediaSuspended) {
444
- throw new Error("FaceTime talk startup aborted");
445
- }
446
- bridge = createRealtimeVoiceBridgeSession({
447
- provider: resolved.provider,
448
- capabilities: resolved.capabilities,
449
- cfg: params.fullConfig,
450
- agentId: consultAgentId,
451
- providerConfig: resolved.providerConfig,
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: role === "assistant" ? { text } : { role, text },
511
- final,
461
+ payload: { byteLength: audio.byteLength },
512
462
  });
513
- if (role === "user" && final) {
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
- onEvent(event) {
465
+ getPlaybackState: () => pump.getPlaybackState(),
466
+ clearAudio() {
531
467
  if (stopped || mediaSuspended) {
532
468
  return;
533
469
  }
534
- if (!(event.direction === "client" && event.type === "input_audio_buffer.append")) {
535
- remember({
536
- type: "health.changed",
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
- onResponseDone(outcome) {
568
- finishResponseOutcome(outcome, true);
569
- },
570
- onToolCall: consultController.handleToolCall,
571
- onReady() {
572
- if (!stopped && !mediaSuspended) {
573
- remember({ type: "session.ready", payload: { callUUID: params.callUUID } });
574
- providerReadyDeferred.resolve();
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
- onError(error) {
578
- if (stopped || mediaSuspended) {
579
- return;
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: formatErrorMessage(error) },
542
+ payload: { message: event.detail ?? "Realtime provider error" },
584
543
  final: true,
585
544
  });
586
- params.logger.warn(`[facetime] realtime bridge failed: ${formatErrorMessage(error)}`);
587
- if (!providerReady && !startupSettled) {
588
- signalStartupFailure(error);
589
- }
590
- },
591
- onClose(reason) {
592
- if (stopped || mediaSuspended) {
593
- return;
594
- }
595
- finishOutputAudio(reason);
596
- remember({ type: "session.closed", payload: { reason }, final: true });
597
- const error = new Error(`Realtime bridge closed unexpectedly: ${reason}`);
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
- void (async () => {
600
- await suspendMedia("provider-closed");
601
- const safeToClose = await reportFailure(error);
602
- if (safeToClose) {
603
- await close("provider-closed");
604
- }
605
- })();
606
- },
607
- });
608
- await bridge.connect();
609
- };
610
- try {
611
- // Provider connect() may return before the server's setup-complete
612
- // event. onReady is the contract that the session can accept audio.
613
- await Promise.race([
614
- Promise.all([prepareAndConnect(), providerReadyDeferred.promise]),
615
- startupFailurePromise,
616
- interrupted,
617
- readinessTimedOut,
618
- ]);
619
- if (startupFailure) {
620
- throw startupFailure;
621
- }
622
- if (params.signal?.aborted || stopped || mediaSuspended) {
623
- throw new Error("FaceTime talk startup aborted");
624
- }
625
- startupSettled = true;
626
- providerReady = true;
627
- } catch (error) {
628
- const normalized = error instanceof Error ? error : new Error(String(error));
629
- await suspendMedia(
630
- params.signal?.aborted || stopped ? "startup-aborted" : "connect-failed",
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
- return await providerConnectPromise;
648
- };
649
- const providerConnect = connectProvider();
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]);