@absolutejs/voice 0.0.22-beta.31 → 0.0.22-beta.310

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +3250 -73
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agentSquadContract.d.ts +98 -0
  4. package/dist/angular/index.d.ts +16 -0
  5. package/dist/angular/index.js +3498 -1128
  6. package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
  7. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  8. package/dist/angular/voice-controller.service.d.ts +1 -0
  9. package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
  10. package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
  11. package/dist/angular/voice-live-ops.service.d.ts +11 -0
  12. package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
  13. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  14. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  15. package/dist/angular/voice-platform-coverage.service.d.ts +12 -0
  16. package/dist/angular/voice-proof-trends.service.d.ts +12 -0
  17. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  18. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  19. package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
  20. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  21. package/dist/angular/voice-stream.service.d.ts +1 -0
  22. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  23. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  24. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  25. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  26. package/dist/audit.d.ts +128 -0
  27. package/dist/auditDeliveryRoutes.d.ts +85 -0
  28. package/dist/auditExport.d.ts +34 -0
  29. package/dist/auditRoutes.d.ts +66 -0
  30. package/dist/auditSinks.d.ts +151 -0
  31. package/dist/bargeInRoutes.d.ts +56 -0
  32. package/dist/campaign.d.ts +768 -0
  33. package/dist/campaignDialers.d.ts +111 -0
  34. package/dist/client/actions.d.ts +83 -0
  35. package/dist/client/agentSquadStatus.d.ts +37 -0
  36. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  37. package/dist/client/bargeInMonitor.d.ts +7 -0
  38. package/dist/client/campaignDialerProof.d.ts +23 -0
  39. package/dist/client/deliveryRuntime.d.ts +34 -0
  40. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  41. package/dist/client/duplex.d.ts +1 -1
  42. package/dist/client/htmxBootstrap.js +703 -13
  43. package/dist/client/index.d.ts +70 -0
  44. package/dist/client/index.js +5257 -19
  45. package/dist/client/liveOps.d.ts +22 -0
  46. package/dist/client/liveOpsWidget.d.ts +23 -0
  47. package/dist/client/liveTurnLatency.d.ts +41 -0
  48. package/dist/client/opsActionCenter.d.ts +54 -0
  49. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  50. package/dist/client/opsActionHistory.d.ts +19 -0
  51. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  52. package/dist/client/opsStatus.d.ts +19 -0
  53. package/dist/client/opsStatusWidget.d.ts +40 -0
  54. package/dist/client/platformCoverage.d.ts +19 -0
  55. package/dist/client/platformCoverageWidget.d.ts +37 -0
  56. package/dist/client/proofTrends.d.ts +19 -0
  57. package/dist/client/proofTrendsWidget.d.ts +37 -0
  58. package/dist/client/providerCapabilities.d.ts +19 -0
  59. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  60. package/dist/client/providerContracts.d.ts +19 -0
  61. package/dist/client/providerContractsWidget.d.ts +37 -0
  62. package/dist/client/providerSimulationControls.d.ts +33 -0
  63. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  64. package/dist/client/providerStatusWidget.d.ts +32 -0
  65. package/dist/client/readinessFailures.d.ts +19 -0
  66. package/dist/client/readinessFailuresWidget.d.ts +42 -0
  67. package/dist/client/routingStatus.d.ts +19 -0
  68. package/dist/client/routingStatusWidget.d.ts +28 -0
  69. package/dist/client/traceTimeline.d.ts +19 -0
  70. package/dist/client/traceTimelineWidget.d.ts +36 -0
  71. package/dist/client/turnLatency.d.ts +22 -0
  72. package/dist/client/turnLatencyWidget.d.ts +33 -0
  73. package/dist/client/turnQuality.d.ts +19 -0
  74. package/dist/client/turnQualityWidget.d.ts +32 -0
  75. package/dist/client/workflowStatus.d.ts +19 -0
  76. package/dist/competitiveCoverage.d.ts +141 -0
  77. package/dist/dataControl.d.ts +180 -0
  78. package/dist/deliveryRuntime.d.ts +158 -0
  79. package/dist/deliverySinkRoutes.d.ts +117 -0
  80. package/dist/demoReadyRoutes.d.ts +98 -0
  81. package/dist/diagnosticsRoutes.d.ts +44 -0
  82. package/dist/evalRoutes.d.ts +219 -0
  83. package/dist/fileStore.d.ts +14 -2
  84. package/dist/guardrails.d.ts +128 -0
  85. package/dist/incidentBundle.d.ts +116 -0
  86. package/dist/index.d.ts +144 -13
  87. package/dist/index.js +28070 -5219
  88. package/dist/latencySlo.d.ts +56 -0
  89. package/dist/liveLatency.d.ts +78 -0
  90. package/dist/liveOps.d.ts +190 -0
  91. package/dist/mediaPipeline.d.ts +125 -0
  92. package/dist/modelAdapters.d.ts +60 -2
  93. package/dist/observabilityExport.d.ts +481 -0
  94. package/dist/openaiTTS.d.ts +18 -0
  95. package/dist/operationsRecord.d.ts +254 -0
  96. package/dist/opsActionAuditRoutes.d.ts +99 -0
  97. package/dist/opsConsoleRoutes.d.ts +80 -0
  98. package/dist/opsRecovery.d.ts +137 -0
  99. package/dist/opsStatus.d.ts +76 -0
  100. package/dist/opsStatusRoutes.d.ts +33 -0
  101. package/dist/outcomeContract.d.ts +146 -0
  102. package/dist/phoneAgent.d.ts +139 -0
  103. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  104. package/dist/platformCoverage.d.ts +91 -0
  105. package/dist/postCallAnalysis.d.ts +98 -0
  106. package/dist/postgresStore.d.ts +13 -2
  107. package/dist/productionReadiness.d.ts +559 -0
  108. package/dist/proofTrends.d.ts +133 -0
  109. package/dist/providerAdapters.d.ts +48 -0
  110. package/dist/providerCapabilities.d.ts +92 -0
  111. package/dist/providerDecisionTraces.d.ts +130 -0
  112. package/dist/providerHealth.d.ts +1 -0
  113. package/dist/providerOrchestration.d.ts +109 -0
  114. package/dist/providerRoutingContract.d.ts +71 -0
  115. package/dist/providerSlo.d.ts +142 -0
  116. package/dist/providerStackRecommendations.d.ts +187 -0
  117. package/dist/qualityRoutes.d.ts +76 -0
  118. package/dist/queue.d.ts +9 -0
  119. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  120. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  121. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  122. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  123. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  124. package/dist/react/VoiceProofTrends.d.ts +6 -0
  125. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  126. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  127. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  128. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  129. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  130. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  131. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  132. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  133. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  134. package/dist/react/index.d.ts +32 -0
  135. package/dist/react/index.js +5059 -31
  136. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  137. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  138. package/dist/react/useVoiceController.d.ts +1 -0
  139. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  140. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  141. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  142. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  143. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  144. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  145. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  146. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  147. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  148. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  149. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  150. package/dist/react/useVoiceStream.d.ts +1 -0
  151. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  152. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  153. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  154. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  155. package/dist/readinessProfiles.d.ts +38 -0
  156. package/dist/realtimeChannel.d.ts +136 -0
  157. package/dist/realtimeProviderContracts.d.ts +133 -0
  158. package/dist/reconnectContract.d.ts +88 -0
  159. package/dist/resilienceRoutes.d.ts +143 -0
  160. package/dist/sessionReplay.d.ts +12 -0
  161. package/dist/simulationSuite.d.ts +143 -0
  162. package/dist/sloCalibration.d.ts +185 -0
  163. package/dist/sqliteStore.d.ts +13 -2
  164. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  165. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  166. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  167. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  168. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  169. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  170. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  171. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  172. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  173. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  174. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  175. package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
  176. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  177. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  178. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  179. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  180. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  181. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  182. package/dist/svelte/index.d.ts +17 -0
  183. package/dist/svelte/index.js +4924 -420
  184. package/dist/telephony/contract.d.ts +61 -0
  185. package/dist/telephony/matrix.d.ts +97 -0
  186. package/dist/telephony/plivo.d.ts +303 -0
  187. package/dist/telephony/security.d.ts +182 -0
  188. package/dist/telephony/telnyx.d.ts +291 -0
  189. package/dist/telephony/twilio.d.ts +135 -2
  190. package/dist/telephonyOutcome.d.ts +273 -0
  191. package/dist/testing/index.d.ts +1 -0
  192. package/dist/testing/index.js +1767 -44
  193. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  194. package/dist/toolContract.d.ts +161 -0
  195. package/dist/toolRuntime.d.ts +50 -0
  196. package/dist/trace.d.ts +19 -1
  197. package/dist/traceDeliveryRoutes.d.ts +86 -0
  198. package/dist/traceTimeline.d.ts +97 -0
  199. package/dist/turnLatency.d.ts +95 -0
  200. package/dist/turnQuality.d.ts +94 -0
  201. package/dist/types.d.ts +97 -3
  202. package/dist/voiceMonitoring.d.ts +444 -0
  203. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  204. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  205. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  206. package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
  207. package/dist/vue/VoiceProofTrends.d.ts +21 -0
  208. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  209. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  210. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  211. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  212. package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
  213. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  214. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  215. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  216. package/dist/vue/index.d.ts +30 -0
  217. package/dist/vue/index.js +4828 -56
  218. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  219. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  220. package/dist/vue/useVoiceController.d.ts +2 -1
  221. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  222. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  223. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  224. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  225. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  226. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  227. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  228. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  229. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  230. package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
  231. package/dist/vue/useVoiceReadinessFailures.d.ts +775 -0
  232. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  233. package/dist/vue/useVoiceStream.d.ts +2 -1
  234. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  235. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  236. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  237. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  238. package/dist/workflowContract.d.ts +91 -0
  239. package/package.json +1 -1
@@ -188,6 +188,11 @@ var serverMessageToAction = (message) => {
188
188
  sessionId: message.sessionId,
189
189
  type: "complete"
190
190
  };
191
+ case "connection":
192
+ return {
193
+ reconnect: message.reconnect,
194
+ type: "connection"
195
+ };
191
196
  case "call_lifecycle":
192
197
  return {
193
198
  event: message.event,
@@ -209,6 +214,17 @@ var serverMessageToAction = (message) => {
209
214
  transcript: message.transcript,
210
215
  type: "partial"
211
216
  };
217
+ case "replay":
218
+ return {
219
+ assistantTexts: message.assistantTexts,
220
+ call: message.call,
221
+ partial: message.partial,
222
+ scenarioId: message.scenarioId,
223
+ sessionId: message.sessionId,
224
+ status: message.status,
225
+ turns: message.turns,
226
+ type: "replay"
227
+ };
212
228
  case "session":
213
229
  return {
214
230
  sessionId: message.sessionId,
@@ -269,10 +285,12 @@ var isVoiceServerMessage = (value) => {
269
285
  case "assistant":
270
286
  case "call_lifecycle":
271
287
  case "complete":
288
+ case "connection":
272
289
  case "error":
273
290
  case "final":
274
291
  case "partial":
275
292
  case "pong":
293
+ case "replay":
276
294
  case "session":
277
295
  case "turn":
278
296
  return true;
@@ -309,6 +327,9 @@ var createVoiceConnection = (path, options = {}) => {
309
327
  sessionId: options.sessionId ?? createSessionId(),
310
328
  ws: null
311
329
  };
330
+ const emitConnection = (reconnect) => {
331
+ listeners.forEach((listener) => listener(reconnect));
332
+ };
312
333
  const clearTimers = () => {
313
334
  if (state.pingInterval) {
314
335
  clearInterval(state.pingInterval);
@@ -331,9 +352,28 @@ var createVoiceConnection = (path, options = {}) => {
331
352
  }
332
353
  };
333
354
  const scheduleReconnect = () => {
355
+ const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
334
356
  state.reconnectAttempts += 1;
357
+ emitConnection({
358
+ reconnect: {
359
+ attempts: state.reconnectAttempts,
360
+ lastDisconnectAt: Date.now(),
361
+ maxAttempts: maxReconnectAttempts,
362
+ nextAttemptAt,
363
+ status: "reconnecting"
364
+ },
365
+ type: "connection"
366
+ });
335
367
  state.reconnectTimeout = setTimeout(() => {
336
368
  if (state.reconnectAttempts > maxReconnectAttempts) {
369
+ emitConnection({
370
+ reconnect: {
371
+ attempts: state.reconnectAttempts,
372
+ maxAttempts: maxReconnectAttempts,
373
+ status: "exhausted"
374
+ },
375
+ type: "connection"
376
+ });
337
377
  return;
338
378
  }
339
379
  connect();
@@ -343,9 +383,21 @@ var createVoiceConnection = (path, options = {}) => {
343
383
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
344
384
  ws.binaryType = "arraybuffer";
345
385
  ws.onopen = () => {
386
+ const wasReconnecting = state.reconnectAttempts > 0;
346
387
  state.isConnected = true;
347
- state.reconnectAttempts = 0;
348
388
  flushPendingMessages();
389
+ if (wasReconnecting) {
390
+ emitConnection({
391
+ reconnect: {
392
+ attempts: state.reconnectAttempts,
393
+ lastResumedAt: Date.now(),
394
+ maxAttempts: maxReconnectAttempts,
395
+ status: "resumed"
396
+ },
397
+ type: "connection"
398
+ });
399
+ state.reconnectAttempts = 0;
400
+ }
349
401
  listeners.forEach((listener) => listener({
350
402
  scenarioId: state.scenarioId ?? undefined,
351
403
  sessionId: state.sessionId,
@@ -375,6 +427,16 @@ var createVoiceConnection = (path, options = {}) => {
375
427
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
376
428
  if (reconnectable) {
377
429
  scheduleReconnect();
430
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
431
+ emitConnection({
432
+ reconnect: {
433
+ attempts: state.reconnectAttempts,
434
+ lastDisconnectAt: Date.now(),
435
+ maxAttempts: maxReconnectAttempts,
436
+ status: "exhausted"
437
+ },
438
+ type: "connection"
439
+ });
378
440
  }
379
441
  };
380
442
  state.ws = ws;
@@ -445,6 +507,11 @@ var createVoiceConnection = (path, options = {}) => {
445
507
  };
446
508
 
447
509
  // src/client/store.ts
510
+ var createInitialReconnectState = () => ({
511
+ attempts: 0,
512
+ maxAttempts: 0,
513
+ status: "idle"
514
+ });
448
515
  var createInitialState = () => ({
449
516
  assistantAudio: [],
450
517
  assistantTexts: [],
@@ -453,6 +520,7 @@ var createInitialState = () => ({
453
520
  isConnected: false,
454
521
  scenarioId: null,
455
522
  partial: "",
523
+ reconnect: createInitialReconnectState(),
456
524
  sessionId: null,
457
525
  status: "idle",
458
526
  turns: []
@@ -509,7 +577,19 @@ var createVoiceStreamStore = () => {
509
577
  case "connected":
510
578
  state = {
511
579
  ...state,
512
- isConnected: true
580
+ isConnected: true,
581
+ reconnect: state.reconnect.status === "reconnecting" ? {
582
+ ...state.reconnect,
583
+ lastResumedAt: Date.now(),
584
+ nextAttemptAt: undefined,
585
+ status: "resumed"
586
+ } : state.reconnect
587
+ };
588
+ break;
589
+ case "connection":
590
+ state = {
591
+ ...state,
592
+ reconnect: action.reconnect
513
593
  };
514
594
  break;
515
595
  case "disconnected":
@@ -537,6 +617,26 @@ var createVoiceStreamStore = () => {
537
617
  partial: action.transcript.text
538
618
  };
539
619
  break;
620
+ case "replay":
621
+ state = {
622
+ ...state,
623
+ assistantTexts: [...action.assistantTexts],
624
+ call: action.call ?? null,
625
+ error: null,
626
+ isConnected: action.status === "active",
627
+ partial: action.partial,
628
+ reconnect: state.reconnect.status === "reconnecting" ? {
629
+ ...state.reconnect,
630
+ lastResumedAt: Date.now(),
631
+ nextAttemptAt: undefined,
632
+ status: "resumed"
633
+ } : state.reconnect,
634
+ scenarioId: action.scenarioId ?? state.scenarioId,
635
+ sessionId: action.sessionId,
636
+ status: action.status,
637
+ turns: [...action.turns]
638
+ };
639
+ break;
540
640
  case "session":
541
641
  state = {
542
642
  ...state,
@@ -584,10 +684,34 @@ var createVoiceStream = (path, options = {}) => {
584
684
  const notify = () => {
585
685
  subscribers.forEach((subscriber) => subscriber());
586
686
  };
687
+ const reportReconnect = () => {
688
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
689
+ return;
690
+ }
691
+ const snapshot = store.getSnapshot();
692
+ const body = JSON.stringify({
693
+ at: Date.now(),
694
+ reconnect: snapshot.reconnect,
695
+ scenarioId: snapshot.scenarioId,
696
+ sessionId: connection.getSessionId(),
697
+ turnIds: snapshot.turns.map((turn) => turn.id)
698
+ });
699
+ fetch(options.reconnectReportPath, {
700
+ body,
701
+ headers: {
702
+ "Content-Type": "application/json"
703
+ },
704
+ keepalive: true,
705
+ method: "POST"
706
+ }).catch(() => {});
707
+ };
587
708
  const unsubscribeConnection = connection.subscribe((message) => {
588
709
  const action = serverMessageToAction(message);
589
710
  if (action) {
590
711
  store.dispatch(action);
712
+ if (message.type === "connection") {
713
+ reportReconnect();
714
+ }
591
715
  notify();
592
716
  }
593
717
  });
@@ -623,6 +747,9 @@ var createVoiceStream = (path, options = {}) => {
623
747
  get partial() {
624
748
  return store.getSnapshot().partial;
625
749
  },
750
+ get reconnect() {
751
+ return store.getSnapshot().reconnect;
752
+ },
626
753
  get sessionId() {
627
754
  return connection.getSessionId();
628
755
  },
@@ -941,6 +1068,7 @@ var createInitialState2 = (stream) => ({
941
1068
  isConnected: stream.isConnected,
942
1069
  isRecording: false,
943
1070
  partial: stream.partial,
1071
+ reconnect: stream.reconnect,
944
1072
  recordingError: null,
945
1073
  sessionId: stream.sessionId,
946
1074
  scenarioId: stream.scenarioId,
@@ -970,6 +1098,7 @@ var createVoiceController = (path, options = {}) => {
970
1098
  error: stream.error,
971
1099
  isConnected: stream.isConnected,
972
1100
  partial: stream.partial,
1101
+ reconnect: stream.reconnect,
973
1102
  sessionId: stream.sessionId,
974
1103
  scenarioId: stream.scenarioId,
975
1104
  status: stream.status,
@@ -994,7 +1123,13 @@ var createVoiceController = (path, options = {}) => {
994
1123
  capture = createMicrophoneCapture({
995
1124
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
1125
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
1126
+ onAudio: (audio) => {
1127
+ if (options.capture?.onAudio) {
1128
+ options.capture.onAudio(audio, stream.sendAudio);
1129
+ return;
1130
+ }
1131
+ stream.sendAudio(audio);
1132
+ },
998
1133
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
999
1134
  });
1000
1135
  return capture;
@@ -1064,6 +1199,9 @@ var createVoiceController = (path, options = {}) => {
1064
1199
  get recordingError() {
1065
1200
  return state.recordingError;
1066
1201
  },
1202
+ get reconnect() {
1203
+ return state.reconnect;
1204
+ },
1067
1205
  sendAudio: (audio) => stream.sendAudio(audio),
1068
1206
  get sessionId() {
1069
1207
  return state.sessionId;
@@ -1104,6 +1242,475 @@ var createVoiceController = (path, options = {}) => {
1104
1242
  };
1105
1243
  };
1106
1244
 
1245
+ // src/client/audioPlayer.ts
1246
+ var DEFAULT_LOOKAHEAD_MS = 15;
1247
+ var createInitialState3 = () => ({
1248
+ activeSourceCount: 0,
1249
+ error: null,
1250
+ isActive: false,
1251
+ isPlaying: false,
1252
+ lastInterruptLatencyMs: undefined,
1253
+ lastPlaybackStopLatencyMs: undefined,
1254
+ processedChunkCount: 0,
1255
+ queuedChunkCount: 0
1256
+ });
1257
+ var getAudioContextCtor = () => {
1258
+ if (typeof window === "undefined") {
1259
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1260
+ }
1261
+ return window.AudioContext ?? window.webkitAudioContext;
1262
+ };
1263
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1264
+ const format = chunk.format;
1265
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1266
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1267
+ }
1268
+ const bytes = chunk.chunk;
1269
+ const channels = Math.max(1, format.channels);
1270
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1271
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1272
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1273
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1274
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1275
+ const channelData = audioBuffer.getChannelData(channelIndex);
1276
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1277
+ const sampleIndex = frameIndex * channels + channelIndex;
1278
+ const sampleOffset = sampleIndex * 2;
1279
+ if (sampleOffset + 1 >= bytes.byteLength) {
1280
+ channelData[frameIndex] = 0;
1281
+ continue;
1282
+ }
1283
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1284
+ }
1285
+ }
1286
+ return audioBuffer;
1287
+ };
1288
+ var createVoiceAudioPlayer = (source, options = {}) => {
1289
+ const subscribers = new Set;
1290
+ const sourceNodes = new Set;
1291
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1292
+ let state = createInitialState3();
1293
+ let audioContext = null;
1294
+ let outputNode = null;
1295
+ let queueEndTime = 0;
1296
+ let syncPromise = Promise.resolve();
1297
+ let interruptStartedAt = null;
1298
+ let interruptPromise = null;
1299
+ let resolveInterruptPromise = null;
1300
+ let interruptFallbackTimer = null;
1301
+ const notify = () => {
1302
+ for (const subscriber of subscribers) {
1303
+ subscriber();
1304
+ }
1305
+ };
1306
+ const setState = (next) => {
1307
+ state = {
1308
+ ...state,
1309
+ ...next
1310
+ };
1311
+ notify();
1312
+ };
1313
+ const clearError = () => {
1314
+ if (state.error !== null) {
1315
+ setState({ error: null });
1316
+ }
1317
+ };
1318
+ const clearInterruptTimer = () => {
1319
+ if (interruptFallbackTimer !== null) {
1320
+ clearTimeout(interruptFallbackTimer);
1321
+ interruptFallbackTimer = null;
1322
+ }
1323
+ };
1324
+ const resolveInterrupt = (latencyMs) => {
1325
+ clearInterruptTimer();
1326
+ interruptStartedAt = null;
1327
+ setState({
1328
+ activeSourceCount: sourceNodes.size,
1329
+ isPlaying: false,
1330
+ lastInterruptLatencyMs: latencyMs,
1331
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1332
+ });
1333
+ resolveInterruptPromise?.();
1334
+ resolveInterruptPromise = null;
1335
+ interruptPromise = null;
1336
+ };
1337
+ const estimateOutputStopLatencyMs = (context) => {
1338
+ if (!context) {
1339
+ return 0;
1340
+ }
1341
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1342
+ };
1343
+ const restoreOutputGain = (context) => {
1344
+ if (!outputNode) {
1345
+ return;
1346
+ }
1347
+ const gainValue = 1;
1348
+ if (outputNode.gain.setValueAtTime) {
1349
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1350
+ return;
1351
+ }
1352
+ outputNode.gain.value = gainValue;
1353
+ };
1354
+ const muteOutputGain = (context) => {
1355
+ if (!outputNode) {
1356
+ return;
1357
+ }
1358
+ const gainValue = 0;
1359
+ if (outputNode.gain.setValueAtTime) {
1360
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1361
+ return;
1362
+ }
1363
+ outputNode.gain.value = gainValue;
1364
+ };
1365
+ const maybeResolveInterrupt = () => {
1366
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1367
+ return;
1368
+ }
1369
+ resolveInterrupt(Date.now() - interruptStartedAt);
1370
+ };
1371
+ const ensureAudioContext = async () => {
1372
+ if (audioContext) {
1373
+ return audioContext;
1374
+ }
1375
+ if (options.createAudioContext) {
1376
+ audioContext = options.createAudioContext();
1377
+ } else {
1378
+ const AudioContextCtor = getAudioContextCtor();
1379
+ if (!AudioContextCtor) {
1380
+ throw new Error("Assistant audio playback requires AudioContext support.");
1381
+ }
1382
+ audioContext = new AudioContextCtor;
1383
+ }
1384
+ if (audioContext.createGain) {
1385
+ outputNode = audioContext.createGain();
1386
+ outputNode.connect?.(audioContext.destination);
1387
+ }
1388
+ queueEndTime = audioContext.currentTime;
1389
+ return audioContext;
1390
+ };
1391
+ const scheduleChunk = async (chunk) => {
1392
+ const context = await ensureAudioContext();
1393
+ const buffer = decodePCM16LEChunk(context, chunk);
1394
+ const node = context.createBufferSource();
1395
+ node.buffer = buffer;
1396
+ node.connect(outputNode ?? context.destination);
1397
+ node.onended = () => {
1398
+ sourceNodes.delete(node);
1399
+ node.disconnect?.();
1400
+ setState({
1401
+ activeSourceCount: sourceNodes.size,
1402
+ isPlaying: sourceNodes.size > 0 && state.isActive
1403
+ });
1404
+ maybeResolveInterrupt();
1405
+ };
1406
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1407
+ queueEndTime = startAt + buffer.duration;
1408
+ sourceNodes.add(node);
1409
+ setState({
1410
+ activeSourceCount: sourceNodes.size,
1411
+ isPlaying: true
1412
+ });
1413
+ node.start(startAt);
1414
+ };
1415
+ const stopQueuedPlayback = (options2) => {
1416
+ for (const node of [...sourceNodes]) {
1417
+ node.stop?.();
1418
+ }
1419
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1420
+ if (options2?.forceClear) {
1421
+ for (const node of sourceNodes) {
1422
+ node.disconnect?.();
1423
+ }
1424
+ sourceNodes.clear();
1425
+ maybeResolveInterrupt();
1426
+ }
1427
+ };
1428
+ const sync = async () => {
1429
+ if (!state.isActive) {
1430
+ return;
1431
+ }
1432
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1433
+ if (nextChunks.length === 0) {
1434
+ return;
1435
+ }
1436
+ try {
1437
+ clearError();
1438
+ for (const chunk of nextChunks) {
1439
+ await scheduleChunk(chunk);
1440
+ }
1441
+ setState({
1442
+ processedChunkCount: source.assistantAudio.length,
1443
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1444
+ });
1445
+ } catch (error) {
1446
+ setState({
1447
+ error: error instanceof Error ? error.message : String(error)
1448
+ });
1449
+ }
1450
+ };
1451
+ const queueSync = () => {
1452
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1453
+ return syncPromise;
1454
+ };
1455
+ const unsubscribeSource = source.subscribe(() => {
1456
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1457
+ player.start();
1458
+ return;
1459
+ }
1460
+ if (state.isActive) {
1461
+ queueSync();
1462
+ }
1463
+ });
1464
+ const player = {
1465
+ close: async () => {
1466
+ unsubscribeSource();
1467
+ stopQueuedPlayback({ forceClear: true });
1468
+ clearInterruptTimer();
1469
+ resolveInterruptPromise?.();
1470
+ resolveInterruptPromise = null;
1471
+ interruptPromise = null;
1472
+ interruptStartedAt = null;
1473
+ if (audioContext && audioContext.state !== "closed") {
1474
+ await audioContext.close();
1475
+ }
1476
+ audioContext = null;
1477
+ outputNode?.disconnect?.();
1478
+ outputNode = null;
1479
+ queueEndTime = 0;
1480
+ setState({
1481
+ activeSourceCount: 0,
1482
+ isActive: false,
1483
+ isPlaying: false
1484
+ });
1485
+ },
1486
+ get activeSourceCount() {
1487
+ return state.activeSourceCount;
1488
+ },
1489
+ get error() {
1490
+ return state.error;
1491
+ },
1492
+ getSnapshot: () => state,
1493
+ get isActive() {
1494
+ return state.isActive;
1495
+ },
1496
+ get isPlaying() {
1497
+ return state.isPlaying;
1498
+ },
1499
+ interrupt: async () => {
1500
+ const startedAt = Date.now();
1501
+ const context = await ensureAudioContext();
1502
+ interruptStartedAt = startedAt;
1503
+ muteOutputGain(context);
1504
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
1505
+ setState({
1506
+ isActive: false,
1507
+ isPlaying: sourceNodes.size > 0,
1508
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
1509
+ });
1510
+ if (sourceNodes.size === 0) {
1511
+ resolveInterrupt(playbackStopLatencyMs);
1512
+ return;
1513
+ }
1514
+ if (!interruptPromise) {
1515
+ interruptPromise = new Promise((resolve) => {
1516
+ resolveInterruptPromise = resolve;
1517
+ });
1518
+ }
1519
+ clearInterruptTimer();
1520
+ interruptFallbackTimer = setTimeout(() => {
1521
+ for (const node of sourceNodes) {
1522
+ node.disconnect?.();
1523
+ }
1524
+ sourceNodes.clear();
1525
+ resolveInterrupt(Date.now() - startedAt);
1526
+ }, 250);
1527
+ stopQueuedPlayback();
1528
+ await interruptPromise;
1529
+ },
1530
+ get lastInterruptLatencyMs() {
1531
+ return state.lastInterruptLatencyMs;
1532
+ },
1533
+ get lastPlaybackStopLatencyMs() {
1534
+ return state.lastPlaybackStopLatencyMs;
1535
+ },
1536
+ pause: async () => {
1537
+ if (!audioContext) {
1538
+ setState({
1539
+ activeSourceCount: 0,
1540
+ isActive: false,
1541
+ isPlaying: false
1542
+ });
1543
+ return;
1544
+ }
1545
+ await audioContext.suspend();
1546
+ setState({
1547
+ activeSourceCount: sourceNodes.size,
1548
+ isActive: false,
1549
+ isPlaying: false
1550
+ });
1551
+ },
1552
+ get processedChunkCount() {
1553
+ return state.processedChunkCount;
1554
+ },
1555
+ get queuedChunkCount() {
1556
+ return state.queuedChunkCount;
1557
+ },
1558
+ start: async () => {
1559
+ try {
1560
+ clearError();
1561
+ const context = await ensureAudioContext();
1562
+ restoreOutputGain(context);
1563
+ if (context.state === "suspended") {
1564
+ await context.resume();
1565
+ }
1566
+ setState({
1567
+ activeSourceCount: sourceNodes.size,
1568
+ isActive: true,
1569
+ isPlaying: context.state === "running"
1570
+ });
1571
+ await queueSync();
1572
+ } catch (error) {
1573
+ setState({
1574
+ error: error instanceof Error ? error.message : String(error),
1575
+ isActive: false,
1576
+ isPlaying: false
1577
+ });
1578
+ throw error;
1579
+ }
1580
+ },
1581
+ subscribe: (subscriber) => {
1582
+ subscribers.add(subscriber);
1583
+ return () => {
1584
+ subscribers.delete(subscriber);
1585
+ };
1586
+ }
1587
+ };
1588
+ return player;
1589
+ };
1590
+
1591
+ // src/client/bargeInMonitor.ts
1592
+ var DEFAULT_THRESHOLD_MS = 250;
1593
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1594
+ var summarize = (events, thresholdMs) => {
1595
+ const stopped = events.filter((event) => event.status === "stopped");
1596
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1597
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1598
+ const passed = stopped.length - failed;
1599
+ return {
1600
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1601
+ events: [...events],
1602
+ failed,
1603
+ lastEvent: events.at(-1),
1604
+ passed,
1605
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1606
+ thresholdMs,
1607
+ total: stopped.length
1608
+ };
1609
+ };
1610
+ var createVoiceBargeInMonitor = (options = {}) => {
1611
+ const listeners = new Set;
1612
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1613
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1614
+ const events = [];
1615
+ const emit = () => {
1616
+ for (const listener of listeners) {
1617
+ listener();
1618
+ }
1619
+ };
1620
+ const postEvent = (event) => {
1621
+ if (!options.path || typeof fetchImpl !== "function") {
1622
+ return;
1623
+ }
1624
+ fetchImpl(options.path, {
1625
+ body: JSON.stringify(event),
1626
+ headers: {
1627
+ "Content-Type": "application/json"
1628
+ },
1629
+ method: "POST"
1630
+ }).catch(() => {});
1631
+ };
1632
+ const record = (status, input) => {
1633
+ const event = {
1634
+ at: Date.now(),
1635
+ id: createEventId(),
1636
+ latencyMs: input.latencyMs,
1637
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1638
+ reason: input.reason,
1639
+ sessionId: input.sessionId,
1640
+ status,
1641
+ thresholdMs
1642
+ };
1643
+ events.push(event);
1644
+ postEvent(event);
1645
+ emit();
1646
+ return event;
1647
+ };
1648
+ return {
1649
+ getSnapshot: () => summarize(events, thresholdMs),
1650
+ recordRequested: (input) => record("requested", input),
1651
+ recordSkipped: (input) => record("skipped", input),
1652
+ recordStopped: (input) => record("stopped", input),
1653
+ subscribe: (subscriber) => {
1654
+ listeners.add(subscriber);
1655
+ return () => {
1656
+ listeners.delete(subscriber);
1657
+ };
1658
+ }
1659
+ };
1660
+ };
1661
+
1662
+ // src/client/duplex.ts
1663
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1664
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1665
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
1666
+ let lastPartial = controller.partial;
1667
+ const interruptIfPlaying = (reason) => {
1668
+ if (!player.isPlaying || options.enabled === false) {
1669
+ options.monitor?.recordSkipped({
1670
+ reason,
1671
+ sessionId: controller.sessionId
1672
+ });
1673
+ return;
1674
+ }
1675
+ options.monitor?.recordRequested({
1676
+ reason,
1677
+ sessionId: controller.sessionId
1678
+ });
1679
+ player.interrupt().then(() => {
1680
+ options.monitor?.recordStopped({
1681
+ latencyMs: player.lastInterruptLatencyMs,
1682
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1683
+ reason,
1684
+ sessionId: controller.sessionId
1685
+ });
1686
+ });
1687
+ };
1688
+ const unsubscribe = controller.subscribe(() => {
1689
+ if (options.interruptOnPartial === false) {
1690
+ lastPartial = controller.partial;
1691
+ return;
1692
+ }
1693
+ if (!lastPartial && controller.partial) {
1694
+ interruptIfPlaying("partial-transcript");
1695
+ }
1696
+ lastPartial = controller.partial;
1697
+ });
1698
+ return {
1699
+ close: () => {
1700
+ unsubscribe();
1701
+ },
1702
+ handleLevel: (level) => {
1703
+ if (shouldInterruptForLevel(level, options)) {
1704
+ interruptIfPlaying("input-level");
1705
+ }
1706
+ },
1707
+ sendAudio: (audio) => {
1708
+ interruptIfPlaying("manual-audio");
1709
+ controller.sendAudio(audio);
1710
+ }
1711
+ };
1712
+ };
1713
+
1107
1714
  // src/client/htmxBootstrap.ts
1108
1715
  var VOICE_WAVE_POINTS = 48;
1109
1716
  var VOICE_WAVE_WIDTH = 320;
@@ -1160,6 +1767,17 @@ var formatErrorMessage = (error) => {
1160
1767
  }
1161
1768
  return "Unexpected error";
1162
1769
  };
1770
+ var formatReconnectState = (reconnect) => {
1771
+ const pieces = [reconnect.status];
1772
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
1773
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
1774
+ }
1775
+ if (reconnect.nextAttemptAt) {
1776
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
1777
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
1778
+ }
1779
+ return pieces.join(" · ");
1780
+ };
1163
1781
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1164
1782
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1165
1783
  const next = levels.slice(-(count - 1));
@@ -1216,6 +1834,17 @@ var parsePromptList = (value) => {
1216
1834
  } catch {}
1217
1835
  return DEFAULT_GUIDED_PROMPTS;
1218
1836
  };
1837
+ var parseOptionalNumber = (value) => {
1838
+ if (!value) {
1839
+ return;
1840
+ }
1841
+ const parsed = Number(value);
1842
+ return Number.isFinite(parsed) ? parsed : undefined;
1843
+ };
1844
+ var resolveElement2 = (root, selector, ctor) => {
1845
+ const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
1846
+ return value instanceof ctor ? value : null;
1847
+ };
1219
1848
  var requireElement = (root, selector, ctor, name) => {
1220
1849
  const value = selector ? document.querySelector(selector) : null;
1221
1850
  if (value instanceof ctor) {
@@ -1266,11 +1895,20 @@ var initVoiceHTMXRoot = (root) => {
1266
1895
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
1896
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
1897
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
1898
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
1899
+ const bargeInPath = root.dataset.voiceBargeInPath;
1900
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
1901
+ path: bargeInPath,
1902
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
1903
+ }) : null;
1904
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
1905
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
1906
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
1907
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
1908
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1272
1909
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1273
1910
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
1911
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1274
1912
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1275
1913
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1276
1914
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1279,35 +1917,70 @@ var initVoiceHTMXRoot = (root) => {
1279
1917
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
1918
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
1919
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
1920
+ let activeMode = null;
1921
+ let hasStartedModes = {
1922
+ general: false,
1923
+ guided: false
1924
+ };
1925
+ let isCapturing = false;
1926
+ let micError = null;
1927
+ let waveLevels = createInitialVoiceWaveLevels();
1928
+ let guidedBargeInBinding = null;
1929
+ let generalBargeInBinding = null;
1282
1930
  const guidedVoice = createVoiceController(guidedPath, {
1283
1931
  capture: {
1932
+ onAudio: (audio, sendAudio) => {
1933
+ if (guidedBargeInBinding) {
1934
+ guidedBargeInBinding.sendAudio(audio);
1935
+ return;
1936
+ }
1937
+ sendAudio(audio);
1938
+ },
1284
1939
  onLevel: (level) => {
1940
+ guidedBargeInBinding?.handleLevel(level);
1285
1941
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
1942
  renderWave();
1287
1943
  }
1288
1944
  },
1945
+ connection: {
1946
+ reconnectReportPath
1947
+ },
1289
1948
  preset: "guided-intake"
1290
1949
  });
1291
1950
  const generalVoice = createVoiceController(generalPath, {
1292
1951
  capture: {
1952
+ onAudio: (audio, sendAudio) => {
1953
+ if (generalBargeInBinding) {
1954
+ generalBargeInBinding.sendAudio(audio);
1955
+ return;
1956
+ }
1957
+ sendAudio(audio);
1958
+ },
1293
1959
  onLevel: (level) => {
1960
+ generalBargeInBinding?.handleLevel(level);
1294
1961
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
1962
  renderWave();
1296
1963
  }
1297
1964
  },
1965
+ connection: {
1966
+ reconnectReportPath
1967
+ },
1298
1968
  preset: "dictation"
1299
1969
  });
1300
1970
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
1971
  const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
1302
- let activeMode = null;
1303
- let hasStartedModes = {
1304
- general: false,
1305
- guided: false
1306
- };
1307
- let isCapturing = false;
1308
- let micError = null;
1309
- let waveLevels = createInitialVoiceWaveLevels();
1972
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
1973
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
1974
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
1975
+ interruptThreshold: bargeInSpeechThreshold,
1976
+ monitor: bargeInMonitor ?? undefined
1977
+ });
1978
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
1979
+ interruptThreshold: bargeInSpeechThreshold,
1980
+ monitor: bargeInMonitor ?? undefined
1981
+ });
1310
1982
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
1983
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1311
1984
  const renderWave = () => {
1312
1985
  const path = createVoiceWavePath(waveLevels);
1313
1986
  voiceWaveGlow.setAttribute("d", path);
@@ -1322,6 +1995,9 @@ var initVoiceHTMXRoot = (root) => {
1322
1995
  const status = voice.status;
1323
1996
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1324
1997
  errorStatus.textContent = micError || voice.error || "None";
1998
+ if (reconnectStatus) {
1999
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2000
+ }
1325
2001
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1326
2002
  promptStatus.textContent = resolvePromptMessage({
1327
2003
  guidedPrompts,
@@ -1385,8 +2061,18 @@ var initVoiceHTMXRoot = (root) => {
1385
2061
  render();
1386
2062
  }
1387
2063
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
2064
+ guidedVoice.subscribe(() => {
2065
+ if (guidedVoice.assistantAudio.length > 0) {
2066
+ guidedAudioPlayer.start().catch(() => {});
2067
+ }
2068
+ render();
2069
+ });
2070
+ generalVoice.subscribe(() => {
2071
+ if (generalVoice.assistantAudio.length > 0) {
2072
+ generalAudioPlayer.start().catch(() => {});
2073
+ }
2074
+ render();
2075
+ });
1390
2076
  startGuidedButton.addEventListener("click", () => {
1391
2077
  startMode("guided");
1392
2078
  });
@@ -1399,6 +2085,10 @@ var initVoiceHTMXRoot = (root) => {
1399
2085
  window.addEventListener("beforeunload", () => {
1400
2086
  guidedVoice.stopRecording();
1401
2087
  generalVoice.stopRecording();
2088
+ guidedBargeInBinding?.close();
2089
+ generalBargeInBinding?.close();
2090
+ guidedAudioPlayer.close();
2091
+ generalAudioPlayer.close();
1402
2092
  stopGuidedBinding();
1403
2093
  stopGeneralBinding();
1404
2094
  guidedVoice.close();