@absolutejs/voice 0.0.22-beta.648 → 0.0.22-beta.649

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.
@@ -343,29 +343,34 @@ var createMicrophoneCapture = (options) => {
343
343
  if (!AudioContextCtor) {
344
344
  throw new Error("Browser microphone capture requires AudioContext support.");
345
345
  }
346
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
347
- audio: {
348
- autoGainControl: true,
349
- channelCount: options.channelCount ?? 1,
350
- echoCancellation: true,
351
- noiseSuppression: true
346
+ try {
347
+ mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
348
+ audio: {
349
+ autoGainControl: true,
350
+ channelCount: options.channelCount ?? 1,
351
+ echoCancellation: true,
352
+ noiseSuppression: true
353
+ }
354
+ });
355
+ audioContext = new AudioContextCtor;
356
+ if (audioContext.state === "suspended") {
357
+ await audioContext.resume();
352
358
  }
353
- });
354
- audioContext = new AudioContextCtor;
355
- if (audioContext.state === "suspended") {
356
- await audioContext.resume();
357
- }
358
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
359
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
360
- processorNode.onaudioprocess = (event) => {
361
- const channel = event.inputBuffer.getChannelData(0);
362
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
363
- const pcm = floatTo16BitPCM(downsampled);
364
- options.onLevel?.(getPcmLevel(pcm));
365
- options.onAudio(pcm);
366
- };
367
- sourceNode.connect(processorNode);
368
- processorNode.connect(audioContext.destination);
359
+ sourceNode = audioContext.createMediaStreamSource(mediaStream);
360
+ processorNode = audioContext.createScriptProcessor(4096, 1, 1);
361
+ processorNode.onaudioprocess = (event) => {
362
+ const channel = event.inputBuffer.getChannelData(0);
363
+ const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
364
+ const pcm = floatTo16BitPCM(downsampled);
365
+ options.onLevel?.(getPcmLevel(pcm));
366
+ options.onAudio(pcm);
367
+ };
368
+ sourceNode.connect(processorNode);
369
+ processorNode.connect(audioContext.destination);
370
+ } catch (error) {
371
+ stop();
372
+ throw error;
373
+ }
369
374
  };
370
375
  const stop = () => {
371
376
  processorNode?.disconnect();
@@ -585,6 +590,7 @@ var noopUnsubscribe = () => noop;
585
590
  var NOOP_CONNECTION = {
586
591
  callControl: noop,
587
592
  close: noop,
593
+ disconnect: noop,
588
594
  endTurn: noop,
589
595
  send: noop,
590
596
  sendAudio: noop,
@@ -596,7 +602,7 @@ var NOOP_CONNECTION = {
596
602
  start: () => {}
597
603
  };
598
604
  var createSessionId = () => crypto.randomUUID();
599
- var buildWsUrl = (path, sessionId, scenarioId) => {
605
+ var buildWsUrl = (path, sessionId, scenarioId, query) => {
600
606
  const { hostname, port, protocol } = window.location;
601
607
  const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
602
608
  const portSuffix = port ? `:${port}` : "";
@@ -605,6 +611,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
605
611
  if (scenarioId) {
606
612
  url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
607
613
  }
614
+ for (const [key, value] of Object.entries(query)) {
615
+ if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
616
+ url.searchParams.set(key, value);
617
+ }
618
+ }
608
619
  return url.toString();
609
620
  };
610
621
  var isVoiceServerMessage = (value) => {
@@ -714,7 +725,7 @@ var createVoiceConnection = (path, options = {}) => {
714
725
  }, delayMs);
715
726
  };
716
727
  const connect = () => {
717
- const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
728
+ const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
718
729
  ws.binaryType = "arraybuffer";
719
730
  ws.onopen = () => {
720
731
  const wasReconnecting = state.reconnectAttempts > 0;
@@ -810,7 +821,19 @@ var createVoiceConnection = (path, options = {}) => {
810
821
  type: "call_control"
811
822
  });
812
823
  };
813
- const close = () => {
824
+ const close = (reason = "client-close") => {
825
+ clearTimers();
826
+ if (state.ws) {
827
+ if (state.ws.readyState === WS_OPEN) {
828
+ state.ws.send(JSON.stringify({ reason, type: "close" }));
829
+ }
830
+ state.ws.close(WS_NORMAL_CLOSURE);
831
+ state.ws = null;
832
+ }
833
+ state.isConnected = false;
834
+ listeners.clear();
835
+ };
836
+ const disconnect = () => {
814
837
  clearTimers();
815
838
  if (state.ws) {
816
839
  state.ws.close(WS_NORMAL_CLOSURE);
@@ -834,6 +857,7 @@ var createVoiceConnection = (path, options = {}) => {
834
857
  return {
835
858
  callControl,
836
859
  close,
860
+ disconnect,
837
861
  endTurn,
838
862
  send,
839
863
  sendAudio,
@@ -1114,10 +1138,17 @@ var createVoiceStream = (path, options = {}) => {
1114
1138
  callControl(message) {
1115
1139
  connection.callControl(message);
1116
1140
  },
1117
- close() {
1141
+ close(reason) {
1118
1142
  unsubscribeConnection();
1119
1143
  browserMediaReporter?.close();
1120
- connection.close();
1144
+ connection.close(reason);
1145
+ store.dispatch({ type: "disconnected" });
1146
+ notify();
1147
+ },
1148
+ disconnect() {
1149
+ unsubscribeConnection();
1150
+ browserMediaReporter?.close();
1151
+ connection.disconnect();
1121
1152
  store.dispatch({ type: "disconnected" });
1122
1153
  notify();
1123
1154
  },
@@ -1599,6 +1630,7 @@ var createVoiceController = (path, options = {}) => {
1599
1630
  };
1600
1631
  notify();
1601
1632
  } catch (error) {
1633
+ capture?.stop();
1602
1634
  capture = null;
1603
1635
  state = {
1604
1636
  ...state,
@@ -1609,13 +1641,19 @@ var createVoiceController = (path, options = {}) => {
1609
1641
  throw error;
1610
1642
  }
1611
1643
  };
1612
- const close = () => {
1644
+ const close = (reason) => {
1645
+ unsubscribeStream();
1646
+ stopRecording();
1647
+ stream.close(reason);
1648
+ };
1649
+ const disconnect = () => {
1613
1650
  unsubscribeStream();
1614
1651
  stopRecording();
1615
- stream.close();
1652
+ stream.disconnect();
1616
1653
  };
1617
1654
  return {
1618
1655
  close,
1656
+ disconnect,
1619
1657
  startRecording,
1620
1658
  stopRecording,
1621
1659
  get assistantAudio() {
@@ -11,7 +11,8 @@ type VoiceConnectionHandle = {
11
11
  sessionId?: string;
12
12
  scenarioId?: string;
13
13
  }) => void;
14
- close: () => void;
14
+ close: (reason?: string) => void;
15
+ disconnect: () => void;
15
16
  endTurn: () => void;
16
17
  getReadyState: () => number;
17
18
  getScenarioId: () => string;
@@ -111,29 +111,34 @@ var createMicrophoneCapture = (options) => {
111
111
  if (!AudioContextCtor) {
112
112
  throw new Error("Browser microphone capture requires AudioContext support.");
113
113
  }
114
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
115
- audio: {
116
- autoGainControl: true,
117
- channelCount: options.channelCount ?? 1,
118
- echoCancellation: true,
119
- noiseSuppression: true
114
+ try {
115
+ mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
116
+ audio: {
117
+ autoGainControl: true,
118
+ channelCount: options.channelCount ?? 1,
119
+ echoCancellation: true,
120
+ noiseSuppression: true
121
+ }
122
+ });
123
+ audioContext = new AudioContextCtor;
124
+ if (audioContext.state === "suspended") {
125
+ await audioContext.resume();
120
126
  }
121
- });
122
- audioContext = new AudioContextCtor;
123
- if (audioContext.state === "suspended") {
124
- await audioContext.resume();
125
- }
126
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
127
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
128
- processorNode.onaudioprocess = (event) => {
129
- const channel = event.inputBuffer.getChannelData(0);
130
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
131
- const pcm = floatTo16BitPCM(downsampled);
132
- options.onLevel?.(getPcmLevel(pcm));
133
- options.onAudio(pcm);
134
- };
135
- sourceNode.connect(processorNode);
136
- processorNode.connect(audioContext.destination);
127
+ sourceNode = audioContext.createMediaStreamSource(mediaStream);
128
+ processorNode = audioContext.createScriptProcessor(4096, 1, 1);
129
+ processorNode.onaudioprocess = (event) => {
130
+ const channel = event.inputBuffer.getChannelData(0);
131
+ const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
132
+ const pcm = floatTo16BitPCM(downsampled);
133
+ options.onLevel?.(getPcmLevel(pcm));
134
+ options.onAudio(pcm);
135
+ };
136
+ sourceNode.connect(processorNode);
137
+ processorNode.connect(audioContext.destination);
138
+ } catch (error) {
139
+ stop();
140
+ throw error;
141
+ }
137
142
  };
138
143
  const stop = () => {
139
144
  processorNode?.disconnect();
@@ -502,6 +507,7 @@ var noopUnsubscribe = () => noop;
502
507
  var NOOP_CONNECTION = {
503
508
  callControl: noop,
504
509
  close: noop,
510
+ disconnect: noop,
505
511
  endTurn: noop,
506
512
  send: noop,
507
513
  sendAudio: noop,
@@ -513,7 +519,7 @@ var NOOP_CONNECTION = {
513
519
  start: () => {}
514
520
  };
515
521
  var createSessionId = () => crypto.randomUUID();
516
- var buildWsUrl = (path, sessionId, scenarioId) => {
522
+ var buildWsUrl = (path, sessionId, scenarioId, query) => {
517
523
  const { hostname, port, protocol } = window.location;
518
524
  const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
519
525
  const portSuffix = port ? `:${port}` : "";
@@ -522,6 +528,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
522
528
  if (scenarioId) {
523
529
  url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
524
530
  }
531
+ for (const [key, value] of Object.entries(query)) {
532
+ if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
533
+ url.searchParams.set(key, value);
534
+ }
535
+ }
525
536
  return url.toString();
526
537
  };
527
538
  var isVoiceServerMessage = (value) => {
@@ -631,7 +642,7 @@ var createVoiceConnection = (path, options = {}) => {
631
642
  }, delayMs);
632
643
  };
633
644
  const connect = () => {
634
- const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
645
+ const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
635
646
  ws.binaryType = "arraybuffer";
636
647
  ws.onopen = () => {
637
648
  const wasReconnecting = state.reconnectAttempts > 0;
@@ -727,7 +738,19 @@ var createVoiceConnection = (path, options = {}) => {
727
738
  type: "call_control"
728
739
  });
729
740
  };
730
- const close = () => {
741
+ const close = (reason = "client-close") => {
742
+ clearTimers();
743
+ if (state.ws) {
744
+ if (state.ws.readyState === WS_OPEN) {
745
+ state.ws.send(JSON.stringify({ reason, type: "close" }));
746
+ }
747
+ state.ws.close(WS_NORMAL_CLOSURE);
748
+ state.ws = null;
749
+ }
750
+ state.isConnected = false;
751
+ listeners.clear();
752
+ };
753
+ const disconnect = () => {
731
754
  clearTimers();
732
755
  if (state.ws) {
733
756
  state.ws.close(WS_NORMAL_CLOSURE);
@@ -751,6 +774,7 @@ var createVoiceConnection = (path, options = {}) => {
751
774
  return {
752
775
  callControl,
753
776
  close,
777
+ disconnect,
754
778
  endTurn,
755
779
  send,
756
780
  sendAudio,
@@ -1031,10 +1055,17 @@ var createVoiceStream = (path, options = {}) => {
1031
1055
  callControl(message) {
1032
1056
  connection.callControl(message);
1033
1057
  },
1034
- close() {
1058
+ close(reason) {
1035
1059
  unsubscribeConnection();
1036
1060
  browserMediaReporter?.close();
1037
- connection.close();
1061
+ connection.close(reason);
1062
+ store.dispatch({ type: "disconnected" });
1063
+ notify();
1064
+ },
1065
+ disconnect() {
1066
+ unsubscribeConnection();
1067
+ browserMediaReporter?.close();
1068
+ connection.disconnect();
1038
1069
  store.dispatch({ type: "disconnected" });
1039
1070
  notify();
1040
1071
  },
@@ -1479,6 +1510,7 @@ var createVoiceController = (path, options = {}) => {
1479
1510
  };
1480
1511
  notify();
1481
1512
  } catch (error) {
1513
+ capture?.stop();
1482
1514
  capture = null;
1483
1515
  state = {
1484
1516
  ...state,
@@ -1489,13 +1521,19 @@ var createVoiceController = (path, options = {}) => {
1489
1521
  throw error;
1490
1522
  }
1491
1523
  };
1492
- const close = () => {
1524
+ const close = (reason) => {
1525
+ unsubscribeStream();
1526
+ stopRecording();
1527
+ stream.close(reason);
1528
+ };
1529
+ const disconnect = () => {
1493
1530
  unsubscribeStream();
1494
1531
  stopRecording();
1495
- stream.close();
1532
+ stream.disconnect();
1496
1533
  };
1497
1534
  return {
1498
1535
  close,
1536
+ disconnect,
1499
1537
  startRecording,
1500
1538
  stopRecording,
1501
1539
  get assistantAudio() {
@@ -115,6 +115,7 @@ var noopUnsubscribe = () => noop;
115
115
  var NOOP_CONNECTION = {
116
116
  callControl: noop,
117
117
  close: noop,
118
+ disconnect: noop,
118
119
  endTurn: noop,
119
120
  send: noop,
120
121
  sendAudio: noop,
@@ -126,7 +127,7 @@ var NOOP_CONNECTION = {
126
127
  start: () => {}
127
128
  };
128
129
  var createSessionId = () => crypto.randomUUID();
129
- var buildWsUrl = (path, sessionId, scenarioId) => {
130
+ var buildWsUrl = (path, sessionId, scenarioId, query) => {
130
131
  const { hostname, port, protocol } = window.location;
131
132
  const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
132
133
  const portSuffix = port ? `:${port}` : "";
@@ -135,6 +136,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
135
136
  if (scenarioId) {
136
137
  url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
137
138
  }
139
+ for (const [key, value] of Object.entries(query)) {
140
+ if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
141
+ url.searchParams.set(key, value);
142
+ }
143
+ }
138
144
  return url.toString();
139
145
  };
140
146
  var isVoiceServerMessage = (value) => {
@@ -244,7 +250,7 @@ var createVoiceConnection = (path, options = {}) => {
244
250
  }, delayMs);
245
251
  };
246
252
  const connect = () => {
247
- const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
253
+ const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
248
254
  ws.binaryType = "arraybuffer";
249
255
  ws.onopen = () => {
250
256
  const wasReconnecting = state.reconnectAttempts > 0;
@@ -340,7 +346,19 @@ var createVoiceConnection = (path, options = {}) => {
340
346
  type: "call_control"
341
347
  });
342
348
  };
343
- const close = () => {
349
+ const close = (reason = "client-close") => {
350
+ clearTimers();
351
+ if (state.ws) {
352
+ if (state.ws.readyState === WS_OPEN) {
353
+ state.ws.send(JSON.stringify({ reason, type: "close" }));
354
+ }
355
+ state.ws.close(WS_NORMAL_CLOSURE);
356
+ state.ws = null;
357
+ }
358
+ state.isConnected = false;
359
+ listeners.clear();
360
+ };
361
+ const disconnect = () => {
344
362
  clearTimers();
345
363
  if (state.ws) {
346
364
  state.ws.close(WS_NORMAL_CLOSURE);
@@ -364,6 +382,7 @@ var createVoiceConnection = (path, options = {}) => {
364
382
  return {
365
383
  callControl,
366
384
  close,
385
+ disconnect,
367
386
  endTurn,
368
387
  send,
369
388
  sendAudio,
@@ -1563,10 +1582,17 @@ var createVoiceStream = (path, options = {}) => {
1563
1582
  callControl(message) {
1564
1583
  connection.callControl(message);
1565
1584
  },
1566
- close() {
1585
+ close(reason) {
1586
+ unsubscribeConnection();
1587
+ browserMediaReporter?.close();
1588
+ connection.close(reason);
1589
+ store.dispatch({ type: "disconnected" });
1590
+ notify();
1591
+ },
1592
+ disconnect() {
1567
1593
  unsubscribeConnection();
1568
1594
  browserMediaReporter?.close();
1569
- connection.close();
1595
+ connection.disconnect();
1570
1596
  store.dispatch({ type: "disconnected" });
1571
1597
  notify();
1572
1598
  },
@@ -1730,29 +1756,34 @@ var createMicrophoneCapture = (options) => {
1730
1756
  if (!AudioContextCtor) {
1731
1757
  throw new Error("Browser microphone capture requires AudioContext support.");
1732
1758
  }
1733
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
1734
- audio: {
1735
- autoGainControl: true,
1736
- channelCount: options.channelCount ?? 1,
1737
- echoCancellation: true,
1738
- noiseSuppression: true
1759
+ try {
1760
+ mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
1761
+ audio: {
1762
+ autoGainControl: true,
1763
+ channelCount: options.channelCount ?? 1,
1764
+ echoCancellation: true,
1765
+ noiseSuppression: true
1766
+ }
1767
+ });
1768
+ audioContext = new AudioContextCtor;
1769
+ if (audioContext.state === "suspended") {
1770
+ await audioContext.resume();
1739
1771
  }
1740
- });
1741
- audioContext = new AudioContextCtor;
1742
- if (audioContext.state === "suspended") {
1743
- await audioContext.resume();
1744
- }
1745
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
1746
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
1747
- processorNode.onaudioprocess = (event) => {
1748
- const channel = event.inputBuffer.getChannelData(0);
1749
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
1750
- const pcm = floatTo16BitPCM(downsampled);
1751
- options.onLevel?.(getPcmLevel(pcm));
1752
- options.onAudio(pcm);
1753
- };
1754
- sourceNode.connect(processorNode);
1755
- processorNode.connect(audioContext.destination);
1772
+ sourceNode = audioContext.createMediaStreamSource(mediaStream);
1773
+ processorNode = audioContext.createScriptProcessor(4096, 1, 1);
1774
+ processorNode.onaudioprocess = (event) => {
1775
+ const channel = event.inputBuffer.getChannelData(0);
1776
+ const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
1777
+ const pcm = floatTo16BitPCM(downsampled);
1778
+ options.onLevel?.(getPcmLevel(pcm));
1779
+ options.onAudio(pcm);
1780
+ };
1781
+ sourceNode.connect(processorNode);
1782
+ processorNode.connect(audioContext.destination);
1783
+ } catch (error) {
1784
+ stop();
1785
+ throw error;
1786
+ }
1756
1787
  };
1757
1788
  const stop = () => {
1758
1789
  processorNode?.disconnect();
@@ -2195,6 +2226,7 @@ var createVoiceController = (path, options = {}) => {
2195
2226
  };
2196
2227
  notify();
2197
2228
  } catch (error) {
2229
+ capture?.stop();
2198
2230
  capture = null;
2199
2231
  state = {
2200
2232
  ...state,
@@ -2205,13 +2237,19 @@ var createVoiceController = (path, options = {}) => {
2205
2237
  throw error;
2206
2238
  }
2207
2239
  };
2208
- const close = () => {
2240
+ const close = (reason) => {
2241
+ unsubscribeStream();
2242
+ stopRecording();
2243
+ stream.close(reason);
2244
+ };
2245
+ const disconnect = () => {
2209
2246
  unsubscribeStream();
2210
2247
  stopRecording();
2211
- stream.close();
2248
+ stream.disconnect();
2212
2249
  };
2213
2250
  return {
2214
2251
  close,
2252
+ disconnect,
2215
2253
  startRecording,
2216
2254
  stopRecording,
2217
2255
  get assistantAudio() {
@@ -871,6 +871,19 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
871
871
  };
872
872
  costTelemetry?: VoiceCostTelemetryConfig<TContext, TSession, TResult>;
873
873
  path: string;
874
+ /**
875
+ * Authorize a WebSocket before the plugin supersedes an existing session or
876
+ * creates provider-backed resources. Return false to close it with 4401.
877
+ * Applications can validate a short-lived admission credential from `query`
878
+ * without coupling Voice to a particular authentication implementation.
879
+ */
880
+ authorizeConnection?: (input: {
881
+ context: TContext;
882
+ path: string;
883
+ query: Record<string, unknown>;
884
+ scenarioId?: string;
885
+ sessionId: string;
886
+ }) => boolean | Promise<boolean>;
874
887
  greeting?: string | ((input: {
875
888
  session: TSession;
876
889
  }) => string | Promise<string>);
@@ -1331,6 +1344,8 @@ export type VoiceConnectionOptions = {
1331
1344
  * the caller losing the call. */
1332
1345
  reconnectMaxDelayMs?: number;
1333
1346
  pingInterval?: number;
1347
+ /** Additional query values sent on every initial or reconnecting socket. */
1348
+ query?: Record<string, string>;
1334
1349
  sessionId?: string;
1335
1350
  };
1336
1351
  export type VoiceBrowserMediaReportPayload = {
@@ -1502,7 +1517,9 @@ export type VoiceStreamState<TResult = unknown> = {
1502
1517
  export type VoiceStream<TResult = unknown> = {
1503
1518
  call: VoiceCallLifecycleState | null;
1504
1519
  callControl: (message: Omit<VoiceClientCallControlMessage, "type">) => void;
1505
- close: () => void;
1520
+ close: (reason?: string) => void;
1521
+ /** Release the transport without terminally completing the server session. */
1522
+ disconnect: () => void;
1506
1523
  start: (input?: {
1507
1524
  scenarioId?: string;
1508
1525
  sessionId?: string;
@@ -1597,7 +1614,9 @@ export type VoiceController<TResult = unknown> = {
1597
1614
  bindHTMX: (options: VoiceHTMXBindingOptions) => () => void;
1598
1615
  call: VoiceCallLifecycleState | null;
1599
1616
  callControl: (message: Omit<VoiceClientCallControlMessage, "type">) => void;
1600
- close: () => void;
1617
+ close: (reason?: string) => void;
1618
+ /** Release the transport while leaving the server session resumable. */
1619
+ disconnect: () => void;
1601
1620
  endTurn: () => void;
1602
1621
  start: (input?: {
1603
1622
  scenarioId?: string;