@absolutejs/voice 0.0.22-beta.586 → 0.0.22-beta.588

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.
@@ -574,9 +574,11 @@ var createVoiceBrowserMediaReporter = (options) => {
574
574
  var WS_OPEN = 1;
575
575
  var WS_CLOSED = 3;
576
576
  var WS_NORMAL_CLOSURE = 1000;
577
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
577
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
578
578
  var DEFAULT_PING_INTERVAL = 30000;
579
- var RECONNECT_DELAY_MS = 500;
579
+ var RECONNECT_BASE_DELAY_MS = 500;
580
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
581
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
580
582
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
581
583
  var noop = () => {};
582
584
  var noopUnsubscribe = () => noop;
@@ -645,7 +647,9 @@ var createVoiceConnection = (path, options = {}) => {
645
647
  const listeners = new Set;
646
648
  const shouldReconnect = options.reconnect !== false;
647
649
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
650
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
648
651
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
652
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
649
653
  const state = {
650
654
  isConnected: false,
651
655
  pendingMessages: [],
@@ -681,8 +685,9 @@ var createVoiceConnection = (path, options = {}) => {
681
685
  }
682
686
  };
683
687
  const scheduleReconnect = () => {
684
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
685
688
  state.reconnectAttempts += 1;
689
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
690
+ const nextAttemptAt = Date.now() + delayMs;
686
691
  emitConnection({
687
692
  reconnect: {
688
693
  attempts: state.reconnectAttempts,
@@ -706,7 +711,7 @@ var createVoiceConnection = (path, options = {}) => {
706
711
  return;
707
712
  }
708
713
  connect();
709
- }, RECONNECT_DELAY_MS);
714
+ }, delayMs);
710
715
  };
711
716
  const connect = () => {
712
717
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -847,6 +852,27 @@ var createInitialReconnectState = () => ({
847
852
  maxAttempts: 0,
848
853
  status: "idle"
849
854
  });
855
+ var appendSegmentText = (accumulated, next) => {
856
+ const nextText = next.trim().replace(/\s+/g, " ");
857
+ if (!nextText)
858
+ return accumulated;
859
+ if (!accumulated)
860
+ return nextText;
861
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
862
+ return accumulated;
863
+ }
864
+ if (nextText.includes(accumulated))
865
+ return nextText;
866
+ return `${accumulated} ${nextText}`;
867
+ };
868
+ var joinPartial = (finalized, interim) => {
869
+ const interimText = interim.trim().replace(/\s+/g, " ");
870
+ if (!finalized)
871
+ return interimText;
872
+ if (!interimText || finalized.endsWith(interimText))
873
+ return finalized;
874
+ return `${finalized} ${interimText}`;
875
+ };
850
876
  var createInitialState = () => ({
851
877
  assistantAudio: [],
852
878
  assistantStreamingText: "",
@@ -864,6 +890,7 @@ var createInitialState = () => ({
864
890
  });
865
891
  var createVoiceStreamStore = () => {
866
892
  let state = createInitialState();
893
+ let turnFinalText = "";
867
894
  const subscribers = new Set;
868
895
  const notify = () => {
869
896
  subscribers.forEach((subscriber) => subscriber());
@@ -949,19 +976,20 @@ var createVoiceStreamStore = () => {
949
976
  };
950
977
  break;
951
978
  case "final":
979
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
952
980
  state = {
953
981
  ...state,
954
- partial: action.transcript.text,
955
- turns: state.turns.map((turn) => turn)
982
+ partial: turnFinalText
956
983
  };
957
984
  break;
958
985
  case "partial":
959
986
  state = {
960
987
  ...state,
961
- partial: action.transcript.text
988
+ partial: joinPartial(turnFinalText, action.transcript.text)
962
989
  };
963
990
  break;
964
991
  case "replay":
992
+ turnFinalText = action.partial;
965
993
  state = {
966
994
  ...state,
967
995
  assistantStreamingText: "",
@@ -995,6 +1023,7 @@ var createVoiceStreamStore = () => {
995
1023
  };
996
1024
  break;
997
1025
  case "turn":
1026
+ turnFinalText = "";
998
1027
  state = {
999
1028
  ...state,
1000
1029
  partial: "",
@@ -1,4 +1,8 @@
1
1
  import type { VoiceClientMessage, VoiceConnectionOptions, VoiceServerMessage } from "../core/types";
2
+ /** Exponential reconnect backoff for attempt N (1-based): baseMs doubles each
3
+ * attempt, capped at maxDelayMs. Exported so the backoff window is unit-tested
4
+ * without a DOM/WebSocket harness. */
5
+ export declare const computeVoiceReconnectDelayMs: (attempt: number, baseMs: number, maxDelayMs: number) => number;
2
6
  type VoiceConnectionHandle = {
3
7
  callControl: (message: Omit<VoiceClientMessage & {
4
8
  type: "call_control";
@@ -491,9 +491,11 @@ var createVoiceBrowserMediaReporter = (options) => {
491
491
  var WS_OPEN = 1;
492
492
  var WS_CLOSED = 3;
493
493
  var WS_NORMAL_CLOSURE = 1000;
494
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
494
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
495
495
  var DEFAULT_PING_INTERVAL = 30000;
496
- var RECONNECT_DELAY_MS = 500;
496
+ var RECONNECT_BASE_DELAY_MS = 500;
497
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
498
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
497
499
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
498
500
  var noop = () => {};
499
501
  var noopUnsubscribe = () => noop;
@@ -562,7 +564,9 @@ var createVoiceConnection = (path, options = {}) => {
562
564
  const listeners = new Set;
563
565
  const shouldReconnect = options.reconnect !== false;
564
566
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
567
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
565
568
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
569
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
566
570
  const state = {
567
571
  isConnected: false,
568
572
  pendingMessages: [],
@@ -598,8 +602,9 @@ var createVoiceConnection = (path, options = {}) => {
598
602
  }
599
603
  };
600
604
  const scheduleReconnect = () => {
601
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
602
605
  state.reconnectAttempts += 1;
606
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
607
+ const nextAttemptAt = Date.now() + delayMs;
603
608
  emitConnection({
604
609
  reconnect: {
605
610
  attempts: state.reconnectAttempts,
@@ -623,7 +628,7 @@ var createVoiceConnection = (path, options = {}) => {
623
628
  return;
624
629
  }
625
630
  connect();
626
- }, RECONNECT_DELAY_MS);
631
+ }, delayMs);
627
632
  };
628
633
  const connect = () => {
629
634
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -764,6 +769,27 @@ var createInitialReconnectState = () => ({
764
769
  maxAttempts: 0,
765
770
  status: "idle"
766
771
  });
772
+ var appendSegmentText = (accumulated, next) => {
773
+ const nextText = next.trim().replace(/\s+/g, " ");
774
+ if (!nextText)
775
+ return accumulated;
776
+ if (!accumulated)
777
+ return nextText;
778
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
779
+ return accumulated;
780
+ }
781
+ if (nextText.includes(accumulated))
782
+ return nextText;
783
+ return `${accumulated} ${nextText}`;
784
+ };
785
+ var joinPartial = (finalized, interim) => {
786
+ const interimText = interim.trim().replace(/\s+/g, " ");
787
+ if (!finalized)
788
+ return interimText;
789
+ if (!interimText || finalized.endsWith(interimText))
790
+ return finalized;
791
+ return `${finalized} ${interimText}`;
792
+ };
767
793
  var createInitialState = () => ({
768
794
  assistantAudio: [],
769
795
  assistantStreamingText: "",
@@ -781,6 +807,7 @@ var createInitialState = () => ({
781
807
  });
782
808
  var createVoiceStreamStore = () => {
783
809
  let state = createInitialState();
810
+ let turnFinalText = "";
784
811
  const subscribers = new Set;
785
812
  const notify = () => {
786
813
  subscribers.forEach((subscriber) => subscriber());
@@ -866,19 +893,20 @@ var createVoiceStreamStore = () => {
866
893
  };
867
894
  break;
868
895
  case "final":
896
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
869
897
  state = {
870
898
  ...state,
871
- partial: action.transcript.text,
872
- turns: state.turns.map((turn) => turn)
899
+ partial: turnFinalText
873
900
  };
874
901
  break;
875
902
  case "partial":
876
903
  state = {
877
904
  ...state,
878
- partial: action.transcript.text
905
+ partial: joinPartial(turnFinalText, action.transcript.text)
879
906
  };
880
907
  break;
881
908
  case "replay":
909
+ turnFinalText = action.partial;
882
910
  state = {
883
911
  ...state,
884
912
  assistantStreamingText: "",
@@ -912,6 +940,7 @@ var createVoiceStreamStore = () => {
912
940
  };
913
941
  break;
914
942
  case "turn":
943
+ turnFinalText = "";
915
944
  state = {
916
945
  ...state,
917
946
  partial: "",
@@ -104,9 +104,11 @@ var voiceSseReactiveSource = (topic, options = {}) => (refresh) => {
104
104
  var WS_OPEN = 1;
105
105
  var WS_CLOSED = 3;
106
106
  var WS_NORMAL_CLOSURE = 1000;
107
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
107
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
108
108
  var DEFAULT_PING_INTERVAL = 30000;
109
- var RECONNECT_DELAY_MS = 500;
109
+ var RECONNECT_BASE_DELAY_MS = 500;
110
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
111
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
110
112
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
111
113
  var noop = () => {};
112
114
  var noopUnsubscribe = () => noop;
@@ -175,7 +177,9 @@ var createVoiceConnection = (path, options = {}) => {
175
177
  const listeners = new Set;
176
178
  const shouldReconnect = options.reconnect !== false;
177
179
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
180
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
178
181
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
182
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
179
183
  const state = {
180
184
  isConnected: false,
181
185
  pendingMessages: [],
@@ -211,8 +215,9 @@ var createVoiceConnection = (path, options = {}) => {
211
215
  }
212
216
  };
213
217
  const scheduleReconnect = () => {
214
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
215
218
  state.reconnectAttempts += 1;
219
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
220
+ const nextAttemptAt = Date.now() + delayMs;
216
221
  emitConnection({
217
222
  reconnect: {
218
223
  attempts: state.reconnectAttempts,
@@ -236,7 +241,7 @@ var createVoiceConnection = (path, options = {}) => {
236
241
  return;
237
242
  }
238
243
  connect();
239
- }, RECONNECT_DELAY_MS);
244
+ }, delayMs);
240
245
  };
241
246
  const connect = () => {
242
247
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -1151,6 +1156,27 @@ var createInitialReconnectState = () => ({
1151
1156
  maxAttempts: 0,
1152
1157
  status: "idle"
1153
1158
  });
1159
+ var appendSegmentText = (accumulated, next) => {
1160
+ const nextText = next.trim().replace(/\s+/g, " ");
1161
+ if (!nextText)
1162
+ return accumulated;
1163
+ if (!accumulated)
1164
+ return nextText;
1165
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
1166
+ return accumulated;
1167
+ }
1168
+ if (nextText.includes(accumulated))
1169
+ return nextText;
1170
+ return `${accumulated} ${nextText}`;
1171
+ };
1172
+ var joinPartial = (finalized, interim) => {
1173
+ const interimText = interim.trim().replace(/\s+/g, " ");
1174
+ if (!finalized)
1175
+ return interimText;
1176
+ if (!interimText || finalized.endsWith(interimText))
1177
+ return finalized;
1178
+ return `${finalized} ${interimText}`;
1179
+ };
1154
1180
  var createInitialState2 = () => ({
1155
1181
  assistantAudio: [],
1156
1182
  assistantStreamingText: "",
@@ -1168,6 +1194,7 @@ var createInitialState2 = () => ({
1168
1194
  });
1169
1195
  var createVoiceStreamStore = () => {
1170
1196
  let state = createInitialState2();
1197
+ let turnFinalText = "";
1171
1198
  const subscribers = new Set;
1172
1199
  const notify = () => {
1173
1200
  subscribers.forEach((subscriber) => subscriber());
@@ -1253,19 +1280,20 @@ var createVoiceStreamStore = () => {
1253
1280
  };
1254
1281
  break;
1255
1282
  case "final":
1283
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
1256
1284
  state = {
1257
1285
  ...state,
1258
- partial: action.transcript.text,
1259
- turns: state.turns.map((turn) => turn)
1286
+ partial: turnFinalText
1260
1287
  };
1261
1288
  break;
1262
1289
  case "partial":
1263
1290
  state = {
1264
1291
  ...state,
1265
- partial: action.transcript.text
1292
+ partial: joinPartial(turnFinalText, action.transcript.text)
1266
1293
  };
1267
1294
  break;
1268
1295
  case "replay":
1296
+ turnFinalText = action.partial;
1269
1297
  state = {
1270
1298
  ...state,
1271
1299
  assistantStreamingText: "",
@@ -1299,6 +1327,7 @@ var createVoiceStreamStore = () => {
1299
1327
  };
1300
1328
  break;
1301
1329
  case "turn":
1330
+ turnFinalText = "";
1302
1331
  state = {
1303
1332
  ...state,
1304
1333
  partial: "",
@@ -1113,6 +1113,12 @@ export type VoiceConnectionOptions = {
1113
1113
  reconnect?: boolean;
1114
1114
  reconnectReportPath?: string;
1115
1115
  maxReconnectAttempts?: number;
1116
+ /** Cap on the exponential reconnect backoff (ms). The delay doubles from 500ms
1117
+ * up to this ceiling each attempt, so the total retry window is roughly
1118
+ * maxReconnectAttempts spread across it. Default 8000 — with the default 15
1119
+ * attempts that's a ~95s window, enough to ride out a server redeploy without
1120
+ * the caller losing the call. */
1121
+ reconnectMaxDelayMs?: number;
1116
1122
  pingInterval?: number;
1117
1123
  sessionId?: string;
1118
1124
  };
@@ -0,0 +1,41 @@
1
+ import type { VoiceLogger, VoiceSessionRecord, VoiceSessionStore } from "./types";
2
+ export type VoiceWriteBehindStore<TSession extends VoiceSessionRecord = VoiceSessionRecord> = VoiceSessionStore<TSession> & {
3
+ /** Force every pending snapshot out to the persistent store. Call on graceful
4
+ * shutdown (after the deploy drain) so an in-flight call survives the restart
5
+ * even if it was killed at the drain ceiling. Resolves once the persistent
6
+ * store has the latest snapshot of every dirty session. */
7
+ flush: () => Promise<void>;
8
+ /** Stop the debounce timer. Call after the final flush() on shutdown. */
9
+ dispose: () => void;
10
+ };
11
+ export type VoiceWriteBehindStoreOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = {
12
+ /** The durable store snapshots are written back to (Postgres / SQLite / file).
13
+ * This is what a fresh process reads from to resume a call after a restart. */
14
+ persistent: VoiceSessionStore<TSession>;
15
+ /** The synchronous hot-path store. Defaults to an in-memory Map store — the
16
+ * config that keeps up with high-frequency STT callbacks without dropping
17
+ * transcripts. Override only for tests. */
18
+ memory?: VoiceSessionStore<TSession>;
19
+ /** How long a write may sit in memory before it's flushed to the persistent
20
+ * store. Bounds the worst-case state lost on a hard crash. Default 750ms. A
21
+ * turn commit (turns array grows) bypasses this and flushes immediately. */
22
+ flushDebounceMs?: number;
23
+ logger?: VoiceLogger;
24
+ };
25
+ /**
26
+ * A session store that is fast AND durable.
27
+ *
28
+ * The in-memory store is authoritative for the live call: get/set are
29
+ * synchronous, so the hot path (STT partials/finals firing every ~100ms) never
30
+ * waits on the database and concurrent writes can't race. Every write is ALSO
31
+ * recorded as a pending snapshot and flushed to the persistent store on a
32
+ * debounce (coalescing a burst of partials into ~one DB write), with a turn
33
+ * commit flushing immediately so completed turns are durable right away.
34
+ *
35
+ * On a cold `get`/`getOrCreate` miss — e.g. a fresh process after a deploy — it
36
+ * hydrates the session from the persistent store and repopulates memory. That's
37
+ * the resume path: the client reconnects with the same session id, the server
38
+ * finds the persisted session, so it does NOT re-fire the greeting and replays
39
+ * the prior turns instead of starting over.
40
+ */
41
+ export declare const createVoiceWriteBehindStore: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceWriteBehindStoreOptions<TSession>) => VoiceWriteBehindStore<TSession>;
@@ -488,9 +488,11 @@ var createVoiceBrowserMediaReporter = (options) => {
488
488
  var WS_OPEN = 1;
489
489
  var WS_CLOSED = 3;
490
490
  var WS_NORMAL_CLOSURE = 1000;
491
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
491
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
492
492
  var DEFAULT_PING_INTERVAL = 30000;
493
- var RECONNECT_DELAY_MS = 500;
493
+ var RECONNECT_BASE_DELAY_MS = 500;
494
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
495
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
494
496
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
495
497
  var noop = () => {};
496
498
  var noopUnsubscribe = () => noop;
@@ -559,7 +561,9 @@ var createVoiceConnection = (path, options = {}) => {
559
561
  const listeners = new Set;
560
562
  const shouldReconnect = options.reconnect !== false;
561
563
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
564
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
562
565
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
566
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
563
567
  const state = {
564
568
  isConnected: false,
565
569
  pendingMessages: [],
@@ -595,8 +599,9 @@ var createVoiceConnection = (path, options = {}) => {
595
599
  }
596
600
  };
597
601
  const scheduleReconnect = () => {
598
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
599
602
  state.reconnectAttempts += 1;
603
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
604
+ const nextAttemptAt = Date.now() + delayMs;
600
605
  emitConnection({
601
606
  reconnect: {
602
607
  attempts: state.reconnectAttempts,
@@ -620,7 +625,7 @@ var createVoiceConnection = (path, options = {}) => {
620
625
  return;
621
626
  }
622
627
  connect();
623
- }, RECONNECT_DELAY_MS);
628
+ }, delayMs);
624
629
  };
625
630
  const connect = () => {
626
631
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -761,6 +766,27 @@ var createInitialReconnectState = () => ({
761
766
  maxAttempts: 0,
762
767
  status: "idle"
763
768
  });
769
+ var appendSegmentText = (accumulated, next) => {
770
+ const nextText = next.trim().replace(/\s+/g, " ");
771
+ if (!nextText)
772
+ return accumulated;
773
+ if (!accumulated)
774
+ return nextText;
775
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
776
+ return accumulated;
777
+ }
778
+ if (nextText.includes(accumulated))
779
+ return nextText;
780
+ return `${accumulated} ${nextText}`;
781
+ };
782
+ var joinPartial = (finalized, interim) => {
783
+ const interimText = interim.trim().replace(/\s+/g, " ");
784
+ if (!finalized)
785
+ return interimText;
786
+ if (!interimText || finalized.endsWith(interimText))
787
+ return finalized;
788
+ return `${finalized} ${interimText}`;
789
+ };
764
790
  var createInitialState = () => ({
765
791
  assistantAudio: [],
766
792
  assistantStreamingText: "",
@@ -778,6 +804,7 @@ var createInitialState = () => ({
778
804
  });
779
805
  var createVoiceStreamStore = () => {
780
806
  let state = createInitialState();
807
+ let turnFinalText = "";
781
808
  const subscribers = new Set;
782
809
  const notify = () => {
783
810
  subscribers.forEach((subscriber) => subscriber());
@@ -863,19 +890,20 @@ var createVoiceStreamStore = () => {
863
890
  };
864
891
  break;
865
892
  case "final":
893
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
866
894
  state = {
867
895
  ...state,
868
- partial: action.transcript.text,
869
- turns: state.turns.map((turn) => turn)
896
+ partial: turnFinalText
870
897
  };
871
898
  break;
872
899
  case "partial":
873
900
  state = {
874
901
  ...state,
875
- partial: action.transcript.text
902
+ partial: joinPartial(turnFinalText, action.transcript.text)
876
903
  };
877
904
  break;
878
905
  case "replay":
906
+ turnFinalText = action.partial;
879
907
  state = {
880
908
  ...state,
881
909
  assistantStreamingText: "",
@@ -909,6 +937,7 @@ var createVoiceStreamStore = () => {
909
937
  };
910
938
  break;
911
939
  case "turn":
940
+ turnFinalText = "";
912
941
  state = {
913
942
  ...state,
914
943
  partial: "",
@@ -1,10 +1,10 @@
1
- (()=>{var{defineProperty:Q,getOwnPropertyNames:Ic,getOwnPropertyDescriptor:Vc}=Object,Sc=Object.prototype.hasOwnProperty;function Rc(c){return this[c]}var yc=(c)=>{var g=(z??=new WeakMap).get(c),C;if(g)return g;if(g=Q({},"__esModule",{value:!0}),c&&typeof c==="object"||typeof c==="function"){for(var A of Ic(c))if(!Sc.call(g,A))Q(g,A,{get:Rc.bind(c,A),enumerable:!(C=Vc(c,A))||C.enumerable})}return z.set(c,g),g},z;var _c=(c)=>c;function Tc(c,g){this[c]=_c.bind(null,g)}var wc=(c,g)=>{for(var C in g)Q(c,C,{get:g[C],enumerable:!0,configurable:!0,set:Tc.bind(g,C)})};var Fc={};wc(Fc,{mount:()=>cc,default:()=>oc,VOICE_EMBED_VERSION:()=>gc});var hc=(c)=>{if(typeof c!=="string")return c;return document.querySelector(c)},Lc=(c,g,C,A)=>{let I=g??c.getAttribute("hx-get")??"";if(!I)return"";let R=new URL(I,window.location.origin);if(A)R.searchParams.set(C,A);else R.searchParams.delete(C);return`${R.pathname}${R.search}${R.hash}`},B=(c,g)=>{if(typeof window>"u"||typeof document>"u")return()=>{};let C=hc(g.element);if(!C)return()=>{};let A=g.eventName??"voice-refresh",I=g.sessionQueryParam??"sessionId",R=()=>{let T=window,w=Lc(C,g.route,I,c.sessionId);if(w)C.setAttribute("hx-get",w);T.htmx?.process?.(C),T.htmx?.trigger?.(C,A)},S=c.subscribe(R);return R(),()=>{S()}};var Uc=(c)=>Math.max(-1,Math.min(1,c)),$c=(c)=>{let g=new Int16Array(c.length);for(let C=0;C<c.length;C+=1){let A=Uc(c[C]??0);g[C]=A<0?A*32768:A*32767}return new Uint8Array(g.buffer)},nc=(c)=>{let g=c instanceof Uint8Array?c:new Uint8Array(c);if(g.byteLength<2)return 0;let C=new Int16Array(g.buffer,g.byteOffset,Math.floor(g.byteLength/2));if(C.length===0)return 0;let A=0;for(let I of C){let R=I/32768;A+=R*R}return Math.min(1,Math.max(0,Math.sqrt(A/C.length)*5.5))},Oc=(c,g,C)=>{if(g===C)return c;let A=g/C,I=Math.round(c.length/A),R=new Float32Array(I),S=0,T=0;while(S<R.length){let w=Math.round((S+1)*A),U=0,_=0;for(let L=T;L<w&&L<c.length;L+=1)U+=c[L]??0,_+=1;R[S]=_>0?U/_:0,S+=1,T=w}return R},j=(c)=>{let g=null,C=null,A=null,I=null;return{start:async()=>{if(!c.stream&&(typeof navigator>"u"||!navigator.mediaDevices?.getUserMedia))throw Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");let T=(typeof window<"u"?window.AudioContext??window.webkitAudioContext:void 0)??AudioContext;if(!T)throw Error("Browser microphone capture requires AudioContext support.");if(I=c.stream??await navigator.mediaDevices.getUserMedia({audio:{autoGainControl:!0,channelCount:c.channelCount??1,echoCancellation:!0,noiseSuppression:!0}}),g=new T,g.state==="suspended")await g.resume();C=g.createMediaStreamSource(I),A=g.createScriptProcessor(4096,1,1),A.onaudioprocess=(w)=>{let U=w.inputBuffer.getChannelData(0),_=Oc(U,g?.sampleRate??48000,c.sampleRateHz??16000),L=$c(_);c.onLevel?.(nc(L)),c.onAudio(L)},C.connect(A),A.connect(g.destination)},stop:()=>{A?.disconnect(),C?.disconnect(),I?.getTracks().forEach((T)=>T.stop()),g?.close(),c.onLevel?.(0),g=null,I=null,A=null,C=null}}};var K=(c)=>{if(typeof c==="string"&&c.trim())return c;if(c instanceof Error&&c.message.trim())return c.message;if(c&&typeof c==="object"){let g=c;for(let C of["message","reason","description"]){let A=g[C];if(typeof A==="string"&&A.trim())return A}if("error"in g)return K(g.error);if("cause"in g)return K(g.cause);try{return JSON.stringify(c)}catch{}}return"Unexpected error"},d=(c)=>{switch(c.type){case"audio":return{chunk:Uint8Array.from(atob(c.chunkBase64),(g)=>g.charCodeAt(0)),format:c.format,receivedAt:c.receivedAt,turnId:c.turnId,type:"audio"};case"assistant":return{text:c.text,turnId:c.turnId,type:"assistant"};case"assistant_delta":return{delta:c.delta,turnId:c.turnId,type:"assistant_delta"};case"complete":return{sessionId:c.sessionId,type:"complete"};case"connection":return{reconnect:c.reconnect,type:"connection"};case"call_lifecycle":return{event:c.event,sessionId:c.sessionId,type:"call_lifecycle"};case"error":return{message:K(c.message),type:"error"};case"final":return{transcript:c.transcript,type:"final"};case"partial":return{transcript:c.transcript,type:"partial"};case"replay":return{assistantTexts:c.assistantTexts,call:c.call,partial:c.partial,scenarioId:c.scenarioId,sessionId:c.sessionId,sessionMetadata:c.sessionMetadata,status:c.status,turns:c.turns,type:"replay"};case"session":return{sessionId:c.sessionId,sessionMetadata:c.sessionMetadata,scenarioId:c.scenarioId,status:c.status,type:"session"};case"turn":return{turn:c.turn,type:"turn"};default:return null}};var tc=Math.PI*2;var N=(c,g,C,A)=>{c.push({code:C,message:A,severity:g})};var bc=(c)=>c.length===0?void 0:c.reduce((g,C)=>g+C,0)/c.length,J=(c)=>c.length===0?void 0:Math.max(...c);var n=(c,g)=>{let C=c[g];return typeof C==="number"&&Number.isFinite(C)?C:void 0},Y=(c,g)=>{let C=c[g];return typeof C==="boolean"?C:void 0},O=(c,g)=>{let C=c[g];return typeof C==="string"?C:void 0},q=(c)=>String(c.id??O(c,"ssrc")??n(c,"ssrc")??O(c,"trackIdentifier")??O(c,"mid")??"unknown"),k=(c)=>c===void 0?void 0:c*1000;var Dc=(c)=>{let g={};for(let[C,A]of Object.entries(c))if(A===null||typeof A==="boolean"||typeof A==="number"||typeof A==="string")g[C]=A;return g};var x=(c={})=>{let g=c.stats??[],C=[],A=g.filter((V)=>V.type==="inbound-rtp"&&O(V,"kind")!=="video"),I=g.filter((V)=>V.type==="outbound-rtp"&&O(V,"kind")!=="video"),R=g.filter((V)=>V.type==="candidate-pair"),S=g.filter((V)=>(V.type==="track"||V.type==="media-source")&&O(V,"kind")==="audio"),T=R.filter((V)=>Y(V,"selected")===!0||Y(V,"nominated")===!0||O(V,"state")==="succeeded").length,w=S.filter((V)=>O(V,"readyState")!=="ended"&&O(V,"trackState")!=="ended"&&Y(V,"ended")!==!0).length,U=S.filter((V)=>O(V,"readyState")==="ended"||O(V,"trackState")==="ended"||Y(V,"ended")===!0).length,_=A.reduce((V,$)=>V+(n($,"packetsReceived")??0),0),L=I.reduce((V,$)=>V+(n($,"packetsSent")??0),0),y=[...A,...I].reduce((V,$)=>V+Math.max(0,n($,"packetsLost")??0),0),b=_+y,h=b===0?0:y/b,E=A.reduce((V,$)=>V+(n($,"bytesReceived")??0),0),H=I.reduce((V,$)=>V+(n($,"bytesSent")??0),0),G=J(R.map((V)=>k(n(V,"currentRoundTripTime")??n(V,"roundTripTime"))).filter((V)=>V!==void 0)),M=J([...A,...I].map((V)=>k(n(V,"jitter"))).filter((V)=>V!==void 0)),X=J(A.map((V)=>{let $=n(V,"jitterBufferDelay"),D=n(V,"jitterBufferEmittedCount");return $!==void 0&&D!==void 0&&D>0?$/D*1000:void 0}).filter((V)=>V!==void 0)),P=S.map((V)=>n(V,"audioLevel")).filter((V)=>V!==void 0);if(c.requireConnectedCandidatePair&&R.length>0&&T===0)N(C,"error","media.webrtc_candidate_pair_missing","No active WebRTC candidate pair was observed.");if(c.requireLiveAudioTrack&&w===0)N(C,"error","media.webrtc_audio_track_missing","No live WebRTC audio track was observed.");if(c.maxPacketLossRatio!==void 0&&h>c.maxPacketLossRatio)N(C,"warning","media.webrtc_packet_loss",`Observed WebRTC packet loss ratio ${String(h)} above ${String(c.maxPacketLossRatio)}.`);if(c.maxRoundTripTimeMs!==void 0&&G!==void 0&&G>c.maxRoundTripTimeMs)N(C,"warning","media.webrtc_round_trip_time",`Observed WebRTC RTT ${String(G)}ms above ${String(c.maxRoundTripTimeMs)}ms.`);if(c.maxJitterMs!==void 0&&M!==void 0&&M>c.maxJitterMs)N(C,"warning","media.webrtc_jitter",`Observed WebRTC jitter ${String(M)}ms above ${String(c.maxJitterMs)}ms.`);return{activeCandidatePairs:T,audioLevelAverage:bc(P),bytesReceived:E,bytesSent:H,checkedAt:Date.now(),endedAudioTracks:U,inboundPackets:_,issues:C,jitterBufferDelayMs:X,jitterMs:M,liveAudioTracks:w,outboundPackets:L,packetLossRatio:h,packetsLost:y,roundTripTimeMs:G,status:C.some((V)=>V.severity==="error")?"fail":C.length>0?"warn":"pass",totalStats:g.length}},i=async(c)=>{return[...(await c.peerConnection.getStats(c.selector??null)).values()].map(Dc)};var f=(c={})=>{let g=c.stats??[],C=c.previousStats??[],A=[],I=new Map(C.map((y)=>[q(y),y])),S=g.filter((y)=>(y.type==="inbound-rtp"||y.type==="outbound-rtp")&&O(y,"kind")!=="video"&&O(y,"mediaType")!=="video").map((y)=>{let b=y.type==="outbound-rtp"?"outbound":"inbound",h=b==="outbound"?"packetsSent":"packetsReceived",E=b==="outbound"?"bytesSent":"bytesReceived",H=I.get(q(y)),G=n(y,h),M=H?n(H,h):void 0,X=n(y,E),P=H?n(H,E):void 0,V=y.timestamp!==void 0&&H?.timestamp!==void 0?y.timestamp-H.timestamp:void 0;return{bytesDelta:X!==void 0&&P!==void 0?X-P:void 0,currentPackets:G,direction:b,id:q(y),packetDelta:G!==void 0&&M!==void 0?G-M:void 0,previousPackets:M,timeDeltaMs:V}}),T=S.filter((y)=>y.direction==="inbound"),w=S.filter((y)=>y.direction==="outbound"),U=J(S.map((y)=>y.timeDeltaMs).filter((y)=>y!==void 0)),_=T.filter((y)=>c.maxInboundPacketStallMs!==void 0&&y.timeDeltaMs!==void 0&&y.timeDeltaMs>=c.maxInboundPacketStallMs&&y.packetDelta!==void 0&&y.packetDelta<=0).length,L=w.filter((y)=>c.maxOutboundPacketStallMs!==void 0&&y.timeDeltaMs!==void 0&&y.timeDeltaMs>=c.maxOutboundPacketStallMs&&y.packetDelta!==void 0&&y.packetDelta<=0).length;if(c.requireInboundAudio&&T.length===0)N(A,"error","media.webrtc_inbound_audio_missing","No inbound WebRTC audio RTP stream was observed.");if(c.requireOutboundAudio&&w.length===0)N(A,"error","media.webrtc_outbound_audio_missing","No outbound WebRTC audio RTP stream was observed.");if(c.maxGapMs!==void 0&&U!==void 0&&U>c.maxGapMs)N(A,"warning","media.webrtc_stream_gap",`Observed WebRTC stream sample gap ${String(U)}ms above ${String(c.maxGapMs)}ms.`);if(_>0)N(A,"error","media.webrtc_inbound_stalled",`${String(_)} inbound WebRTC audio stream(s) stopped receiving packets.`);if(L>0)N(A,"error","media.webrtc_outbound_stalled",`${String(L)} outbound WebRTC audio stream(s) stopped sending packets.`);return{checkedAt:Date.now(),inboundAudioStreams:T.length,issues:A,maxObservedGapMs:U,outboundAudioStreams:w.length,stalledInboundStreams:_,stalledOutboundStreams:L,status:A.some((y)=>y.severity==="error")?"fail":A.length>0?"warn":"pass",streams:S,totalStats:g.length}};var Hc="/api/voice/browser-media",Gc=5000,Mc=async(c)=>c.peerConnection??await c.getPeerConnection?.()??null,Nc=async(c,g)=>{let C=g.fetch??globalThis.fetch;if(!C)return;await C(g.path??Hc,{body:JSON.stringify(c),headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"})},F=(c)=>{let g=null,C=[],A=async()=>{let S=await Mc(c);if(!S)return;let T=await i({peerConnection:S}),w=x({...c,stats:T}),U=c.continuity===!1?void 0:f({...c.continuity,previousStats:C,stats:T}),_={at:Date.now(),continuity:U,report:w,scenarioId:c.getScenarioId?.()??null,sessionId:c.getSessionId?.()??null};return C=T,c.onReport?.(_),await Nc(_,c),_},I=()=>{A().catch((S)=>{c.onError?.(S)})},R=()=>{if(g)clearInterval(g),g=null};return{close:R,reportOnce:A,stop:R,start:()=>{if(g)return;I(),g=setInterval(I,c.intervalMs??Gc)}}};var W=()=>{},lc=()=>W,Wc={callControl:W,close:W,endTurn:W,send:W,sendAudio:W,simulateDisconnect:W,subscribe:lc,getReadyState:()=>3,getScenarioId:()=>"",getSessionId:()=>"",start:()=>{}},Ec=()=>crypto.randomUUID(),Xc=(c,g,C)=>{let{hostname:A,port:I,protocol:R}=window.location,S=R==="https:"?"wss:":"ws:",T=I?`:${I}`:"",w=new URL(`${S}//${A}${T}${c}`);if(w.searchParams.set("sessionId",g),C)w.searchParams.set("scenarioId",C);return w.toString()},Pc=(c)=>{if(!c||typeof c!=="object"||!("type"in c))return!1;switch(c.type){case"audio":case"assistant":case"call_lifecycle":case"complete":case"connection":case"error":case"final":case"partial":case"pong":case"replay":case"session":case"turn":return!0;default:return!1}},Yc=(c)=>{if(typeof c.data!=="string")return null;try{let g=JSON.parse(c.data);return Pc(g)?g:null}catch{return null}},o=(c,g={})=>{if(typeof window>"u")return Wc;let C=new Set,A=g.reconnect!==!1,I=g.maxReconnectAttempts??10,R=g.pingInterval??30000,S={isConnected:!1,pendingMessages:[],scenarioId:g.scenarioId??null,pingInterval:null,reconnectAttempts:0,reconnectTimeout:null,sessionId:g.sessionId??Ec(),ws:null},T=(V)=>{C.forEach(($)=>$(V))},w=()=>{if(S.pingInterval)clearInterval(S.pingInterval),S.pingInterval=null;if(S.reconnectTimeout)clearTimeout(S.reconnectTimeout),S.reconnectTimeout=null},U=()=>{if(S.ws?.readyState!==1)return;while(S.pendingMessages.length>0){let V=S.pendingMessages.shift();if(V!==void 0)S.ws.send(V)}},_=()=>{let V=Date.now()+500;S.reconnectAttempts+=1,T({reconnect:{attempts:S.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:I,nextAttemptAt:V,status:"reconnecting"},type:"connection"}),S.reconnectTimeout=setTimeout(()=>{if(S.reconnectAttempts>I){T({reconnect:{attempts:S.reconnectAttempts,maxAttempts:I,status:"exhausted"},type:"connection"});return}L()},500)},L=()=>{let V=new WebSocket(Xc(c,S.sessionId,S.scenarioId));V.binaryType="arraybuffer",V.onopen=()=>{let $=S.reconnectAttempts>0;if(S.isConnected=!0,U(),$)T({reconnect:{attempts:S.reconnectAttempts,lastResumedAt:Date.now(),maxAttempts:I,status:"resumed"},type:"connection"}),S.reconnectAttempts=0;C.forEach((D)=>D({scenarioId:S.scenarioId??void 0,sessionId:S.sessionId,status:"active",type:"session"})),S.pingInterval=setInterval(()=>{if(V.readyState===1)V.send(JSON.stringify({type:"ping"}))},R)},V.onmessage=($)=>{let D=Yc($);if(!D)return;if(D.type==="session")S.sessionId=D.sessionId,S.scenarioId=D.scenarioId??S.scenarioId;C.forEach((Cc)=>Cc(D))},V.onclose=($)=>{if(S.isConnected=!1,w(),A&&$.code!==1000&&S.reconnectAttempts<I)_();else if(A&&$.code!==1000)T({reconnect:{attempts:S.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:I,status:"exhausted"},type:"connection"})},S.ws=V},y=(V)=>{if(S.ws?.readyState===1){S.ws.send(V);return}S.pendingMessages.push(V)},b=(V)=>{y(JSON.stringify(V))},h=(V={})=>{if(V.sessionId)S.sessionId=V.sessionId;if(V.scenarioId)S.scenarioId=V.scenarioId;b({scenarioId:S.scenarioId??void 0,sessionId:S.sessionId,type:"start"})},E=(V)=>{y(V)},H=()=>{b({type:"end_turn"})},G=(V)=>{b({...V,type:"call_control"})},M=()=>{if(w(),S.ws)S.ws.close(1000),S.ws=null;S.isConnected=!1,C.clear()},X=()=>{if(S.ws?.readyState===1)S.ws.close(4000,"absolutejs-voice-reconnect-proof")},P=(V)=>{return C.add(V),()=>{C.delete(V)}};return L(),{callControl:G,close:M,endTurn:H,send:b,sendAudio:E,simulateDisconnect:X,start:h,subscribe:P,getReadyState:()=>S.ws?.readyState??3,getScenarioId:()=>S.scenarioId??"",getSessionId:()=>S.sessionId}};var Jc=()=>({attempts:0,maxAttempts:0,status:"idle"}),Zc=()=>({assistantAudio:[],assistantStreamingText:"",assistantTexts:[],call:null,error:null,isConnected:!1,partial:"",reconnect:Jc(),scenarioId:null,sessionId:null,sessionMetadata:null,status:"idle",turns:[]}),s=()=>{let c=Zc(),g=new Set,C=()=>{g.forEach((I)=>I())};return{dispatch:(I)=>{switch(I.type){case"audio":c={...c,assistantAudio:[...c.assistantAudio,{chunk:I.chunk,format:I.format,receivedAt:I.receivedAt,turnId:I.turnId}]};break;case"assistant":c={...c,assistantStreamingText:"",assistantTexts:[...c.assistantTexts,I.text]};break;case"assistant_delta":c={...c,assistantStreamingText:`${c.assistantStreamingText}${I.delta}`};break;case"complete":c={...c,sessionId:I.sessionId,status:"completed"};break;case"call_lifecycle":c={...c,call:{...c.call,disposition:I.event.type==="end"?I.event.disposition:c.call?.disposition,endedAt:I.event.type==="end"?I.event.at:c.call?.endedAt,events:[...c.call?.events??[],I.event],lastEventAt:I.event.at,startedAt:c.call?.startedAt??I.event.at},sessionId:I.sessionId};break;case"connected":c={...c,isConnected:!0,reconnect:c.reconnect.status==="reconnecting"?{...c.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:c.reconnect};break;case"connection":c={...c,reconnect:I.reconnect};break;case"disconnected":c={...c,isConnected:!1};break;case"error":c={...c,error:I.message};break;case"final":c={...c,partial:I.transcript.text,turns:c.turns.map((R)=>R)};break;case"partial":c={...c,partial:I.transcript.text};break;case"replay":c={...c,assistantStreamingText:"",assistantTexts:[...I.assistantTexts],call:I.call??null,error:null,isConnected:I.status==="active",partial:I.partial,reconnect:c.reconnect.status==="reconnecting"?{...c.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:c.reconnect,scenarioId:I.scenarioId??c.scenarioId,sessionId:I.sessionId,sessionMetadata:I.sessionMetadata??c.sessionMetadata,status:I.status,turns:[...I.turns]};break;case"session":c={...c,error:null,scenarioId:I.scenarioId??c.scenarioId,isConnected:I.status==="active",sessionId:I.sessionId,sessionMetadata:I.sessionMetadata??c.sessionMetadata,status:I.status};break;case"turn":c={...c,partial:"",turns:[...c.turns,I.turn]};break}C()},getServerSnapshot:()=>c,getSnapshot:()=>c,subscribe:(I)=>{return g.add(I),()=>{g.delete(I)}}}};var v=(c,g={})=>{let C=o(c,g),A=s(),I=g.browserMedia&&typeof window<"u"?F({...g.browserMedia,getScenarioId:()=>g.browserMedia?g.browserMedia.getScenarioId?.()??C.getScenarioId():C.getScenarioId(),getSessionId:()=>g.browserMedia?g.browserMedia.getSessionId?.()??C.getSessionId():C.getSessionId()}):null,R=new Set,S=(_)=>Promise.resolve().then(()=>{if(!_?.sessionId&&!_?.scenarioId)return;C.start(_),I?.start()}),T=()=>{R.forEach((_)=>_())},w=()=>{if(!g.reconnectReportPath||typeof fetch>"u")return;let _=A.getSnapshot(),L=JSON.stringify({at:Date.now(),reconnect:_.reconnect,scenarioId:_.scenarioId,sessionId:C.getSessionId(),turnIds:_.turns.map((y)=>y.id)});fetch(g.reconnectReportPath,{body:L,headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"}).catch(()=>{})},U=C.subscribe((_)=>{let L=d(_);if(L){if(A.dispatch(L),_.type==="connection")w();T()}});return{start:S,get assistantAudio(){return A.getSnapshot().assistantAudio},get assistantTexts(){return A.getSnapshot().assistantTexts},get assistantStreamingText(){return A.getSnapshot().assistantStreamingText},get call(){return A.getSnapshot().call},callControl(_){C.callControl(_)},close(){U(),I?.close(),C.close(),A.dispatch({type:"disconnected"}),T()},endTurn(){C.endTurn()},get error(){return A.getSnapshot().error},getServerSnapshot(){return A.getServerSnapshot()},getSnapshot(){return A.getSnapshot()},get isConnected(){return A.getSnapshot().isConnected},get partial(){return A.getSnapshot().partial},get reconnect(){return A.getSnapshot().reconnect},get scenarioId(){return A.getSnapshot().scenarioId},sendAudio(_){C.sendAudio(_)},get sessionId(){return C.getSessionId()},get sessionMetadata(){return A.getSnapshot().sessionMetadata},simulateDisconnect(){C.simulateDisconnect()},get status(){return A.getSnapshot().status},subscribe(_){return R.add(_),()=>{R.delete(_)}},get turns(){return A.getSnapshot().turns}}};var a=(c)=>{if(!c||c.enabled===!1)return;return{enabled:!0,maxGain:c.maxGain??3,noiseGateAttenuation:c.noiseGateAttenuation??0.15,noiseGateThreshold:c.noiseGateThreshold??0.006,targetLevel:c.targetLevel??0.08}};var Z=1200;var Qc={balanced:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:Z,silenceMs:1400,speechThreshold:0.012,transcriptStabilityMs:1000},fast:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:Z,silenceMs:700,speechThreshold:0.015,transcriptStabilityMs:450},"long-form":{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:Z,silenceMs:2200,speechThreshold:0.01,transcriptStabilityMs:1500}},Kc={"accent-heavy":{silenceMs:1200,speechThreshold:0.01,transcriptStabilityMs:1200},general:{},"noisy-room":{silenceMs:2000,speechThreshold:0.02,transcriptStabilityMs:1600},"short-command":{silenceMs:500,speechThreshold:0.016,transcriptStabilityMs:420}},qc="fast",zc="general",r=(c)=>{let g=c?.profile??qc,C=c?.qualityProfile??zc,A=Qc[g],I=Kc[C];return{profile:g,qualityProfile:C,semanticVetoMaxMs:c?.semanticVetoMaxMs??A.semanticVetoMaxMs,semanticVetoRecheckMs:c?.semanticVetoRecheckMs??A.semanticVetoRecheckMs,silenceMs:c?.silenceMs??I.silenceMs??A.silenceMs,speechThreshold:c?.speechThreshold??I.speechThreshold??A.speechThreshold,transcriptStabilityMs:c?.transcriptStabilityMs??I.transcriptStabilityMs??A.transcriptStabilityMs}};var Bc={chat:{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"balanced",qualityProfile:"short-command"}},default:{capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"fast",qualityProfile:"general"}},dictation:{audioConditioning:{enabled:!0,maxGain:2.25,noiseGateAttenuation:0.05,noiseGateThreshold:0.003,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"guided-intake":{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"turn-scoped",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"noisy-room":{audioConditioning:{enabled:!0,maxGain:3,noiseGateAttenuation:0.12,noiseGateThreshold:0.006,targetLevel:0.085},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:2100,speechThreshold:0.02,transcriptStabilityMs:1650}},"pstn-balanced":{audioConditioning:{enabled:!0,maxGain:2.8,noiseGateAttenuation:0.07,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:660,speechThreshold:0.012,transcriptStabilityMs:300}},"pstn-fast":{audioConditioning:{enabled:!0,maxGain:2.75,noiseGateAttenuation:0.06,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:620,speechThreshold:0.012,transcriptStabilityMs:280}},reliability:{audioConditioning:{enabled:!0,maxGain:2.9,noiseGateAttenuation:0.08,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room"}}},t=(c="default")=>{let g=Bc[c];return{audioConditioning:a(g.audioConditioning),capture:{channelCount:g.capture?.channelCount??1,sampleRateHz:g.capture?.sampleRateHz??16000},connection:{...g.connection},name:c,sttLifecycle:g.sttLifecycle??"continuous",turnDetection:r(g.turnDetection)}};var jc=(c)=>({assistantAudio:[...c.assistantAudio],assistantStreamingText:c.assistantStreamingText,assistantTexts:[...c.assistantTexts],call:c.call,error:c.error,isConnected:c.isConnected,isRecording:!1,partial:c.partial,reconnect:c.reconnect,recordingError:null,sessionId:c.sessionId,sessionMetadata:c.sessionMetadata,scenarioId:c.scenarioId,status:c.status,turns:[...c.turns]}),e=(c,g={})=>{let C=t(g.preset),A=v(c,{...C.connection,...g.connection}),I=null,R=jc(A),S=new Set,T=()=>{for(let h of S)h()},w=()=>{if(R={...R,assistantAudio:[...A.assistantAudio],assistantStreamingText:A.assistantStreamingText,assistantTexts:[...A.assistantTexts],call:A.call,error:A.error,isConnected:A.isConnected,partial:A.partial,reconnect:A.reconnect,sessionId:A.sessionId,sessionMetadata:A.sessionMetadata,scenarioId:A.scenarioId,status:A.status,turns:[...A.turns]},g.autoStopOnComplete!==!1&&R.status==="completed"&&R.isRecording)I?.stop(),I=null,R={...R,isRecording:!1};T()},U=A.subscribe(w);w();let _=()=>{if(I)return I;return I=j({channelCount:g.capture?.channelCount??C.capture.channelCount,onLevel:g.capture?.onLevel,onAudio:(h)=>{if(g.capture?.onAudio){g.capture.onAudio(h,A.sendAudio);return}A.sendAudio(h)},sampleRateHz:g.capture?.sampleRateHz??C.capture.sampleRateHz,...g.capture?.stream?{stream:g.capture.stream}:{}}),I},L=()=>{I?.stop(),I=null,R={...R,isRecording:!1},T()},y=async()=>{if(R.isRecording)return;try{R={...R,recordingError:null},T(),await _().start(),R={...R,isRecording:!0},T()}catch(h){throw I=null,R={...R,isRecording:!1,recordingError:h instanceof Error?h.message:String(h)},T(),h}};return{close:()=>{U(),L(),A.close()},startRecording:y,stopRecording:L,get assistantAudio(){return R.assistantAudio},get assistantTexts(){return R.assistantTexts},get assistantStreamingText(){return R.assistantStreamingText},bindHTMX(h){return B(A,h)},get call(){return R.call},callControl:(h)=>A.callControl(h),endTurn:()=>A.endTurn(),get error(){return R.error},getServerSnapshot:()=>R,getSnapshot:()=>R,get isConnected(){return R.isConnected},get isRecording(){return R.isRecording},get partial(){return R.partial},get reconnect(){return R.reconnect},get recordingError(){return R.recordingError},get scenarioId(){return R.scenarioId},sendAudio:(h)=>A.sendAudio(h),get sessionId(){return R.sessionId},get sessionMetadata(){return R.sessionMetadata},simulateDisconnect:()=>A.simulateDisconnect(),get status(){return R.status},subscribe:(h)=>{return S.add(h),()=>{S.delete(h)}},toggleRecording:async()=>{if(R.isRecording){L();return}await y()},get turns(){return R.turns}}};var l=(c)=>String(c).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;");var m=(c)=>{if(!c.isConnected)return"idle";if(c.isPlaying)return"speaking";if(c.isRecording&&c.hasActivePartial)return"listening";if(c.isRecording)return"listening";if(c.lastTranscriptAt&&!c.lastAssistantAt)return"thinking";if(c.lastTranscriptAt&&c.lastAssistantAt&&c.lastTranscriptAt>c.lastAssistantAt)return"thinking";return"idle"};var dc={accent:"#3b82f6",background:"#0f172a",errorAccent:"#ef4444",fontFamily:'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',foreground:"#f8fafc",radius:16},kc={callEnded:"Call ended",connecting:"Connecting…",endCall:"End call",idle:"Idle",listening:"Listening",mute:"Mute",speaking:"Speaking",startCall:"Start call",thinking:"Thinking",unmute:"Unmute"},xc=(c,g)=>{switch(c){case"listening":return g.listening;case"speaking":return g.speaking;case"thinking":return g.thinking;case"idle":return g.idle}},u=(c)=>{let g={...dc,...c.theme},C={...kc,...c.labels},A=c.state.assistantAudio.at(-1)?.receivedAt,I=c.state.turns.at(-1)?.committedAt,R=m({hasActivePartial:c.state.partial.length>0,isConnected:c.state.isConnected,isPlaying:!1,isRecording:c.state.isRecording,lastAssistantAt:A,lastTranscriptAt:I}),S=!c.state.isConnected&&c.state.status!=="idle"&&!c.state.error,T=c.state.error?"Error":S?C.connecting:c.state.status==="completed"?C.callEnded:xc(R,C);return{agentState:R,classes:{container:`absolute-voice-widget absolute-voice-widget--${R}`,dot:`absolute-voice-widget__dot${c.state.error?" absolute-voice-widget__dot--error":""}`},controls:{canEnd:c.state.isConnected,canMute:c.state.isRecording,canStart:!c.state.isRecording&&c.state.status!=="completed"},errorMessage:c.state.error??void 0,labels:C,partial:c.state.partial||void 0,statusLabel:T,theme:g,title:c.title??"Voice"}},ic=(c)=>typeof c==="number"?`${c}px`:c,p=(c)=>{let g=c.theme,C=`background:${g.background};border-radius:${ic(g.radius)};color:${g.foreground};font-family:${g.fontFamily};min-width:240px;padding:20px 22px;`,A=`background:${c.errorMessage?g.errorAccent:c.agentState==="idle"?"rgba(148,163,184,0.6)":g.accent};border-radius:50%;height:10px;width:10px;`,I=[];if(c.controls.canStart)I.push(`<button type="button" data-action="start" style="background:${g.accent};border:none;border-radius:12px;color:${g.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${l(c.labels.startCall)}</button>`);if(c.controls.canMute)I.push(`<button type="button" data-action="mute" style="background:transparent;border:1px solid rgba(255,255,255,0.18);border-radius:12px;color:${g.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${l(c.labels.mute)}</button>`);if(c.controls.canEnd)I.push(`<button type="button" data-action="end" style="background:${g.errorAccent};border:none;border-radius:12px;color:${g.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${l(c.labels.endCall)}</button>`);return`<div role="region" aria-live="polite" data-agent-state="${c.agentState}" class="${l(c.classes.container)}" style="${C}">
1
+ (()=>{var{defineProperty:E,getOwnPropertyNames:I0,getOwnPropertyDescriptor:H0}=Object,_0=Object.prototype.hasOwnProperty;function G0(g){return this[g]}var L0=(g)=>{var A=(x??=new WeakMap).get(g),C;if(A)return A;if(A=E({},"__esModule",{value:!0}),g&&typeof g==="object"||typeof g==="function"){for(var V of I0(g))if(!_0.call(A,V))E(A,V,{get:G0.bind(g,V),enumerable:!(C=H0(g,V))||C.enumerable})}return x.set(g,A),A},x;var U0=(g)=>g;function w0(g,A){this[g]=U0.bind(null,A)}var y0=(g,A)=>{for(var C in A)E(g,C,{get:A[C],enumerable:!0,configurable:!0,set:w0.bind(A,C)})};var r0={};y0(r0,{mount:()=>V0,default:()=>s0,VOICE_EMBED_VERSION:()=>C0});var X0=(g)=>{if(typeof g!=="string")return g;return document.querySelector(g)},h0=(g,A,C,V)=>{let I=A??g.getAttribute("hx-get")??"";if(!I)return"";let R=new URL(I,window.location.origin);if(V)R.searchParams.set(C,V);else R.searchParams.delete(C);return`${R.pathname}${R.search}${R.hash}`},F=(g,A)=>{if(typeof window>"u"||typeof document>"u")return()=>{};let C=X0(A.element);if(!C)return()=>{};let V=A.eventName??"voice-refresh",I=A.sessionQueryParam??"sessionId",R=()=>{let L=window,$=h0(C,A.route,I,g.sessionId);if($)C.setAttribute("hx-get",$);L.htmx?.process?.(C),L.htmx?.trigger?.(C,V)},w=g.subscribe(R);return R(),()=>{w()}};var D0=(g)=>Math.max(-1,Math.min(1,g)),J0=(g)=>{let A=new Int16Array(g.length);for(let C=0;C<g.length;C+=1){let V=D0(g[C]??0);A[C]=V<0?V*32768:V*32767}return new Uint8Array(A.buffer)},Y0=(g)=>{let A=g instanceof Uint8Array?g:new Uint8Array(g);if(A.byteLength<2)return 0;let C=new Int16Array(A.buffer,A.byteOffset,Math.floor(A.byteLength/2));if(C.length===0)return 0;let V=0;for(let I of C){let R=I/32768;V+=R*R}return Math.min(1,Math.max(0,Math.sqrt(V/C.length)*5.5))},Z0=(g,A,C)=>{if(A===C)return g;let V=A/C,I=Math.round(g.length/V),R=new Float32Array(I),w=0,L=0;while(w<R.length){let $=Math.round((w+1)*V),X=0,G=0;for(let h=L;h<$&&h<g.length;h+=1)X+=g[h]??0,G+=1;R[w]=G>0?X/G:0,w+=1,L=$}return R},f=(g)=>{let A=null,C=null,V=null,I=null;return{start:async()=>{if(!g.stream&&(typeof navigator>"u"||!navigator.mediaDevices?.getUserMedia))throw Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");let L=(typeof window<"u"?window.AudioContext??window.webkitAudioContext:void 0)??AudioContext;if(!L)throw Error("Browser microphone capture requires AudioContext support.");if(I=g.stream??await navigator.mediaDevices.getUserMedia({audio:{autoGainControl:!0,channelCount:g.channelCount??1,echoCancellation:!0,noiseSuppression:!0}}),A=new L,A.state==="suspended")await A.resume();C=A.createMediaStreamSource(I),V=A.createScriptProcessor(4096,1,1),V.onaudioprocess=($)=>{let X=$.inputBuffer.getChannelData(0),G=Z0(X,A?.sampleRate??48000,g.sampleRateHz??16000),h=J0(G);g.onLevel?.(Y0(h)),g.onAudio(h)},C.connect(V),V.connect(A.destination)},stop:()=>{V?.disconnect(),C?.disconnect(),I?.getTracks().forEach((L)=>L.stop()),A?.close(),g.onLevel?.(0),A=null,I=null,V=null,C=null}}};var M=(g)=>{if(typeof g==="string"&&g.trim())return g;if(g instanceof Error&&g.message.trim())return g.message;if(g&&typeof g==="object"){let A=g;for(let C of["message","reason","description"]){let V=A[C];if(typeof V==="string"&&V.trim())return V}if("error"in A)return M(A.error);if("cause"in A)return M(A.cause);try{return JSON.stringify(g)}catch{}}return"Unexpected error"},l=(g)=>{switch(g.type){case"audio":return{chunk:Uint8Array.from(atob(g.chunkBase64),(A)=>A.charCodeAt(0)),format:g.format,receivedAt:g.receivedAt,turnId:g.turnId,type:"audio"};case"assistant":return{text:g.text,turnId:g.turnId,type:"assistant"};case"assistant_delta":return{delta:g.delta,turnId:g.turnId,type:"assistant_delta"};case"complete":return{sessionId:g.sessionId,type:"complete"};case"connection":return{reconnect:g.reconnect,type:"connection"};case"call_lifecycle":return{event:g.event,sessionId:g.sessionId,type:"call_lifecycle"};case"error":return{message:M(g.message),type:"error"};case"final":return{transcript:g.transcript,type:"final"};case"partial":return{transcript:g.transcript,type:"partial"};case"replay":return{assistantTexts:g.assistantTexts,call:g.call,partial:g.partial,scenarioId:g.scenarioId,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,status:g.status,turns:g.turns,type:"replay"};case"session":return{sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,type:"session"};case"turn":return{turn:g.turn,type:"turn"};default:return null}};var gg=Math.PI*2;var T=(g,A,C,V)=>{g.push({code:C,message:V,severity:A})};var O0=(g)=>g.length===0?void 0:g.reduce((A,C)=>A+C,0)/g.length,z=(g)=>g.length===0?void 0:Math.max(...g);var D=(g,A)=>{let C=g[A];return typeof C==="number"&&Number.isFinite(C)?C:void 0},B=(g,A)=>{let C=g[A];return typeof C==="boolean"?C:void 0},Y=(g,A)=>{let C=g[A];return typeof C==="string"?C:void 0},k=(g)=>String(g.id??Y(g,"ssrc")??D(g,"ssrc")??Y(g,"trackIdentifier")??Y(g,"mid")??"unknown"),d=(g)=>g===void 0?void 0:g*1000;var S0=(g)=>{let A={};for(let[C,V]of Object.entries(g))if(V===null||typeof V==="boolean"||typeof V==="number"||typeof V==="string")A[C]=V;return A};var v=(g={})=>{let A=g.stats??[],C=[],V=A.filter((H)=>H.type==="inbound-rtp"&&Y(H,"kind")!=="video"),I=A.filter((H)=>H.type==="outbound-rtp"&&Y(H,"kind")!=="video"),R=A.filter((H)=>H.type==="candidate-pair"),w=A.filter((H)=>(H.type==="track"||H.type==="media-source")&&Y(H,"kind")==="audio"),L=R.filter((H)=>B(H,"selected")===!0||B(H,"nominated")===!0||Y(H,"state")==="succeeded").length,$=w.filter((H)=>Y(H,"readyState")!=="ended"&&Y(H,"trackState")!=="ended"&&B(H,"ended")!==!0).length,X=w.filter((H)=>Y(H,"readyState")==="ended"||Y(H,"trackState")==="ended"||B(H,"ended")===!0).length,G=V.reduce((H,J)=>H+(D(J,"packetsReceived")??0),0),h=I.reduce((H,J)=>H+(D(J,"packetsSent")??0),0),_=[...V,...I].reduce((H,J)=>H+Math.max(0,D(J,"packetsLost")??0),0),Z=G+_,y=Z===0?0:_/Z,K=V.reduce((H,J)=>H+(D(J,"bytesReceived")??0),0),O=I.reduce((H,J)=>H+(D(J,"bytesSent")??0),0),S=z(R.map((H)=>d(D(H,"currentRoundTripTime")??D(H,"roundTripTime"))).filter((H)=>H!==void 0)),W=z([...V,...I].map((H)=>d(D(H,"jitter"))).filter((H)=>H!==void 0)),N=z(V.map((H)=>{let J=D(H,"jitterBufferDelay"),U=D(H,"jitterBufferEmittedCount");return J!==void 0&&U!==void 0&&U>0?J/U*1000:void 0}).filter((H)=>H!==void 0)),j=w.map((H)=>D(H,"audioLevel")).filter((H)=>H!==void 0);if(g.requireConnectedCandidatePair&&R.length>0&&L===0)T(C,"error","media.webrtc_candidate_pair_missing","No active WebRTC candidate pair was observed.");if(g.requireLiveAudioTrack&&$===0)T(C,"error","media.webrtc_audio_track_missing","No live WebRTC audio track was observed.");if(g.maxPacketLossRatio!==void 0&&y>g.maxPacketLossRatio)T(C,"warning","media.webrtc_packet_loss",`Observed WebRTC packet loss ratio ${String(y)} above ${String(g.maxPacketLossRatio)}.`);if(g.maxRoundTripTimeMs!==void 0&&S!==void 0&&S>g.maxRoundTripTimeMs)T(C,"warning","media.webrtc_round_trip_time",`Observed WebRTC RTT ${String(S)}ms above ${String(g.maxRoundTripTimeMs)}ms.`);if(g.maxJitterMs!==void 0&&W!==void 0&&W>g.maxJitterMs)T(C,"warning","media.webrtc_jitter",`Observed WebRTC jitter ${String(W)}ms above ${String(g.maxJitterMs)}ms.`);return{activeCandidatePairs:L,audioLevelAverage:O0(j),bytesReceived:K,bytesSent:O,checkedAt:Date.now(),endedAudioTracks:X,inboundPackets:G,issues:C,jitterBufferDelayMs:N,jitterMs:W,liveAudioTracks:$,outboundPackets:h,packetLossRatio:y,packetsLost:_,roundTripTimeMs:S,status:C.some((H)=>H.severity==="error")?"fail":C.length>0?"warn":"pass",totalStats:A.length}},i=async(g)=>{return[...(await g.peerConnection.getStats(g.selector??null)).values()].map(S0)};var o=(g={})=>{let A=g.stats??[],C=g.previousStats??[],V=[],I=new Map(C.map((_)=>[k(_),_])),w=A.filter((_)=>(_.type==="inbound-rtp"||_.type==="outbound-rtp")&&Y(_,"kind")!=="video"&&Y(_,"mediaType")!=="video").map((_)=>{let Z=_.type==="outbound-rtp"?"outbound":"inbound",y=Z==="outbound"?"packetsSent":"packetsReceived",K=Z==="outbound"?"bytesSent":"bytesReceived",O=I.get(k(_)),S=D(_,y),W=O?D(O,y):void 0,N=D(_,K),j=O?D(O,K):void 0,H=_.timestamp!==void 0&&O?.timestamp!==void 0?_.timestamp-O.timestamp:void 0;return{bytesDelta:N!==void 0&&j!==void 0?N-j:void 0,currentPackets:S,direction:Z,id:k(_),packetDelta:S!==void 0&&W!==void 0?S-W:void 0,previousPackets:W,timeDeltaMs:H}}),L=w.filter((_)=>_.direction==="inbound"),$=w.filter((_)=>_.direction==="outbound"),X=z(w.map((_)=>_.timeDeltaMs).filter((_)=>_!==void 0)),G=L.filter((_)=>g.maxInboundPacketStallMs!==void 0&&_.timeDeltaMs!==void 0&&_.timeDeltaMs>=g.maxInboundPacketStallMs&&_.packetDelta!==void 0&&_.packetDelta<=0).length,h=$.filter((_)=>g.maxOutboundPacketStallMs!==void 0&&_.timeDeltaMs!==void 0&&_.timeDeltaMs>=g.maxOutboundPacketStallMs&&_.packetDelta!==void 0&&_.packetDelta<=0).length;if(g.requireInboundAudio&&L.length===0)T(V,"error","media.webrtc_inbound_audio_missing","No inbound WebRTC audio RTP stream was observed.");if(g.requireOutboundAudio&&$.length===0)T(V,"error","media.webrtc_outbound_audio_missing","No outbound WebRTC audio RTP stream was observed.");if(g.maxGapMs!==void 0&&X!==void 0&&X>g.maxGapMs)T(V,"warning","media.webrtc_stream_gap",`Observed WebRTC stream sample gap ${String(X)}ms above ${String(g.maxGapMs)}ms.`);if(G>0)T(V,"error","media.webrtc_inbound_stalled",`${String(G)} inbound WebRTC audio stream(s) stopped receiving packets.`);if(h>0)T(V,"error","media.webrtc_outbound_stalled",`${String(h)} outbound WebRTC audio stream(s) stopped sending packets.`);return{checkedAt:Date.now(),inboundAudioStreams:L.length,issues:V,maxObservedGapMs:X,outboundAudioStreams:$.length,stalledInboundStreams:G,stalledOutboundStreams:h,status:V.some((_)=>_.severity==="error")?"fail":V.length>0?"warn":"pass",streams:w,totalStats:A.length}};var W0="/api/voice/browser-media",Q0=5000,T0=async(g)=>g.peerConnection??await g.getPeerConnection?.()??null,c0=async(g,A)=>{let C=A.fetch??globalThis.fetch;if(!C)return;await C(A.path??W0,{body:JSON.stringify(g),headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"})},n=(g)=>{let A=null,C=[],V=async()=>{let w=await T0(g);if(!w)return;let L=await i({peerConnection:w}),$=v({...g,stats:L}),X=g.continuity===!1?void 0:o({...g.continuity,previousStats:C,stats:L}),G={at:Date.now(),continuity:X,report:$,scenarioId:g.getScenarioId?.()??null,sessionId:g.getSessionId?.()??null};return C=L,g.onReport?.(G),await c0(G,g),G},I=()=>{V().catch((w)=>{g.onError?.(w)})},R=()=>{if(A)clearInterval(A),A=null};return{close:R,reportOnce:V,stop:R,start:()=>{if(A)return;I(),A=setInterval(I,g.intervalMs??Q0)}}};var K0=(g,A,C)=>Math.min(C,A*2**(Math.max(1,g)-1));var q=()=>{},P0=()=>q,q0={callControl:q,close:q,endTurn:q,send:q,sendAudio:q,simulateDisconnect:q,subscribe:P0,getReadyState:()=>3,getScenarioId:()=>"",getSessionId:()=>"",start:()=>{}},N0=()=>crypto.randomUUID(),j0=(g,A,C)=>{let{hostname:V,port:I,protocol:R}=window.location,w=R==="https:"?"wss:":"ws:",L=I?`:${I}`:"",$=new URL(`${w}//${V}${L}${g}`);if($.searchParams.set("sessionId",A),C)$.searchParams.set("scenarioId",C);return $.toString()},B0=(g)=>{if(!g||typeof g!=="object"||!("type"in g))return!1;switch(g.type){case"audio":case"assistant":case"call_lifecycle":case"complete":case"connection":case"error":case"final":case"partial":case"pong":case"replay":case"session":case"turn":return!0;default:return!1}},z0=(g)=>{if(typeof g.data!=="string")return null;try{let A=JSON.parse(g.data);return B0(A)?A:null}catch{return null}},m=(g,A={})=>{if(typeof window>"u")return q0;let C=new Set,V=A.reconnect!==!1,I=A.maxReconnectAttempts??15,R=A.reconnectMaxDelayMs??8000,w=A.pingInterval??30000,L=(U)=>K0(U,500,R),$={isConnected:!1,pendingMessages:[],scenarioId:A.scenarioId??null,pingInterval:null,reconnectAttempts:0,reconnectTimeout:null,sessionId:A.sessionId??N0(),ws:null},X=(U)=>{C.forEach((Q)=>Q(U))},G=()=>{if($.pingInterval)clearInterval($.pingInterval),$.pingInterval=null;if($.reconnectTimeout)clearTimeout($.reconnectTimeout),$.reconnectTimeout=null},h=()=>{if($.ws?.readyState!==1)return;while($.pendingMessages.length>0){let U=$.pendingMessages.shift();if(U!==void 0)$.ws.send(U)}},_=()=>{$.reconnectAttempts+=1;let U=L($.reconnectAttempts),Q=Date.now()+U;X({reconnect:{attempts:$.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:I,nextAttemptAt:Q,status:"reconnecting"},type:"connection"}),$.reconnectTimeout=setTimeout(()=>{if($.reconnectAttempts>I){X({reconnect:{attempts:$.reconnectAttempts,maxAttempts:I,status:"exhausted"},type:"connection"});return}Z()},U)},Z=()=>{let U=new WebSocket(j0(g,$.sessionId,$.scenarioId));U.binaryType="arraybuffer",U.onopen=()=>{let Q=$.reconnectAttempts>0;if($.isConnected=!0,h(),Q)X({reconnect:{attempts:$.reconnectAttempts,lastResumedAt:Date.now(),maxAttempts:I,status:"resumed"},type:"connection"}),$.reconnectAttempts=0;C.forEach((P)=>P({scenarioId:$.scenarioId??void 0,sessionId:$.sessionId,status:"active",type:"session"})),$.pingInterval=setInterval(()=>{if(U.readyState===1)U.send(JSON.stringify({type:"ping"}))},w)},U.onmessage=(Q)=>{let P=z0(Q);if(!P)return;if(P.type==="session")$.sessionId=P.sessionId,$.scenarioId=P.scenarioId??$.scenarioId;C.forEach(($0)=>$0(P))},U.onclose=(Q)=>{if($.isConnected=!1,G(),V&&Q.code!==1000&&$.reconnectAttempts<I)_();else if(V&&Q.code!==1000)X({reconnect:{attempts:$.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:I,status:"exhausted"},type:"connection"})},$.ws=U},y=(U)=>{if($.ws?.readyState===1){$.ws.send(U);return}$.pendingMessages.push(U)},K=(U)=>{y(JSON.stringify(U))},O=(U={})=>{if(U.sessionId)$.sessionId=U.sessionId;if(U.scenarioId)$.scenarioId=U.scenarioId;K({scenarioId:$.scenarioId??void 0,sessionId:$.sessionId,type:"start"})},S=(U)=>{y(U)},W=()=>{K({type:"end_turn"})},N=(U)=>{K({...U,type:"call_control"})},j=()=>{if(G(),$.ws)$.ws.close(1000),$.ws=null;$.isConnected=!1,C.clear()},H=()=>{if($.ws?.readyState===1)$.ws.close(4000,"absolutejs-voice-reconnect-proof")},J=(U)=>{return C.add(U),()=>{C.delete(U)}};return Z(),{callControl:N,close:j,endTurn:W,send:K,sendAudio:S,simulateDisconnect:H,start:O,subscribe:J,getReadyState:()=>$.ws?.readyState??3,getScenarioId:()=>$.scenarioId??"",getSessionId:()=>$.sessionId}};var b0=()=>({attempts:0,maxAttempts:0,status:"idle"}),E0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!C)return g;if(!g)return C;if(g===C||g.endsWith(C))return g;if(C.includes(g))return C;return`${g} ${C}`},M0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!g)return C;if(!C||g.endsWith(C))return g;return`${g} ${C}`},k0=()=>({assistantAudio:[],assistantStreamingText:"",assistantTexts:[],call:null,error:null,isConnected:!1,partial:"",reconnect:b0(),scenarioId:null,sessionId:null,sessionMetadata:null,status:"idle",turns:[]}),u=()=>{let g=k0(),A="",C=new Set,V=()=>{C.forEach((R)=>R())};return{dispatch:(R)=>{switch(R.type){case"audio":g={...g,assistantAudio:[...g.assistantAudio,{chunk:R.chunk,format:R.format,receivedAt:R.receivedAt,turnId:R.turnId}]};break;case"assistant":g={...g,assistantStreamingText:"",assistantTexts:[...g.assistantTexts,R.text]};break;case"assistant_delta":g={...g,assistantStreamingText:`${g.assistantStreamingText}${R.delta}`};break;case"complete":g={...g,sessionId:R.sessionId,status:"completed"};break;case"call_lifecycle":g={...g,call:{...g.call,disposition:R.event.type==="end"?R.event.disposition:g.call?.disposition,endedAt:R.event.type==="end"?R.event.at:g.call?.endedAt,events:[...g.call?.events??[],R.event],lastEventAt:R.event.at,startedAt:g.call?.startedAt??R.event.at},sessionId:R.sessionId};break;case"connected":g={...g,isConnected:!0,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect};break;case"connection":g={...g,reconnect:R.reconnect};break;case"disconnected":g={...g,isConnected:!1};break;case"error":g={...g,error:R.message};break;case"final":A=E0(A,R.transcript.text),g={...g,partial:A};break;case"partial":g={...g,partial:M0(A,R.transcript.text)};break;case"replay":A=R.partial,g={...g,assistantStreamingText:"",assistantTexts:[...R.assistantTexts],call:R.call??null,error:null,isConnected:R.status==="active",partial:R.partial,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect,scenarioId:R.scenarioId??g.scenarioId,sessionId:R.sessionId,sessionMetadata:R.sessionMetadata??g.sessionMetadata,status:R.status,turns:[...R.turns]};break;case"session":g={...g,error:null,scenarioId:R.scenarioId??g.scenarioId,isConnected:R.status==="active",sessionId:R.sessionId,sessionMetadata:R.sessionMetadata??g.sessionMetadata,status:R.status};break;case"turn":A="",g={...g,partial:"",turns:[...g.turns,R.turn]};break}V()},getServerSnapshot:()=>g,getSnapshot:()=>g,subscribe:(R)=>{return C.add(R),()=>{C.delete(R)}}}};var r=(g,A={})=>{let C=m(g,A),V=u(),I=A.browserMedia&&typeof window<"u"?n({...A.browserMedia,getScenarioId:()=>A.browserMedia?A.browserMedia.getScenarioId?.()??C.getScenarioId():C.getScenarioId(),getSessionId:()=>A.browserMedia?A.browserMedia.getSessionId?.()??C.getSessionId():C.getSessionId()}):null,R=new Set,w=(G)=>Promise.resolve().then(()=>{if(!G?.sessionId&&!G?.scenarioId)return;C.start(G),I?.start()}),L=()=>{R.forEach((G)=>G())},$=()=>{if(!A.reconnectReportPath||typeof fetch>"u")return;let G=V.getSnapshot(),h=JSON.stringify({at:Date.now(),reconnect:G.reconnect,scenarioId:G.scenarioId,sessionId:C.getSessionId(),turnIds:G.turns.map((_)=>_.id)});fetch(A.reconnectReportPath,{body:h,headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"}).catch(()=>{})},X=C.subscribe((G)=>{let h=l(G);if(h){if(V.dispatch(h),G.type==="connection")$();L()}});return{start:w,get assistantAudio(){return V.getSnapshot().assistantAudio},get assistantTexts(){return V.getSnapshot().assistantTexts},get assistantStreamingText(){return V.getSnapshot().assistantStreamingText},get call(){return V.getSnapshot().call},callControl(G){C.callControl(G)},close(){X(),I?.close(),C.close(),V.dispatch({type:"disconnected"}),L()},endTurn(){C.endTurn()},get error(){return V.getSnapshot().error},getServerSnapshot(){return V.getServerSnapshot()},getSnapshot(){return V.getSnapshot()},get isConnected(){return V.getSnapshot().isConnected},get partial(){return V.getSnapshot().partial},get reconnect(){return V.getSnapshot().reconnect},get scenarioId(){return V.getSnapshot().scenarioId},sendAudio(G){C.sendAudio(G)},get sessionId(){return C.getSessionId()},get sessionMetadata(){return V.getSnapshot().sessionMetadata},simulateDisconnect(){C.simulateDisconnect()},get status(){return V.getSnapshot().status},subscribe(G){return R.add(G),()=>{R.delete(G)}},get turns(){return V.getSnapshot().turns}}};var s=(g)=>{if(!g||g.enabled===!1)return;return{enabled:!0,maxGain:g.maxGain??3,noiseGateAttenuation:g.noiseGateAttenuation??0.15,noiseGateThreshold:g.noiseGateThreshold??0.006,targetLevel:g.targetLevel??0.08}};var b=1200;var x0={balanced:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:b,silenceMs:1400,speechThreshold:0.012,transcriptStabilityMs:1000},fast:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:b,silenceMs:700,speechThreshold:0.015,transcriptStabilityMs:450},"long-form":{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:b,silenceMs:2200,speechThreshold:0.01,transcriptStabilityMs:1500}},F0={"accent-heavy":{silenceMs:1200,speechThreshold:0.01,transcriptStabilityMs:1200},general:{},"noisy-room":{silenceMs:2000,speechThreshold:0.02,transcriptStabilityMs:1600},"short-command":{silenceMs:500,speechThreshold:0.016,transcriptStabilityMs:420}},f0="fast",l0="general",a=(g)=>{let A=g?.profile??f0,C=g?.qualityProfile??l0,V=x0[A],I=F0[C];return{profile:A,qualityProfile:C,semanticVetoMaxMs:g?.semanticVetoMaxMs??V.semanticVetoMaxMs,semanticVetoRecheckMs:g?.semanticVetoRecheckMs??V.semanticVetoRecheckMs,silenceMs:g?.silenceMs??I.silenceMs??V.silenceMs,speechThreshold:g?.speechThreshold??I.speechThreshold??V.speechThreshold,transcriptStabilityMs:g?.transcriptStabilityMs??I.transcriptStabilityMs??V.transcriptStabilityMs}};var d0={chat:{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"balanced",qualityProfile:"short-command"}},default:{capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"fast",qualityProfile:"general"}},dictation:{audioConditioning:{enabled:!0,maxGain:2.25,noiseGateAttenuation:0.05,noiseGateThreshold:0.003,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"guided-intake":{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"turn-scoped",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"noisy-room":{audioConditioning:{enabled:!0,maxGain:3,noiseGateAttenuation:0.12,noiseGateThreshold:0.006,targetLevel:0.085},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:2100,speechThreshold:0.02,transcriptStabilityMs:1650}},"pstn-balanced":{audioConditioning:{enabled:!0,maxGain:2.8,noiseGateAttenuation:0.07,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:660,speechThreshold:0.012,transcriptStabilityMs:300}},"pstn-fast":{audioConditioning:{enabled:!0,maxGain:2.75,noiseGateAttenuation:0.06,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:620,speechThreshold:0.012,transcriptStabilityMs:280}},reliability:{audioConditioning:{enabled:!0,maxGain:2.9,noiseGateAttenuation:0.08,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room"}}},p=(g="default")=>{let A=d0[g];return{audioConditioning:s(A.audioConditioning),capture:{channelCount:A.capture?.channelCount??1,sampleRateHz:A.capture?.sampleRateHz??16000},connection:{...A.connection},name:g,sttLifecycle:A.sttLifecycle??"continuous",turnDetection:a(A.turnDetection)}};var v0=(g)=>({assistantAudio:[...g.assistantAudio],assistantStreamingText:g.assistantStreamingText,assistantTexts:[...g.assistantTexts],call:g.call,error:g.error,isConnected:g.isConnected,isRecording:!1,partial:g.partial,reconnect:g.reconnect,recordingError:null,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,turns:[...g.turns]}),t=(g,A={})=>{let C=p(A.preset),V=r(g,{...C.connection,...A.connection}),I=null,R=v0(V),w=new Set,L=()=>{for(let y of w)y()},$=()=>{if(R={...R,assistantAudio:[...V.assistantAudio],assistantStreamingText:V.assistantStreamingText,assistantTexts:[...V.assistantTexts],call:V.call,error:V.error,isConnected:V.isConnected,partial:V.partial,reconnect:V.reconnect,sessionId:V.sessionId,sessionMetadata:V.sessionMetadata,scenarioId:V.scenarioId,status:V.status,turns:[...V.turns]},A.autoStopOnComplete!==!1&&R.status==="completed"&&R.isRecording)I?.stop(),I=null,R={...R,isRecording:!1};L()},X=V.subscribe($);$();let G=()=>{if(I)return I;return I=f({channelCount:A.capture?.channelCount??C.capture.channelCount,onLevel:A.capture?.onLevel,onAudio:(y)=>{if(A.capture?.onAudio){A.capture.onAudio(y,V.sendAudio);return}V.sendAudio(y)},sampleRateHz:A.capture?.sampleRateHz??C.capture.sampleRateHz,...A.capture?.stream?{stream:A.capture.stream}:{}}),I},h=()=>{I?.stop(),I=null,R={...R,isRecording:!1},L()},_=async()=>{if(R.isRecording)return;try{R={...R,recordingError:null},L(),await G().start(),R={...R,isRecording:!0},L()}catch(y){throw I=null,R={...R,isRecording:!1,recordingError:y instanceof Error?y.message:String(y)},L(),y}};return{close:()=>{X(),h(),V.close()},startRecording:_,stopRecording:h,get assistantAudio(){return R.assistantAudio},get assistantTexts(){return R.assistantTexts},get assistantStreamingText(){return R.assistantStreamingText},bindHTMX(y){return F(V,y)},get call(){return R.call},callControl:(y)=>V.callControl(y),endTurn:()=>V.endTurn(),get error(){return R.error},getServerSnapshot:()=>R,getSnapshot:()=>R,get isConnected(){return R.isConnected},get isRecording(){return R.isRecording},get partial(){return R.partial},get reconnect(){return R.reconnect},get recordingError(){return R.recordingError},get scenarioId(){return R.scenarioId},sendAudio:(y)=>V.sendAudio(y),get sessionId(){return R.sessionId},get sessionMetadata(){return R.sessionMetadata},simulateDisconnect:()=>V.simulateDisconnect(),get status(){return R.status},subscribe:(y)=>{return w.add(y),()=>{w.delete(y)}},toggleRecording:async()=>{if(R.isRecording){h();return}await _()},get turns(){return R.turns}}};var c=(g)=>String(g).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;");var e=(g)=>{if(!g.isConnected)return"idle";if(g.isPlaying)return"speaking";if(g.isRecording&&g.hasActivePartial)return"listening";if(g.isRecording)return"listening";if(g.lastTranscriptAt&&!g.lastAssistantAt)return"thinking";if(g.lastTranscriptAt&&g.lastAssistantAt&&g.lastTranscriptAt>g.lastAssistantAt)return"thinking";return"idle"};var i0={accent:"#3b82f6",background:"#0f172a",errorAccent:"#ef4444",fontFamily:'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',foreground:"#f8fafc",radius:16},o0={callEnded:"Call ended",connecting:"Connecting…",endCall:"End call",idle:"Idle",listening:"Listening",mute:"Mute",speaking:"Speaking",startCall:"Start call",thinking:"Thinking",unmute:"Unmute"},n0=(g,A)=>{switch(g){case"listening":return A.listening;case"speaking":return A.speaking;case"thinking":return A.thinking;case"idle":return A.idle}},g0=(g)=>{let A={...i0,...g.theme},C={...o0,...g.labels},V=g.state.assistantAudio.at(-1)?.receivedAt,I=g.state.turns.at(-1)?.committedAt,R=e({hasActivePartial:g.state.partial.length>0,isConnected:g.state.isConnected,isPlaying:!1,isRecording:g.state.isRecording,lastAssistantAt:V,lastTranscriptAt:I}),w=!g.state.isConnected&&g.state.status!=="idle"&&!g.state.error,L=g.state.error?"Error":w?C.connecting:g.state.status==="completed"?C.callEnded:n0(R,C);return{agentState:R,classes:{container:`absolute-voice-widget absolute-voice-widget--${R}`,dot:`absolute-voice-widget__dot${g.state.error?" absolute-voice-widget__dot--error":""}`},controls:{canEnd:g.state.isConnected,canMute:g.state.isRecording,canStart:!g.state.isRecording&&g.state.status!=="completed"},errorMessage:g.state.error??void 0,labels:C,partial:g.state.partial||void 0,statusLabel:L,theme:A,title:g.title??"Voice"}},m0=(g)=>typeof g==="number"?`${g}px`:g,A0=(g)=>{let A=g.theme,C=`background:${A.background};border-radius:${m0(A.radius)};color:${A.foreground};font-family:${A.fontFamily};min-width:240px;padding:20px 22px;`,V=`background:${g.errorMessage?A.errorAccent:g.agentState==="idle"?"rgba(148,163,184,0.6)":A.accent};border-radius:50%;height:10px;width:10px;`,I=[];if(g.controls.canStart)I.push(`<button type="button" data-action="start" style="background:${A.accent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${c(g.labels.startCall)}</button>`);if(g.controls.canMute)I.push(`<button type="button" data-action="mute" style="background:transparent;border:1px solid rgba(255,255,255,0.18);border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${c(g.labels.mute)}</button>`);if(g.controls.canEnd)I.push(`<button type="button" data-action="end" style="background:${A.errorAccent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${c(g.labels.endCall)}</button>`);return`<div role="region" aria-live="polite" data-agent-state="${g.agentState}" class="${c(g.classes.container)}" style="${C}">
2
2
  <div style="align-items:center;display:flex;gap:10px;margin-bottom:12px;">
3
- <span aria-hidden="true" class="${l(c.classes.dot)}" style="${A}"></span>
4
- <strong style="font-size:15px;">${l(c.title)}</strong>
5
- <span style="font-size:13px;margin-left:auto;opacity:0.7;">${l(c.statusLabel)}</span>
3
+ <span aria-hidden="true" class="${c(g.classes.dot)}" style="${V}"></span>
4
+ <strong style="font-size:15px;">${c(g.title)}</strong>
5
+ <span style="font-size:13px;margin-left:auto;opacity:0.7;">${c(g.statusLabel)}</span>
6
6
  </div>
7
- ${c.partial?`<p style="font-size:13px;margin:8px 0 12px;opacity:0.85;word-break:break-word;">“${l(c.partial)}”</p>`:""}
7
+ ${g.partial?`<p style="font-size:13px;margin:8px 0 12px;opacity:0.85;word-break:break-word;">“${c(g.partial)}”</p>`:""}
8
8
  <div style="display:flex;gap:10px;">${I.join("")}</div>
9
- ${c.errorMessage?`<p style="color:${g.errorAccent};font-size:12px;margin-top:12px;">${l(c.errorMessage)}</p>`:""}
10
- </div>`};var fc=(c)=>{if(typeof c!=="string")return c;let g=document.querySelector(c);if(!g)throw Error(`AbsoluteVoice.mount: no element matches "${c}"`);return g},cc=(c,g={})=>{let C=fc(c),A=e(g.path??"/voice",g.controllerOptions),I=null,R=null,S=()=>{let w=u({...g.labels!==void 0?{labels:g.labels}:{},state:{assistantAudio:A.assistantAudio,error:A.error,isConnected:A.isConnected,isRecording:A.isRecording,partial:A.partial,status:A.status,turns:A.turns},...g.theme!==void 0?{theme:g.theme}:{},...g.title!==void 0?{title:g.title}:{}});C.innerHTML=p(w);for(let U of C.querySelectorAll("button[data-action]")){let{action:_}=U.dataset;U.addEventListener("click",()=>{if(_==="start")A.startRecording();else if(_==="mute")A.stopRecording();else if(_==="end")A.close()})}if(A.error&&A.error!==I)I=A.error,g.onError?.(A.error);if(A.status!==R)R=A.status,g.onStatusChange?.(A.status)},T=A.subscribe(S);if(S(),g.autoStart)A.startRecording();return{controller:A,async end(){await A.close()},mute(){A.stopRecording()},async start(){await A.startRecording()},unmount(){T(),A.close(),C.innerHTML=""}}},gc="0.0.22-beta.516",Ac={mount:cc,version:gc};if(typeof globalThis<"u")globalThis.AbsoluteVoice=Ac;var oc=Ac;})();
9
+ ${g.errorMessage?`<p style="color:${A.errorAccent};font-size:12px;margin-top:12px;">${c(g.errorMessage)}</p>`:""}
10
+ </div>`};var u0=(g)=>{if(typeof g!=="string")return g;let A=document.querySelector(g);if(!A)throw Error(`AbsoluteVoice.mount: no element matches "${g}"`);return A},V0=(g,A={})=>{let C=u0(g),V=t(A.path??"/voice",A.controllerOptions),I=null,R=null,w=()=>{let $=g0({...A.labels!==void 0?{labels:A.labels}:{},state:{assistantAudio:V.assistantAudio,error:V.error,isConnected:V.isConnected,isRecording:V.isRecording,partial:V.partial,status:V.status,turns:V.turns},...A.theme!==void 0?{theme:A.theme}:{},...A.title!==void 0?{title:A.title}:{}});C.innerHTML=A0($);for(let X of C.querySelectorAll("button[data-action]")){let{action:G}=X.dataset;X.addEventListener("click",()=>{if(G==="start")V.startRecording();else if(G==="mute")V.stopRecording();else if(G==="end")V.close()})}if(V.error&&V.error!==I)I=V.error,A.onError?.(V.error);if(V.status!==R)R=V.status,A.onStatusChange?.(V.status)},L=V.subscribe(w);if(w(),A.autoStart)V.startRecording();return{controller:V,async end(){await V.close()},mute(){V.stopRecording()},async start(){await V.startRecording()},unmount(){L(),V.close(),C.innerHTML=""}}},C0="0.0.22-beta.516",R0={mount:V0,version:C0};if(typeof globalThis<"u")globalThis.AbsoluteVoice=R0;var s0=R0;})();
package/dist/index.d.ts CHANGED
@@ -192,6 +192,7 @@ export { createVoiceSQLiteAuditEventStore, createVoiceSQLiteAuditSinkDeliverySto
192
192
  export { createVoicePostgresAuditEventStore, createVoicePostgresAuditSinkDeliveryStore, createVoicePostgresCampaignStore, createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore, } from "./core/postgresStore";
193
193
  export { createVoiceS3RecordingStore, createVoiceS3ReviewStore, } from "./core/s3Store";
194
194
  export { createVoiceMemoryStore } from "./core/memoryStore";
195
+ export { createVoiceWriteBehindStore, type VoiceWriteBehindStore, type VoiceWriteBehindStoreOptions, } from "./core/writeBehindStore";
195
196
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks, } from "./core/opsSinks";
196
197
  export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature, } from "./core/opsWebhook";
197
198
  export { applyVoiceHandoffDeliveryResult, createVoiceHandoffDeliveryRecord, createVoiceMemoryHandoffDeliveryStore, createVoiceTwilioRedirectHandoffAdapter, createVoiceWebhookHandoffAdapter, deliverVoiceHandoff, deliverVoiceHandoffDelivery, } from "./core/handoff";
package/dist/index.js CHANGED
@@ -47076,6 +47076,113 @@ var createVoiceMemoryStore = () => {
47076
47076
  };
47077
47077
  return { get, getOrCreate, list, remove, set };
47078
47078
  };
47079
+ // src/core/writeBehindStore.ts
47080
+ var DEFAULT_FLUSH_DEBOUNCE_MS = 750;
47081
+ var MAX_FLUSH_DRAIN_PASSES = 5;
47082
+ var createVoiceWriteBehindStore = (options) => {
47083
+ const memory = options.memory ?? createVoiceMemoryStore();
47084
+ const { persistent } = options;
47085
+ const flushDebounceMs = options.flushDebounceMs ?? DEFAULT_FLUSH_DEBOUNCE_MS;
47086
+ const logger = options.logger;
47087
+ const pending = new Map;
47088
+ const persistedTurnCount = new Map;
47089
+ let timer = null;
47090
+ let flushChain = Promise.resolve();
47091
+ const doFlush = async () => {
47092
+ if (pending.size === 0) {
47093
+ return;
47094
+ }
47095
+ const batch = new Map(pending);
47096
+ pending.clear();
47097
+ for (const [id, value] of batch) {
47098
+ try {
47099
+ await persistent.set(id, value);
47100
+ persistedTurnCount.set(id, value.turns.length);
47101
+ } catch (error) {
47102
+ if (!pending.has(id)) {
47103
+ pending.set(id, value);
47104
+ }
47105
+ logger?.warn?.("voice write-behind flush failed; will retry", {
47106
+ error: error instanceof Error ? error.message : String(error),
47107
+ sessionId: id
47108
+ });
47109
+ scheduleFlush();
47110
+ }
47111
+ }
47112
+ };
47113
+ const runFlush = () => {
47114
+ flushChain = flushChain.then(doFlush, doFlush);
47115
+ return flushChain;
47116
+ };
47117
+ const scheduleFlush = () => {
47118
+ if (timer) {
47119
+ return;
47120
+ }
47121
+ timer = setTimeout(() => {
47122
+ timer = null;
47123
+ runFlush();
47124
+ }, flushDebounceMs);
47125
+ };
47126
+ const markDirty = (id, value) => {
47127
+ pending.set(id, value);
47128
+ if (value.turns.length > (persistedTurnCount.get(id) ?? 0)) {
47129
+ runFlush();
47130
+ } else {
47131
+ scheduleFlush();
47132
+ }
47133
+ };
47134
+ const hydrate = async (id) => {
47135
+ const persisted = await persistent.get(id);
47136
+ if (!persisted) {
47137
+ return;
47138
+ }
47139
+ await memory.set(id, persisted);
47140
+ persistedTurnCount.set(id, persisted.turns.length);
47141
+ return persisted;
47142
+ };
47143
+ const get = async (id) => {
47144
+ const live = await memory.get(id);
47145
+ if (live) {
47146
+ return live;
47147
+ }
47148
+ return hydrate(id);
47149
+ };
47150
+ const getOrCreate = async (id) => {
47151
+ const existing = await get(id);
47152
+ if (existing) {
47153
+ return existing;
47154
+ }
47155
+ return memory.getOrCreate(id);
47156
+ };
47157
+ const set = async (id, value) => {
47158
+ await memory.set(id, value);
47159
+ markDirty(id, value);
47160
+ };
47161
+ const remove = async (id) => {
47162
+ pending.delete(id);
47163
+ persistedTurnCount.delete(id);
47164
+ await memory.remove(id);
47165
+ await persistent.remove(id);
47166
+ };
47167
+ const list = () => memory.list();
47168
+ const flush = async () => {
47169
+ if (timer) {
47170
+ clearTimeout(timer);
47171
+ timer = null;
47172
+ }
47173
+ for (let pass = 0;pass < MAX_FLUSH_DRAIN_PASSES && pending.size > 0; pass += 1) {
47174
+ await runFlush();
47175
+ }
47176
+ await flushChain;
47177
+ };
47178
+ const dispose = () => {
47179
+ if (timer) {
47180
+ clearTimeout(timer);
47181
+ timer = null;
47182
+ }
47183
+ };
47184
+ return { dispose, flush, get, getOrCreate, list, remove, set };
47185
+ };
47079
47186
  // src/core/scribe.ts
47080
47187
  var createVoiceScribe = async (options) => {
47081
47188
  const session = await options.stt.open({
@@ -52916,6 +53023,7 @@ export {
52916
53023
  createVoiceZendeskTicketUpdateSink,
52917
53024
  createVoiceZendeskTicketSyncSinks,
52918
53025
  createVoiceZendeskTicketSink,
53026
+ createVoiceWriteBehindStore,
52919
53027
  createVoiceWorkflowScenario,
52920
53028
  createVoiceWorkflowContractPreset,
52921
53029
  createVoiceWorkflowContractHandler,
@@ -11433,9 +11433,11 @@ var createVoiceBrowserMediaReporter = (options) => {
11433
11433
  var WS_OPEN = 1;
11434
11434
  var WS_CLOSED = 3;
11435
11435
  var WS_NORMAL_CLOSURE = 1000;
11436
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
11436
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
11437
11437
  var DEFAULT_PING_INTERVAL = 30000;
11438
- var RECONNECT_DELAY_MS = 500;
11438
+ var RECONNECT_BASE_DELAY_MS = 500;
11439
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
11440
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
11439
11441
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
11440
11442
  var noop = () => {};
11441
11443
  var noopUnsubscribe = () => noop;
@@ -11504,7 +11506,9 @@ var createVoiceConnection = (path, options = {}) => {
11504
11506
  const listeners = new Set;
11505
11507
  const shouldReconnect = options.reconnect !== false;
11506
11508
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
11509
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
11507
11510
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
11511
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
11508
11512
  const state = {
11509
11513
  isConnected: false,
11510
11514
  pendingMessages: [],
@@ -11540,8 +11544,9 @@ var createVoiceConnection = (path, options = {}) => {
11540
11544
  }
11541
11545
  };
11542
11546
  const scheduleReconnect = () => {
11543
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
11544
11547
  state.reconnectAttempts += 1;
11548
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
11549
+ const nextAttemptAt = Date.now() + delayMs;
11545
11550
  emitConnection({
11546
11551
  reconnect: {
11547
11552
  attempts: state.reconnectAttempts,
@@ -11565,7 +11570,7 @@ var createVoiceConnection = (path, options = {}) => {
11565
11570
  return;
11566
11571
  }
11567
11572
  connect();
11568
- }, RECONNECT_DELAY_MS);
11573
+ }, delayMs);
11569
11574
  };
11570
11575
  const connect = () => {
11571
11576
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -11706,6 +11711,27 @@ var createInitialReconnectState = () => ({
11706
11711
  maxAttempts: 0,
11707
11712
  status: "idle"
11708
11713
  });
11714
+ var appendSegmentText = (accumulated, next) => {
11715
+ const nextText = next.trim().replace(/\s+/g, " ");
11716
+ if (!nextText)
11717
+ return accumulated;
11718
+ if (!accumulated)
11719
+ return nextText;
11720
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
11721
+ return accumulated;
11722
+ }
11723
+ if (nextText.includes(accumulated))
11724
+ return nextText;
11725
+ return `${accumulated} ${nextText}`;
11726
+ };
11727
+ var joinPartial = (finalized, interim) => {
11728
+ const interimText = interim.trim().replace(/\s+/g, " ");
11729
+ if (!finalized)
11730
+ return interimText;
11731
+ if (!interimText || finalized.endsWith(interimText))
11732
+ return finalized;
11733
+ return `${finalized} ${interimText}`;
11734
+ };
11709
11735
  var createInitialState = () => ({
11710
11736
  assistantAudio: [],
11711
11737
  assistantStreamingText: "",
@@ -11723,6 +11749,7 @@ var createInitialState = () => ({
11723
11749
  });
11724
11750
  var createVoiceStreamStore = () => {
11725
11751
  let state = createInitialState();
11752
+ let turnFinalText = "";
11726
11753
  const subscribers = new Set;
11727
11754
  const notify = () => {
11728
11755
  subscribers.forEach((subscriber) => subscriber());
@@ -11808,19 +11835,20 @@ var createVoiceStreamStore = () => {
11808
11835
  };
11809
11836
  break;
11810
11837
  case "final":
11838
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
11811
11839
  state = {
11812
11840
  ...state,
11813
- partial: action.transcript.text,
11814
- turns: state.turns.map((turn) => turn)
11841
+ partial: turnFinalText
11815
11842
  };
11816
11843
  break;
11817
11844
  case "partial":
11818
11845
  state = {
11819
11846
  ...state,
11820
- partial: action.transcript.text
11847
+ partial: joinPartial(turnFinalText, action.transcript.text)
11821
11848
  };
11822
11849
  break;
11823
11850
  case "replay":
11851
+ turnFinalText = action.partial;
11824
11852
  state = {
11825
11853
  ...state,
11826
11854
  assistantStreamingText: "",
@@ -11854,6 +11882,7 @@ var createVoiceStreamStore = () => {
11854
11882
  };
11855
11883
  break;
11856
11884
  case "turn":
11885
+ turnFinalText = "";
11857
11886
  state = {
11858
11887
  ...state,
11859
11888
  partial: "",
@@ -756,9 +756,11 @@ var createVoiceBrowserMediaReporter = (options) => {
756
756
  var WS_OPEN = 1;
757
757
  var WS_CLOSED = 3;
758
758
  var WS_NORMAL_CLOSURE = 1000;
759
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
759
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
760
760
  var DEFAULT_PING_INTERVAL = 30000;
761
- var RECONNECT_DELAY_MS = 500;
761
+ var RECONNECT_BASE_DELAY_MS = 500;
762
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
763
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
762
764
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
763
765
  var noop = () => {};
764
766
  var noopUnsubscribe = () => noop;
@@ -827,7 +829,9 @@ var createVoiceConnection = (path, options = {}) => {
827
829
  const listeners = new Set;
828
830
  const shouldReconnect = options.reconnect !== false;
829
831
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
832
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
830
833
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
834
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
831
835
  const state = {
832
836
  isConnected: false,
833
837
  pendingMessages: [],
@@ -863,8 +867,9 @@ var createVoiceConnection = (path, options = {}) => {
863
867
  }
864
868
  };
865
869
  const scheduleReconnect = () => {
866
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
867
870
  state.reconnectAttempts += 1;
871
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
872
+ const nextAttemptAt = Date.now() + delayMs;
868
873
  emitConnection({
869
874
  reconnect: {
870
875
  attempts: state.reconnectAttempts,
@@ -888,7 +893,7 @@ var createVoiceConnection = (path, options = {}) => {
888
893
  return;
889
894
  }
890
895
  connect();
891
- }, RECONNECT_DELAY_MS);
896
+ }, delayMs);
892
897
  };
893
898
  const connect = () => {
894
899
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -1029,6 +1034,27 @@ var createInitialReconnectState = () => ({
1029
1034
  maxAttempts: 0,
1030
1035
  status: "idle"
1031
1036
  });
1037
+ var appendSegmentText = (accumulated, next) => {
1038
+ const nextText = next.trim().replace(/\s+/g, " ");
1039
+ if (!nextText)
1040
+ return accumulated;
1041
+ if (!accumulated)
1042
+ return nextText;
1043
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
1044
+ return accumulated;
1045
+ }
1046
+ if (nextText.includes(accumulated))
1047
+ return nextText;
1048
+ return `${accumulated} ${nextText}`;
1049
+ };
1050
+ var joinPartial = (finalized, interim) => {
1051
+ const interimText = interim.trim().replace(/\s+/g, " ");
1052
+ if (!finalized)
1053
+ return interimText;
1054
+ if (!interimText || finalized.endsWith(interimText))
1055
+ return finalized;
1056
+ return `${finalized} ${interimText}`;
1057
+ };
1032
1058
  var createInitialState = () => ({
1033
1059
  assistantAudio: [],
1034
1060
  assistantStreamingText: "",
@@ -1046,6 +1072,7 @@ var createInitialState = () => ({
1046
1072
  });
1047
1073
  var createVoiceStreamStore = () => {
1048
1074
  let state = createInitialState();
1075
+ let turnFinalText = "";
1049
1076
  const subscribers = new Set;
1050
1077
  const notify = () => {
1051
1078
  subscribers.forEach((subscriber) => subscriber());
@@ -1131,19 +1158,20 @@ var createVoiceStreamStore = () => {
1131
1158
  };
1132
1159
  break;
1133
1160
  case "final":
1161
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
1134
1162
  state = {
1135
1163
  ...state,
1136
- partial: action.transcript.text,
1137
- turns: state.turns.map((turn) => turn)
1164
+ partial: turnFinalText
1138
1165
  };
1139
1166
  break;
1140
1167
  case "partial":
1141
1168
  state = {
1142
1169
  ...state,
1143
- partial: action.transcript.text
1170
+ partial: joinPartial(turnFinalText, action.transcript.text)
1144
1171
  };
1145
1172
  break;
1146
1173
  case "replay":
1174
+ turnFinalText = action.partial;
1147
1175
  state = {
1148
1176
  ...state,
1149
1177
  assistantStreamingText: "",
@@ -1177,6 +1205,7 @@ var createVoiceStreamStore = () => {
1177
1205
  };
1178
1206
  break;
1179
1207
  case "turn":
1208
+ turnFinalText = "";
1180
1209
  state = {
1181
1210
  ...state,
1182
1211
  partial: "",
@@ -2506,9 +2506,11 @@ var createVoiceBrowserMediaReporter = (options) => {
2506
2506
  var WS_OPEN = 1;
2507
2507
  var WS_CLOSED = 3;
2508
2508
  var WS_NORMAL_CLOSURE = 1000;
2509
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
2509
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
2510
2510
  var DEFAULT_PING_INTERVAL = 30000;
2511
- var RECONNECT_DELAY_MS = 500;
2511
+ var RECONNECT_BASE_DELAY_MS = 500;
2512
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
2513
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
2512
2514
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
2513
2515
  var noop = () => {};
2514
2516
  var noopUnsubscribe = () => noop;
@@ -2577,7 +2579,9 @@ var createVoiceConnection = (path, options = {}) => {
2577
2579
  const listeners = new Set;
2578
2580
  const shouldReconnect = options.reconnect !== false;
2579
2581
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
2582
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
2580
2583
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
2584
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
2581
2585
  const state = {
2582
2586
  isConnected: false,
2583
2587
  pendingMessages: [],
@@ -2613,8 +2617,9 @@ var createVoiceConnection = (path, options = {}) => {
2613
2617
  }
2614
2618
  };
2615
2619
  const scheduleReconnect = () => {
2616
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
2617
2620
  state.reconnectAttempts += 1;
2621
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
2622
+ const nextAttemptAt = Date.now() + delayMs;
2618
2623
  emitConnection({
2619
2624
  reconnect: {
2620
2625
  attempts: state.reconnectAttempts,
@@ -2638,7 +2643,7 @@ var createVoiceConnection = (path, options = {}) => {
2638
2643
  return;
2639
2644
  }
2640
2645
  connect();
2641
- }, RECONNECT_DELAY_MS);
2646
+ }, delayMs);
2642
2647
  };
2643
2648
  const connect = () => {
2644
2649
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -2779,6 +2784,27 @@ var createInitialReconnectState = () => ({
2779
2784
  maxAttempts: 0,
2780
2785
  status: "idle"
2781
2786
  });
2787
+ var appendSegmentText = (accumulated, next) => {
2788
+ const nextText = next.trim().replace(/\s+/g, " ");
2789
+ if (!nextText)
2790
+ return accumulated;
2791
+ if (!accumulated)
2792
+ return nextText;
2793
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
2794
+ return accumulated;
2795
+ }
2796
+ if (nextText.includes(accumulated))
2797
+ return nextText;
2798
+ return `${accumulated} ${nextText}`;
2799
+ };
2800
+ var joinPartial = (finalized, interim) => {
2801
+ const interimText = interim.trim().replace(/\s+/g, " ");
2802
+ if (!finalized)
2803
+ return interimText;
2804
+ if (!interimText || finalized.endsWith(interimText))
2805
+ return finalized;
2806
+ return `${finalized} ${interimText}`;
2807
+ };
2782
2808
  var createInitialState2 = () => ({
2783
2809
  assistantAudio: [],
2784
2810
  assistantStreamingText: "",
@@ -2796,6 +2822,7 @@ var createInitialState2 = () => ({
2796
2822
  });
2797
2823
  var createVoiceStreamStore = () => {
2798
2824
  let state = createInitialState2();
2825
+ let turnFinalText = "";
2799
2826
  const subscribers = new Set;
2800
2827
  const notify = () => {
2801
2828
  subscribers.forEach((subscriber) => subscriber());
@@ -2881,19 +2908,20 @@ var createVoiceStreamStore = () => {
2881
2908
  };
2882
2909
  break;
2883
2910
  case "final":
2911
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
2884
2912
  state = {
2885
2913
  ...state,
2886
- partial: action.transcript.text,
2887
- turns: state.turns.map((turn) => turn)
2914
+ partial: turnFinalText
2888
2915
  };
2889
2916
  break;
2890
2917
  case "partial":
2891
2918
  state = {
2892
2919
  ...state,
2893
- partial: action.transcript.text
2920
+ partial: joinPartial(turnFinalText, action.transcript.text)
2894
2921
  };
2895
2922
  break;
2896
2923
  case "replay":
2924
+ turnFinalText = action.partial;
2897
2925
  state = {
2898
2926
  ...state,
2899
2927
  assistantStreamingText: "",
@@ -2927,6 +2955,7 @@ var createVoiceStreamStore = () => {
2927
2955
  };
2928
2956
  break;
2929
2957
  case "turn":
2958
+ turnFinalText = "";
2930
2959
  state = {
2931
2960
  ...state,
2932
2961
  partial: "",
package/dist/vue/index.js CHANGED
@@ -10829,9 +10829,11 @@ var createVoiceBrowserMediaReporter = (options) => {
10829
10829
  var WS_OPEN = 1;
10830
10830
  var WS_CLOSED = 3;
10831
10831
  var WS_NORMAL_CLOSURE = 1000;
10832
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
10832
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
10833
10833
  var DEFAULT_PING_INTERVAL = 30000;
10834
- var RECONNECT_DELAY_MS = 500;
10834
+ var RECONNECT_BASE_DELAY_MS = 500;
10835
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
10836
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
10835
10837
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
10836
10838
  var noop = () => {};
10837
10839
  var noopUnsubscribe = () => noop;
@@ -10900,7 +10902,9 @@ var createVoiceConnection = (path, options = {}) => {
10900
10902
  const listeners = new Set;
10901
10903
  const shouldReconnect = options.reconnect !== false;
10902
10904
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
10905
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
10903
10906
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
10907
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
10904
10908
  const state = {
10905
10909
  isConnected: false,
10906
10910
  pendingMessages: [],
@@ -10936,8 +10940,9 @@ var createVoiceConnection = (path, options = {}) => {
10936
10940
  }
10937
10941
  };
10938
10942
  const scheduleReconnect = () => {
10939
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
10940
10943
  state.reconnectAttempts += 1;
10944
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
10945
+ const nextAttemptAt = Date.now() + delayMs;
10941
10946
  emitConnection({
10942
10947
  reconnect: {
10943
10948
  attempts: state.reconnectAttempts,
@@ -10961,7 +10966,7 @@ var createVoiceConnection = (path, options = {}) => {
10961
10966
  return;
10962
10967
  }
10963
10968
  connect();
10964
- }, RECONNECT_DELAY_MS);
10969
+ }, delayMs);
10965
10970
  };
10966
10971
  const connect = () => {
10967
10972
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -11102,6 +11107,27 @@ var createInitialReconnectState = () => ({
11102
11107
  maxAttempts: 0,
11103
11108
  status: "idle"
11104
11109
  });
11110
+ var appendSegmentText = (accumulated, next) => {
11111
+ const nextText = next.trim().replace(/\s+/g, " ");
11112
+ if (!nextText)
11113
+ return accumulated;
11114
+ if (!accumulated)
11115
+ return nextText;
11116
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
11117
+ return accumulated;
11118
+ }
11119
+ if (nextText.includes(accumulated))
11120
+ return nextText;
11121
+ return `${accumulated} ${nextText}`;
11122
+ };
11123
+ var joinPartial = (finalized, interim) => {
11124
+ const interimText = interim.trim().replace(/\s+/g, " ");
11125
+ if (!finalized)
11126
+ return interimText;
11127
+ if (!interimText || finalized.endsWith(interimText))
11128
+ return finalized;
11129
+ return `${finalized} ${interimText}`;
11130
+ };
11105
11131
  var createInitialState = () => ({
11106
11132
  assistantAudio: [],
11107
11133
  assistantStreamingText: "",
@@ -11119,6 +11145,7 @@ var createInitialState = () => ({
11119
11145
  });
11120
11146
  var createVoiceStreamStore = () => {
11121
11147
  let state = createInitialState();
11148
+ let turnFinalText = "";
11122
11149
  const subscribers = new Set;
11123
11150
  const notify = () => {
11124
11151
  subscribers.forEach((subscriber) => subscriber());
@@ -11204,19 +11231,20 @@ var createVoiceStreamStore = () => {
11204
11231
  };
11205
11232
  break;
11206
11233
  case "final":
11234
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
11207
11235
  state = {
11208
11236
  ...state,
11209
- partial: action.transcript.text,
11210
- turns: state.turns.map((turn) => turn)
11237
+ partial: turnFinalText
11211
11238
  };
11212
11239
  break;
11213
11240
  case "partial":
11214
11241
  state = {
11215
11242
  ...state,
11216
- partial: action.transcript.text
11243
+ partial: joinPartial(turnFinalText, action.transcript.text)
11217
11244
  };
11218
11245
  break;
11219
11246
  case "replay":
11247
+ turnFinalText = action.partial;
11220
11248
  state = {
11221
11249
  ...state,
11222
11250
  assistantStreamingText: "",
@@ -11250,6 +11278,7 @@ var createVoiceStreamStore = () => {
11250
11278
  };
11251
11279
  break;
11252
11280
  case "turn":
11281
+ turnFinalText = "";
11253
11282
  state = {
11254
11283
  ...state,
11255
11284
  partial: "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.586",
3
+ "version": "0.0.22-beta.588",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",