@absolutejs/voice 0.0.22-beta.44 → 0.0.22-beta.441

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 (328) hide show
  1. package/README.md +4269 -603
  2. package/dist/agent.d.ts +67 -5
  3. package/dist/agentSquadContract.d.ts +98 -0
  4. package/dist/angular/index.d.ts +22 -3
  5. package/dist/angular/index.js +4647 -1140
  6. package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
  7. package/dist/angular/voice-call-debugger.service.d.ts +12 -0
  8. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  9. package/dist/angular/voice-controller.service.d.ts +2 -1
  10. package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
  11. package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
  12. package/dist/angular/voice-live-ops.service.d.ts +11 -0
  13. package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
  14. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  15. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  16. package/dist/angular/voice-platform-coverage.service.d.ts +12 -0
  17. package/dist/angular/voice-profile-comparison.service.d.ts +12 -0
  18. package/dist/angular/voice-proof-trends.service.d.ts +12 -0
  19. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  20. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  21. package/dist/angular/voice-provider-status.service.d.ts +2 -2
  22. package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
  23. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  24. package/dist/angular/voice-session-snapshot.service.d.ts +13 -0
  25. package/dist/angular/voice-stream.service.d.ts +3 -1
  26. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  27. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  28. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  29. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  30. package/dist/assistant.d.ts +11 -11
  31. package/dist/assistantHealth.d.ts +6 -6
  32. package/dist/assistantMemory.d.ts +3 -3
  33. package/dist/audioConditioning.d.ts +1 -1
  34. package/dist/audit.d.ts +131 -0
  35. package/dist/auditDeliveryRoutes.d.ts +85 -0
  36. package/dist/auditExport.d.ts +34 -0
  37. package/dist/auditRoutes.d.ts +66 -0
  38. package/dist/auditSinks.d.ts +151 -0
  39. package/dist/bargeInRoutes.d.ts +56 -0
  40. package/dist/browserCallProfiles.d.ts +120 -0
  41. package/dist/browserMediaRoutes.d.ts +62 -0
  42. package/dist/callDebugger.d.ts +66 -0
  43. package/dist/campaign.d.ts +794 -0
  44. package/dist/campaignDialers.d.ts +111 -0
  45. package/dist/client/actions.d.ts +95 -1
  46. package/dist/client/agentSquadStatus.d.ts +37 -0
  47. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  48. package/dist/client/audioPlayer.d.ts +2 -2
  49. package/dist/client/bargeInMonitor.d.ts +7 -0
  50. package/dist/client/browserMedia.d.ts +8 -0
  51. package/dist/client/callDebugger.d.ts +19 -0
  52. package/dist/client/callDebuggerWidget.d.ts +30 -0
  53. package/dist/client/campaignDialerProof.d.ts +23 -0
  54. package/dist/client/connection.d.ts +3 -3
  55. package/dist/client/controller.d.ts +1 -1
  56. package/dist/client/createVoiceStream.d.ts +1 -1
  57. package/dist/client/deliveryRuntime.d.ts +34 -0
  58. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  59. package/dist/client/duplex.d.ts +2 -2
  60. package/dist/client/htmx.d.ts +1 -1
  61. package/dist/client/htmxBootstrap.js +950 -14
  62. package/dist/client/index.d.ts +96 -9
  63. package/dist/client/index.js +9916 -52
  64. package/dist/client/liveOps.d.ts +22 -0
  65. package/dist/client/liveOpsWidget.d.ts +23 -0
  66. package/dist/client/liveTurnLatency.d.ts +41 -0
  67. package/dist/client/microphone.d.ts +4 -4
  68. package/dist/client/opsActionCenter.d.ts +54 -0
  69. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  70. package/dist/client/opsActionHistory.d.ts +19 -0
  71. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  72. package/dist/client/opsStatus.d.ts +19 -0
  73. package/dist/client/opsStatusWidget.d.ts +40 -0
  74. package/dist/client/platformCoverage.d.ts +19 -0
  75. package/dist/client/platformCoverageWidget.d.ts +37 -0
  76. package/dist/client/profileComparison.d.ts +19 -0
  77. package/dist/client/profileComparisonWidget.d.ts +41 -0
  78. package/dist/client/profileSwitchRecommendation.d.ts +19 -0
  79. package/dist/client/profileSwitchRecommendationWidget.d.ts +12 -0
  80. package/dist/client/proofTrends.d.ts +19 -0
  81. package/dist/client/proofTrendsWidget.d.ts +37 -0
  82. package/dist/client/providerCapabilities.d.ts +19 -0
  83. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  84. package/dist/client/providerContracts.d.ts +19 -0
  85. package/dist/client/providerContractsWidget.d.ts +37 -0
  86. package/dist/client/providerSimulationControls.d.ts +33 -0
  87. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  88. package/dist/client/providerStatus.d.ts +1 -1
  89. package/dist/client/providerStatusWidget.d.ts +32 -0
  90. package/dist/client/readinessFailures.d.ts +19 -0
  91. package/dist/client/readinessFailuresWidget.d.ts +42 -0
  92. package/dist/client/routingStatus.d.ts +19 -0
  93. package/dist/client/routingStatusWidget.d.ts +32 -0
  94. package/dist/client/sessionSnapshot.d.ts +21 -0
  95. package/dist/client/sessionSnapshotWidget.d.ts +33 -0
  96. package/dist/client/store.d.ts +1 -1
  97. package/dist/client/traceTimeline.d.ts +19 -0
  98. package/dist/client/traceTimelineWidget.d.ts +36 -0
  99. package/dist/client/turnLatency.d.ts +22 -0
  100. package/dist/client/turnLatencyWidget.d.ts +33 -0
  101. package/dist/client/turnQuality.d.ts +19 -0
  102. package/dist/client/turnQualityWidget.d.ts +32 -0
  103. package/dist/client/workflowStatus.d.ts +19 -0
  104. package/dist/competitiveCoverage.d.ts +141 -0
  105. package/dist/correction.d.ts +2 -2
  106. package/dist/dataControl.d.ts +180 -0
  107. package/dist/deliveryRuntime.d.ts +158 -0
  108. package/dist/deliverySinkRoutes.d.ts +117 -0
  109. package/dist/demoReadyRoutes.d.ts +98 -0
  110. package/dist/diagnosticsRoutes.d.ts +2 -2
  111. package/dist/evalRoutes.d.ts +47 -4
  112. package/dist/fileStore.d.ts +19 -7
  113. package/dist/guardrails.d.ts +128 -0
  114. package/dist/handoff.d.ts +6 -6
  115. package/dist/handoffHealth.d.ts +5 -5
  116. package/dist/htmx.d.ts +1 -1
  117. package/dist/incidentBundle.d.ts +119 -0
  118. package/dist/incidentTimeline.d.ts +259 -0
  119. package/dist/index.d.ts +215 -70
  120. package/dist/index.js +35848 -6762
  121. package/dist/latencySlo.d.ts +56 -0
  122. package/dist/liveLatency.d.ts +78 -0
  123. package/dist/liveOps.d.ts +190 -0
  124. package/dist/logger.d.ts +1 -1
  125. package/dist/mediaPipelineRoutes.d.ts +117 -0
  126. package/dist/memoryStore.d.ts +1 -1
  127. package/dist/modelAdapters.d.ts +59 -7
  128. package/dist/observabilityExport.d.ts +501 -0
  129. package/dist/openaiTTS.d.ts +18 -0
  130. package/dist/operationalStatus.d.ts +87 -0
  131. package/dist/operationsRecord.d.ts +351 -0
  132. package/dist/ops.d.ts +12 -12
  133. package/dist/opsActionAuditRoutes.d.ts +99 -0
  134. package/dist/opsConsoleRoutes.d.ts +8 -5
  135. package/dist/opsPresets.d.ts +2 -2
  136. package/dist/opsRecovery.d.ts +137 -0
  137. package/dist/opsRuntime.d.ts +6 -6
  138. package/dist/opsSinks.d.ts +13 -13
  139. package/dist/opsStatus.d.ts +76 -0
  140. package/dist/opsStatusRoutes.d.ts +33 -0
  141. package/dist/opsWebhook.d.ts +6 -6
  142. package/dist/outcomeContract.d.ts +146 -0
  143. package/dist/outcomeRecipes.d.ts +4 -4
  144. package/dist/phoneAgent.d.ts +139 -0
  145. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  146. package/dist/platformCoverage.d.ts +91 -0
  147. package/dist/plugin.d.ts +2 -2
  148. package/dist/postCallAnalysis.d.ts +98 -0
  149. package/dist/postgresStore.d.ts +17 -6
  150. package/dist/presets.d.ts +3 -3
  151. package/dist/productionReadiness.d.ts +738 -0
  152. package/dist/profileSwitchRecommendation.d.ts +350 -0
  153. package/dist/proofAssertions.d.ts +32 -0
  154. package/dist/proofPack.d.ts +206 -0
  155. package/dist/proofRunner.d.ts +79 -0
  156. package/dist/proofTrends.d.ts +715 -0
  157. package/dist/providerAdapters.d.ts +16 -5
  158. package/dist/providerCapabilities.d.ts +92 -0
  159. package/dist/providerDecisionTraces.d.ts +130 -0
  160. package/dist/providerHealth.d.ts +3 -3
  161. package/dist/providerOrchestration.d.ts +109 -0
  162. package/dist/providerRouterTraces.d.ts +35 -0
  163. package/dist/providerRoutingContract.d.ts +71 -0
  164. package/dist/providerSlo.d.ts +142 -0
  165. package/dist/providerStackRecommendations.d.ts +187 -0
  166. package/dist/qualityRoutes.d.ts +4 -4
  167. package/dist/queue.d.ts +23 -14
  168. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  169. package/dist/react/VoiceCallDebuggerLaunch.d.ts +6 -0
  170. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  171. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  172. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  173. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  174. package/dist/react/VoiceProfileComparison.d.ts +6 -0
  175. package/dist/react/VoiceProfileSwitchRecommendation.d.ts +6 -0
  176. package/dist/react/VoiceProofTrends.d.ts +6 -0
  177. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  178. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  179. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  180. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  181. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  182. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  183. package/dist/react/VoiceSessionSnapshot.d.ts +6 -0
  184. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  185. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  186. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  187. package/dist/react/index.d.ts +43 -3
  188. package/dist/react/index.js +10650 -31
  189. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  190. package/dist/react/useVoiceCallDebugger.d.ts +8 -0
  191. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  192. package/dist/react/useVoiceController.d.ts +3 -1
  193. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  194. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  195. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  196. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  197. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  198. package/dist/react/useVoiceProfileComparison.d.ts +8 -0
  199. package/dist/react/useVoiceProfileSwitchRecommendation.d.ts +8 -0
  200. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  201. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  202. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  203. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  204. package/dist/react/useVoiceProviderStatus.d.ts +1 -1
  205. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  206. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  207. package/dist/react/useVoiceSessionSnapshot.d.ts +9 -0
  208. package/dist/react/useVoiceStream.d.ts +3 -1
  209. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  210. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  211. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  212. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  213. package/dist/readinessProfiles.d.ts +45 -0
  214. package/dist/realtimeChannel.d.ts +136 -0
  215. package/dist/realtimeProviderContracts.d.ts +133 -0
  216. package/dist/reconnectContract.d.ts +88 -0
  217. package/dist/resilienceRoutes.d.ts +45 -5
  218. package/dist/routing.d.ts +1 -1
  219. package/dist/runtimeOps.d.ts +3 -3
  220. package/dist/s3Store.d.ts +3 -3
  221. package/dist/session.d.ts +1 -1
  222. package/dist/sessionReplay.d.ts +19 -7
  223. package/dist/sessionSnapshot.d.ts +109 -0
  224. package/dist/simulationSuite.d.ts +143 -0
  225. package/dist/sloCalibration.d.ts +185 -0
  226. package/dist/sqliteStore.d.ts +17 -6
  227. package/dist/store.d.ts +1 -1
  228. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  229. package/dist/svelte/createVoiceCallDebugger.d.ts +12 -0
  230. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  231. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  232. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  233. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  234. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  235. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  236. package/dist/svelte/createVoiceProfileComparison.d.ts +7 -0
  237. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  238. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  239. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  240. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  241. package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
  242. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  243. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  244. package/dist/svelte/createVoiceSessionSnapshot.d.ts +13 -0
  245. package/dist/svelte/createVoiceStream.d.ts +1 -1
  246. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  247. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  248. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  249. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  250. package/dist/svelte/index.d.ts +23 -3
  251. package/dist/svelte/index.js +6425 -460
  252. package/dist/telephony/contract.d.ts +61 -0
  253. package/dist/telephony/matrix.d.ts +97 -0
  254. package/dist/telephony/plivo.d.ts +303 -0
  255. package/dist/telephony/response.d.ts +1 -1
  256. package/dist/telephony/security.d.ts +182 -0
  257. package/dist/telephony/telnyx.d.ts +291 -0
  258. package/dist/telephony/twilio.d.ts +147 -13
  259. package/dist/telephonyMediaRoutes.d.ts +72 -0
  260. package/dist/telephonyOutcome.d.ts +273 -0
  261. package/dist/testing/accuracy.d.ts +1 -1
  262. package/dist/testing/benchmark.d.ts +9 -9
  263. package/dist/testing/corrected.d.ts +5 -5
  264. package/dist/testing/duplex.d.ts +3 -3
  265. package/dist/testing/fixtures.d.ts +3 -3
  266. package/dist/testing/index.d.ts +13 -13
  267. package/dist/testing/index.js +5413 -184
  268. package/dist/testing/ioProviderSimulator.d.ts +5 -5
  269. package/dist/testing/providerSimulator.d.ts +5 -5
  270. package/dist/testing/review.d.ts +4 -4
  271. package/dist/testing/sessionBenchmark.d.ts +3 -3
  272. package/dist/testing/stt.d.ts +3 -3
  273. package/dist/testing/telephony.d.ts +25 -0
  274. package/dist/testing/tts.d.ts +1 -1
  275. package/dist/toolContract.d.ts +161 -0
  276. package/dist/toolRuntime.d.ts +50 -0
  277. package/dist/trace.d.ts +46 -7
  278. package/dist/traceDeliveryRoutes.d.ts +86 -0
  279. package/dist/traceTimeline.d.ts +97 -0
  280. package/dist/turnDetection.d.ts +1 -1
  281. package/dist/turnLatency.d.ts +95 -0
  282. package/dist/turnProfiles.d.ts +2 -2
  283. package/dist/turnQuality.d.ts +94 -0
  284. package/dist/types.d.ts +234 -80
  285. package/dist/voiceMonitoring.d.ts +444 -0
  286. package/dist/vue/VoiceCallDebuggerLaunch.d.ts +68 -0
  287. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  288. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  289. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  290. package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
  291. package/dist/vue/VoiceProofTrends.d.ts +21 -0
  292. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  293. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  294. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  295. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  296. package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
  297. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  298. package/dist/vue/VoiceSessionSnapshot.d.ts +68 -0
  299. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  300. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  301. package/dist/vue/index.d.ts +38 -3
  302. package/dist/vue/index.js +10095 -56
  303. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  304. package/dist/vue/useVoiceCallDebugger.d.ts +10 -0
  305. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  306. package/dist/vue/useVoiceController.d.ts +3 -2
  307. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  308. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  309. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  310. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  311. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  312. package/dist/vue/useVoiceProfileComparison.d.ts +9 -0
  313. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  314. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  315. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  316. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  317. package/dist/vue/useVoiceProviderStatus.d.ts +3 -3
  318. package/dist/vue/useVoiceReadinessFailures.d.ts +943 -0
  319. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  320. package/dist/vue/useVoiceSessionSnapshot.d.ts +10 -0
  321. package/dist/vue/useVoiceStream.d.ts +4 -2
  322. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  323. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  324. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  325. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  326. package/dist/workflowContract.d.ts +91 -0
  327. package/fixtures/manifest.json +356 -197
  328. package/package.json +260 -256
@@ -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,9 +214,22 @@ 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
+ sessionMetadata: message.sessionMetadata,
225
+ status: message.status,
226
+ turns: message.turns,
227
+ type: "replay"
228
+ };
212
229
  case "session":
213
230
  return {
214
231
  sessionId: message.sessionId,
232
+ sessionMetadata: message.sessionMetadata,
215
233
  scenarioId: message.scenarioId,
216
234
  status: message.status,
217
235
  type: "session"
@@ -226,6 +244,232 @@ var serverMessageToAction = (message) => {
226
244
  }
227
245
  };
228
246
 
247
+ // node_modules/@absolutejs/media/dist/index.js
248
+ var pushIssue = (issues, severity, code, message) => {
249
+ issues.push({ code, message, severity });
250
+ };
251
+ var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
252
+ var max = (values) => values.length === 0 ? undefined : Math.max(...values);
253
+ var numericStat = (stat, key) => {
254
+ const value = stat[key];
255
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
256
+ };
257
+ var booleanStat = (stat, key) => {
258
+ const value = stat[key];
259
+ return typeof value === "boolean" ? value : undefined;
260
+ };
261
+ var stringStat = (stat, key) => {
262
+ const value = stat[key];
263
+ return typeof value === "string" ? value : undefined;
264
+ };
265
+ var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
266
+ var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
267
+ var normalizeWebRTCStat = (stat) => {
268
+ const sample = {};
269
+ for (const [key, value] of Object.entries(stat)) {
270
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
271
+ sample[key] = value;
272
+ }
273
+ }
274
+ return sample;
275
+ };
276
+ var buildMediaWebRTCStatsReport = (input = {}) => {
277
+ const stats = input.stats ?? [];
278
+ const issues = [];
279
+ const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
280
+ const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
281
+ const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
282
+ const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
283
+ const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
284
+ const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
285
+ const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
286
+ const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
287
+ const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
288
+ const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
289
+ const packetLossDenominator = inboundPackets + packetsLost;
290
+ const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
291
+ const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
292
+ const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
293
+ const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
294
+ const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
295
+ const jitterBufferDelayMs = max(inbound.map((stat) => {
296
+ const delay = numericStat(stat, "jitterBufferDelay");
297
+ const emitted = numericStat(stat, "jitterBufferEmittedCount");
298
+ return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
299
+ }).filter((value) => value !== undefined));
300
+ const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
301
+ if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
302
+ pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
303
+ }
304
+ if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
305
+ pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
306
+ }
307
+ if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
308
+ pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
309
+ }
310
+ if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
311
+ pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
312
+ }
313
+ if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
314
+ pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
315
+ }
316
+ return {
317
+ activeCandidatePairs,
318
+ audioLevelAverage: average(audioLevels),
319
+ bytesReceived,
320
+ bytesSent,
321
+ checkedAt: Date.now(),
322
+ endedAudioTracks,
323
+ inboundPackets,
324
+ issues,
325
+ jitterBufferDelayMs,
326
+ jitterMs,
327
+ liveAudioTracks,
328
+ outboundPackets,
329
+ packetLossRatio,
330
+ packetsLost,
331
+ roundTripTimeMs,
332
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
333
+ totalStats: stats.length
334
+ };
335
+ };
336
+ var collectMediaWebRTCStats = async (input) => {
337
+ const report = await input.peerConnection.getStats(input.selector ?? null);
338
+ return [...report.values()].map(normalizeWebRTCStat);
339
+ };
340
+ var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
341
+ const stats = input.stats ?? [];
342
+ const previousStats = input.previousStats ?? [];
343
+ const issues = [];
344
+ const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
345
+ const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
346
+ const streams = audioRtp.map((stat) => {
347
+ const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
348
+ const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
349
+ const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
350
+ const previous = previousByKey.get(statKey(stat));
351
+ const currentPackets = numericStat(stat, packetsKey);
352
+ const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
353
+ const currentBytes = numericStat(stat, bytesKey);
354
+ const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
355
+ const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
356
+ return {
357
+ bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
358
+ currentPackets,
359
+ direction,
360
+ id: statKey(stat),
361
+ packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
362
+ previousPackets,
363
+ timeDeltaMs
364
+ };
365
+ });
366
+ const inbound = streams.filter((stream) => stream.direction === "inbound");
367
+ const outbound = streams.filter((stream) => stream.direction === "outbound");
368
+ const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
369
+ const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
370
+ const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
371
+ if (input.requireInboundAudio && inbound.length === 0) {
372
+ pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
373
+ }
374
+ if (input.requireOutboundAudio && outbound.length === 0) {
375
+ pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
376
+ }
377
+ if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
378
+ pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
379
+ }
380
+ if (stalledInboundStreams > 0) {
381
+ pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
382
+ }
383
+ if (stalledOutboundStreams > 0) {
384
+ pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
385
+ }
386
+ return {
387
+ checkedAt: Date.now(),
388
+ inboundAudioStreams: inbound.length,
389
+ issues,
390
+ maxObservedGapMs,
391
+ outboundAudioStreams: outbound.length,
392
+ stalledInboundStreams,
393
+ stalledOutboundStreams,
394
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
395
+ streams,
396
+ totalStats: stats.length
397
+ };
398
+ };
399
+
400
+ // src/client/browserMedia.ts
401
+ var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
402
+ var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
403
+ var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
404
+ var postBrowserMediaReport = async (payload, options) => {
405
+ const requestFetch = options.fetch ?? globalThis.fetch;
406
+ if (!requestFetch) {
407
+ return;
408
+ }
409
+ await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
410
+ body: JSON.stringify(payload),
411
+ headers: {
412
+ "Content-Type": "application/json"
413
+ },
414
+ keepalive: true,
415
+ method: "POST"
416
+ });
417
+ };
418
+ var createVoiceBrowserMediaReporter = (options) => {
419
+ let interval = null;
420
+ let previousStats = [];
421
+ const reportOnce = async () => {
422
+ const peerConnection = await resolvePeerConnection(options);
423
+ if (!peerConnection) {
424
+ return;
425
+ }
426
+ const stats = await collectMediaWebRTCStats({ peerConnection });
427
+ const report = buildMediaWebRTCStatsReport({
428
+ ...options,
429
+ stats
430
+ });
431
+ const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
432
+ ...options.continuity,
433
+ previousStats,
434
+ stats
435
+ });
436
+ const payload = {
437
+ at: Date.now(),
438
+ continuity,
439
+ report,
440
+ scenarioId: options.getScenarioId?.() ?? null,
441
+ sessionId: options.getSessionId?.() ?? null
442
+ };
443
+ previousStats = stats;
444
+ options.onReport?.(payload);
445
+ await postBrowserMediaReport(payload, options);
446
+ return payload;
447
+ };
448
+ const run = () => {
449
+ reportOnce().catch((error) => {
450
+ options.onError?.(error);
451
+ });
452
+ };
453
+ const stop = () => {
454
+ if (interval) {
455
+ clearInterval(interval);
456
+ interval = null;
457
+ }
458
+ };
459
+ return {
460
+ close: stop,
461
+ reportOnce,
462
+ start: () => {
463
+ if (interval) {
464
+ return;
465
+ }
466
+ run();
467
+ interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
468
+ },
469
+ stop
470
+ };
471
+ };
472
+
229
473
  // src/client/connection.ts
230
474
  var WS_OPEN = 1;
231
475
  var WS_CLOSED = 3;
@@ -269,10 +513,12 @@ var isVoiceServerMessage = (value) => {
269
513
  case "assistant":
270
514
  case "call_lifecycle":
271
515
  case "complete":
516
+ case "connection":
272
517
  case "error":
273
518
  case "final":
274
519
  case "partial":
275
520
  case "pong":
521
+ case "replay":
276
522
  case "session":
277
523
  case "turn":
278
524
  return true;
@@ -309,6 +555,9 @@ var createVoiceConnection = (path, options = {}) => {
309
555
  sessionId: options.sessionId ?? createSessionId(),
310
556
  ws: null
311
557
  };
558
+ const emitConnection = (reconnect) => {
559
+ listeners.forEach((listener) => listener(reconnect));
560
+ };
312
561
  const clearTimers = () => {
313
562
  if (state.pingInterval) {
314
563
  clearInterval(state.pingInterval);
@@ -331,9 +580,28 @@ var createVoiceConnection = (path, options = {}) => {
331
580
  }
332
581
  };
333
582
  const scheduleReconnect = () => {
583
+ const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
334
584
  state.reconnectAttempts += 1;
585
+ emitConnection({
586
+ reconnect: {
587
+ attempts: state.reconnectAttempts,
588
+ lastDisconnectAt: Date.now(),
589
+ maxAttempts: maxReconnectAttempts,
590
+ nextAttemptAt,
591
+ status: "reconnecting"
592
+ },
593
+ type: "connection"
594
+ });
335
595
  state.reconnectTimeout = setTimeout(() => {
336
596
  if (state.reconnectAttempts > maxReconnectAttempts) {
597
+ emitConnection({
598
+ reconnect: {
599
+ attempts: state.reconnectAttempts,
600
+ maxAttempts: maxReconnectAttempts,
601
+ status: "exhausted"
602
+ },
603
+ type: "connection"
604
+ });
337
605
  return;
338
606
  }
339
607
  connect();
@@ -343,9 +611,21 @@ var createVoiceConnection = (path, options = {}) => {
343
611
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
344
612
  ws.binaryType = "arraybuffer";
345
613
  ws.onopen = () => {
614
+ const wasReconnecting = state.reconnectAttempts > 0;
346
615
  state.isConnected = true;
347
- state.reconnectAttempts = 0;
348
616
  flushPendingMessages();
617
+ if (wasReconnecting) {
618
+ emitConnection({
619
+ reconnect: {
620
+ attempts: state.reconnectAttempts,
621
+ lastResumedAt: Date.now(),
622
+ maxAttempts: maxReconnectAttempts,
623
+ status: "resumed"
624
+ },
625
+ type: "connection"
626
+ });
627
+ state.reconnectAttempts = 0;
628
+ }
349
629
  listeners.forEach((listener) => listener({
350
630
  scenarioId: state.scenarioId ?? undefined,
351
631
  sessionId: state.sessionId,
@@ -375,6 +655,16 @@ var createVoiceConnection = (path, options = {}) => {
375
655
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
376
656
  if (reconnectable) {
377
657
  scheduleReconnect();
658
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
659
+ emitConnection({
660
+ reconnect: {
661
+ attempts: state.reconnectAttempts,
662
+ lastDisconnectAt: Date.now(),
663
+ maxAttempts: maxReconnectAttempts,
664
+ status: "exhausted"
665
+ },
666
+ type: "connection"
667
+ });
378
668
  }
379
669
  };
380
670
  state.ws = ws;
@@ -445,14 +735,21 @@ var createVoiceConnection = (path, options = {}) => {
445
735
  };
446
736
 
447
737
  // src/client/store.ts
738
+ var createInitialReconnectState = () => ({
739
+ attempts: 0,
740
+ maxAttempts: 0,
741
+ status: "idle"
742
+ });
448
743
  var createInitialState = () => ({
449
744
  assistantAudio: [],
450
745
  assistantTexts: [],
451
746
  call: null,
452
747
  error: null,
453
748
  isConnected: false,
749
+ sessionMetadata: null,
454
750
  scenarioId: null,
455
751
  partial: "",
752
+ reconnect: createInitialReconnectState(),
456
753
  sessionId: null,
457
754
  status: "idle",
458
755
  turns: []
@@ -509,7 +806,19 @@ var createVoiceStreamStore = () => {
509
806
  case "connected":
510
807
  state = {
511
808
  ...state,
512
- isConnected: true
809
+ isConnected: true,
810
+ reconnect: state.reconnect.status === "reconnecting" ? {
811
+ ...state.reconnect,
812
+ lastResumedAt: Date.now(),
813
+ nextAttemptAt: undefined,
814
+ status: "resumed"
815
+ } : state.reconnect
816
+ };
817
+ break;
818
+ case "connection":
819
+ state = {
820
+ ...state,
821
+ reconnect: action.reconnect
513
822
  };
514
823
  break;
515
824
  case "disconnected":
@@ -537,6 +846,27 @@ var createVoiceStreamStore = () => {
537
846
  partial: action.transcript.text
538
847
  };
539
848
  break;
849
+ case "replay":
850
+ state = {
851
+ ...state,
852
+ assistantTexts: [...action.assistantTexts],
853
+ call: action.call ?? null,
854
+ error: null,
855
+ isConnected: action.status === "active",
856
+ partial: action.partial,
857
+ reconnect: state.reconnect.status === "reconnecting" ? {
858
+ ...state.reconnect,
859
+ lastResumedAt: Date.now(),
860
+ nextAttemptAt: undefined,
861
+ status: "resumed"
862
+ } : state.reconnect,
863
+ scenarioId: action.scenarioId ?? state.scenarioId,
864
+ sessionId: action.sessionId,
865
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
866
+ status: action.status,
867
+ turns: [...action.turns]
868
+ };
869
+ break;
540
870
  case "session":
541
871
  state = {
542
872
  ...state,
@@ -544,6 +874,7 @@ var createVoiceStreamStore = () => {
544
874
  scenarioId: action.scenarioId ?? state.scenarioId,
545
875
  isConnected: action.status === "active",
546
876
  sessionId: action.sessionId,
877
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
547
878
  status: action.status
548
879
  };
549
880
  break;
@@ -574,20 +905,50 @@ var createVoiceStreamStore = () => {
574
905
  var createVoiceStream = (path, options = {}) => {
575
906
  const connection = createVoiceConnection(path, options);
576
907
  const store = createVoiceStreamStore();
908
+ const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
909
+ ...options.browserMedia,
910
+ getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
911
+ getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
912
+ }) : null;
577
913
  const subscribers = new Set;
578
914
  const start = (input) => Promise.resolve().then(() => {
579
915
  if (!input?.sessionId && !input?.scenarioId) {
580
916
  return;
581
917
  }
582
918
  connection.start(input);
919
+ browserMediaReporter?.start();
583
920
  });
584
921
  const notify = () => {
585
922
  subscribers.forEach((subscriber) => subscriber());
586
923
  };
924
+ const reportReconnect = () => {
925
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
926
+ return;
927
+ }
928
+ const snapshot = store.getSnapshot();
929
+ const body = JSON.stringify({
930
+ at: Date.now(),
931
+ reconnect: snapshot.reconnect,
932
+ scenarioId: snapshot.scenarioId,
933
+ sessionId: connection.getSessionId(),
934
+ turnIds: snapshot.turns.map((turn) => turn.id)
935
+ });
936
+ fetch(options.reconnectReportPath, {
937
+ body,
938
+ headers: {
939
+ "Content-Type": "application/json"
940
+ },
941
+ keepalive: true,
942
+ method: "POST"
943
+ }).catch(() => {});
944
+ };
587
945
  const unsubscribeConnection = connection.subscribe((message) => {
588
946
  const action = serverMessageToAction(message);
589
947
  if (action) {
590
948
  store.dispatch(action);
949
+ if (message.type === "connection") {
950
+ reportReconnect();
951
+ }
591
952
  notify();
592
953
  }
593
954
  });
@@ -597,6 +958,7 @@ var createVoiceStream = (path, options = {}) => {
597
958
  },
598
959
  close() {
599
960
  unsubscribeConnection();
961
+ browserMediaReporter?.close();
600
962
  connection.close();
601
963
  store.dispatch({ type: "disconnected" });
602
964
  notify();
@@ -619,10 +981,16 @@ var createVoiceStream = (path, options = {}) => {
619
981
  get scenarioId() {
620
982
  return store.getSnapshot().scenarioId;
621
983
  },
984
+ get sessionMetadata() {
985
+ return store.getSnapshot().sessionMetadata;
986
+ },
622
987
  start,
623
988
  get partial() {
624
989
  return store.getSnapshot().partial;
625
990
  },
991
+ get reconnect() {
992
+ return store.getSnapshot().reconnect;
993
+ },
626
994
  get sessionId() {
627
995
  return connection.getSessionId();
628
996
  },
@@ -941,8 +1309,10 @@ var createInitialState2 = (stream) => ({
941
1309
  isConnected: stream.isConnected,
942
1310
  isRecording: false,
943
1311
  partial: stream.partial,
1312
+ reconnect: stream.reconnect,
944
1313
  recordingError: null,
945
1314
  sessionId: stream.sessionId,
1315
+ sessionMetadata: stream.sessionMetadata,
946
1316
  scenarioId: stream.scenarioId,
947
1317
  status: stream.status,
948
1318
  turns: [...stream.turns]
@@ -970,7 +1340,9 @@ var createVoiceController = (path, options = {}) => {
970
1340
  error: stream.error,
971
1341
  isConnected: stream.isConnected,
972
1342
  partial: stream.partial,
1343
+ reconnect: stream.reconnect,
973
1344
  sessionId: stream.sessionId,
1345
+ sessionMetadata: stream.sessionMetadata,
974
1346
  scenarioId: stream.scenarioId,
975
1347
  status: stream.status,
976
1348
  turns: [...stream.turns]
@@ -994,7 +1366,13 @@ var createVoiceController = (path, options = {}) => {
994
1366
  capture = createMicrophoneCapture({
995
1367
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
1368
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
1369
+ onAudio: (audio) => {
1370
+ if (options.capture?.onAudio) {
1371
+ options.capture.onAudio(audio, stream.sendAudio);
1372
+ return;
1373
+ }
1374
+ stream.sendAudio(audio);
1375
+ },
998
1376
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
999
1377
  });
1000
1378
  return capture;
@@ -1064,10 +1442,16 @@ var createVoiceController = (path, options = {}) => {
1064
1442
  get recordingError() {
1065
1443
  return state.recordingError;
1066
1444
  },
1445
+ get reconnect() {
1446
+ return state.reconnect;
1447
+ },
1067
1448
  sendAudio: (audio) => stream.sendAudio(audio),
1068
1449
  get sessionId() {
1069
1450
  return state.sessionId;
1070
1451
  },
1452
+ get sessionMetadata() {
1453
+ return state.sessionMetadata;
1454
+ },
1071
1455
  get scenarioId() {
1072
1456
  return state.scenarioId;
1073
1457
  },
@@ -1104,6 +1488,475 @@ var createVoiceController = (path, options = {}) => {
1104
1488
  };
1105
1489
  };
1106
1490
 
1491
+ // src/client/audioPlayer.ts
1492
+ var DEFAULT_LOOKAHEAD_MS = 15;
1493
+ var createInitialState3 = () => ({
1494
+ activeSourceCount: 0,
1495
+ error: null,
1496
+ isActive: false,
1497
+ isPlaying: false,
1498
+ lastInterruptLatencyMs: undefined,
1499
+ lastPlaybackStopLatencyMs: undefined,
1500
+ processedChunkCount: 0,
1501
+ queuedChunkCount: 0
1502
+ });
1503
+ var getAudioContextCtor = () => {
1504
+ if (typeof window === "undefined") {
1505
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1506
+ }
1507
+ return window.AudioContext ?? window.webkitAudioContext;
1508
+ };
1509
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1510
+ const format = chunk.format;
1511
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1512
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1513
+ }
1514
+ const bytes = chunk.chunk;
1515
+ const channels = Math.max(1, format.channels);
1516
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1517
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1518
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1519
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1520
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1521
+ const channelData = audioBuffer.getChannelData(channelIndex);
1522
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1523
+ const sampleIndex = frameIndex * channels + channelIndex;
1524
+ const sampleOffset = sampleIndex * 2;
1525
+ if (sampleOffset + 1 >= bytes.byteLength) {
1526
+ channelData[frameIndex] = 0;
1527
+ continue;
1528
+ }
1529
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1530
+ }
1531
+ }
1532
+ return audioBuffer;
1533
+ };
1534
+ var createVoiceAudioPlayer = (source, options = {}) => {
1535
+ const subscribers = new Set;
1536
+ const sourceNodes = new Set;
1537
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1538
+ let state = createInitialState3();
1539
+ let audioContext = null;
1540
+ let outputNode = null;
1541
+ let queueEndTime = 0;
1542
+ let syncPromise = Promise.resolve();
1543
+ let interruptStartedAt = null;
1544
+ let interruptPromise = null;
1545
+ let resolveInterruptPromise = null;
1546
+ let interruptFallbackTimer = null;
1547
+ const notify = () => {
1548
+ for (const subscriber of subscribers) {
1549
+ subscriber();
1550
+ }
1551
+ };
1552
+ const setState = (next) => {
1553
+ state = {
1554
+ ...state,
1555
+ ...next
1556
+ };
1557
+ notify();
1558
+ };
1559
+ const clearError = () => {
1560
+ if (state.error !== null) {
1561
+ setState({ error: null });
1562
+ }
1563
+ };
1564
+ const clearInterruptTimer = () => {
1565
+ if (interruptFallbackTimer !== null) {
1566
+ clearTimeout(interruptFallbackTimer);
1567
+ interruptFallbackTimer = null;
1568
+ }
1569
+ };
1570
+ const resolveInterrupt = (latencyMs) => {
1571
+ clearInterruptTimer();
1572
+ interruptStartedAt = null;
1573
+ setState({
1574
+ activeSourceCount: sourceNodes.size,
1575
+ isPlaying: false,
1576
+ lastInterruptLatencyMs: latencyMs,
1577
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1578
+ });
1579
+ resolveInterruptPromise?.();
1580
+ resolveInterruptPromise = null;
1581
+ interruptPromise = null;
1582
+ };
1583
+ const estimateOutputStopLatencyMs = (context) => {
1584
+ if (!context) {
1585
+ return 0;
1586
+ }
1587
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1588
+ };
1589
+ const restoreOutputGain = (context) => {
1590
+ if (!outputNode) {
1591
+ return;
1592
+ }
1593
+ const gainValue = 1;
1594
+ if (outputNode.gain.setValueAtTime) {
1595
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1596
+ return;
1597
+ }
1598
+ outputNode.gain.value = gainValue;
1599
+ };
1600
+ const muteOutputGain = (context) => {
1601
+ if (!outputNode) {
1602
+ return;
1603
+ }
1604
+ const gainValue = 0;
1605
+ if (outputNode.gain.setValueAtTime) {
1606
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1607
+ return;
1608
+ }
1609
+ outputNode.gain.value = gainValue;
1610
+ };
1611
+ const maybeResolveInterrupt = () => {
1612
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1613
+ return;
1614
+ }
1615
+ resolveInterrupt(Date.now() - interruptStartedAt);
1616
+ };
1617
+ const ensureAudioContext = async () => {
1618
+ if (audioContext) {
1619
+ return audioContext;
1620
+ }
1621
+ if (options.createAudioContext) {
1622
+ audioContext = options.createAudioContext();
1623
+ } else {
1624
+ const AudioContextCtor = getAudioContextCtor();
1625
+ if (!AudioContextCtor) {
1626
+ throw new Error("Assistant audio playback requires AudioContext support.");
1627
+ }
1628
+ audioContext = new AudioContextCtor;
1629
+ }
1630
+ if (audioContext.createGain) {
1631
+ outputNode = audioContext.createGain();
1632
+ outputNode.connect?.(audioContext.destination);
1633
+ }
1634
+ queueEndTime = audioContext.currentTime;
1635
+ return audioContext;
1636
+ };
1637
+ const scheduleChunk = async (chunk) => {
1638
+ const context = await ensureAudioContext();
1639
+ const buffer = decodePCM16LEChunk(context, chunk);
1640
+ const node = context.createBufferSource();
1641
+ node.buffer = buffer;
1642
+ node.connect(outputNode ?? context.destination);
1643
+ node.onended = () => {
1644
+ sourceNodes.delete(node);
1645
+ node.disconnect?.();
1646
+ setState({
1647
+ activeSourceCount: sourceNodes.size,
1648
+ isPlaying: sourceNodes.size > 0 && state.isActive
1649
+ });
1650
+ maybeResolveInterrupt();
1651
+ };
1652
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1653
+ queueEndTime = startAt + buffer.duration;
1654
+ sourceNodes.add(node);
1655
+ setState({
1656
+ activeSourceCount: sourceNodes.size,
1657
+ isPlaying: true
1658
+ });
1659
+ node.start(startAt);
1660
+ };
1661
+ const stopQueuedPlayback = (options2) => {
1662
+ for (const node of [...sourceNodes]) {
1663
+ node.stop?.();
1664
+ }
1665
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1666
+ if (options2?.forceClear) {
1667
+ for (const node of sourceNodes) {
1668
+ node.disconnect?.();
1669
+ }
1670
+ sourceNodes.clear();
1671
+ maybeResolveInterrupt();
1672
+ }
1673
+ };
1674
+ const sync = async () => {
1675
+ if (!state.isActive) {
1676
+ return;
1677
+ }
1678
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1679
+ if (nextChunks.length === 0) {
1680
+ return;
1681
+ }
1682
+ try {
1683
+ clearError();
1684
+ for (const chunk of nextChunks) {
1685
+ await scheduleChunk(chunk);
1686
+ }
1687
+ setState({
1688
+ processedChunkCount: source.assistantAudio.length,
1689
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1690
+ });
1691
+ } catch (error) {
1692
+ setState({
1693
+ error: error instanceof Error ? error.message : String(error)
1694
+ });
1695
+ }
1696
+ };
1697
+ const queueSync = () => {
1698
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1699
+ return syncPromise;
1700
+ };
1701
+ const unsubscribeSource = source.subscribe(() => {
1702
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1703
+ player.start();
1704
+ return;
1705
+ }
1706
+ if (state.isActive) {
1707
+ queueSync();
1708
+ }
1709
+ });
1710
+ const player = {
1711
+ close: async () => {
1712
+ unsubscribeSource();
1713
+ stopQueuedPlayback({ forceClear: true });
1714
+ clearInterruptTimer();
1715
+ resolveInterruptPromise?.();
1716
+ resolveInterruptPromise = null;
1717
+ interruptPromise = null;
1718
+ interruptStartedAt = null;
1719
+ if (audioContext && audioContext.state !== "closed") {
1720
+ await audioContext.close();
1721
+ }
1722
+ audioContext = null;
1723
+ outputNode?.disconnect?.();
1724
+ outputNode = null;
1725
+ queueEndTime = 0;
1726
+ setState({
1727
+ activeSourceCount: 0,
1728
+ isActive: false,
1729
+ isPlaying: false
1730
+ });
1731
+ },
1732
+ get activeSourceCount() {
1733
+ return state.activeSourceCount;
1734
+ },
1735
+ get error() {
1736
+ return state.error;
1737
+ },
1738
+ getSnapshot: () => state,
1739
+ get isActive() {
1740
+ return state.isActive;
1741
+ },
1742
+ get isPlaying() {
1743
+ return state.isPlaying;
1744
+ },
1745
+ interrupt: async () => {
1746
+ const startedAt = Date.now();
1747
+ const context = await ensureAudioContext();
1748
+ interruptStartedAt = startedAt;
1749
+ muteOutputGain(context);
1750
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
1751
+ setState({
1752
+ isActive: false,
1753
+ isPlaying: sourceNodes.size > 0,
1754
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
1755
+ });
1756
+ if (sourceNodes.size === 0) {
1757
+ resolveInterrupt(playbackStopLatencyMs);
1758
+ return;
1759
+ }
1760
+ if (!interruptPromise) {
1761
+ interruptPromise = new Promise((resolve) => {
1762
+ resolveInterruptPromise = resolve;
1763
+ });
1764
+ }
1765
+ clearInterruptTimer();
1766
+ interruptFallbackTimer = setTimeout(() => {
1767
+ for (const node of sourceNodes) {
1768
+ node.disconnect?.();
1769
+ }
1770
+ sourceNodes.clear();
1771
+ resolveInterrupt(Date.now() - startedAt);
1772
+ }, 250);
1773
+ stopQueuedPlayback();
1774
+ await interruptPromise;
1775
+ },
1776
+ get lastInterruptLatencyMs() {
1777
+ return state.lastInterruptLatencyMs;
1778
+ },
1779
+ get lastPlaybackStopLatencyMs() {
1780
+ return state.lastPlaybackStopLatencyMs;
1781
+ },
1782
+ pause: async () => {
1783
+ if (!audioContext) {
1784
+ setState({
1785
+ activeSourceCount: 0,
1786
+ isActive: false,
1787
+ isPlaying: false
1788
+ });
1789
+ return;
1790
+ }
1791
+ await audioContext.suspend();
1792
+ setState({
1793
+ activeSourceCount: sourceNodes.size,
1794
+ isActive: false,
1795
+ isPlaying: false
1796
+ });
1797
+ },
1798
+ get processedChunkCount() {
1799
+ return state.processedChunkCount;
1800
+ },
1801
+ get queuedChunkCount() {
1802
+ return state.queuedChunkCount;
1803
+ },
1804
+ start: async () => {
1805
+ try {
1806
+ clearError();
1807
+ const context = await ensureAudioContext();
1808
+ restoreOutputGain(context);
1809
+ if (context.state === "suspended") {
1810
+ await context.resume();
1811
+ }
1812
+ setState({
1813
+ activeSourceCount: sourceNodes.size,
1814
+ isActive: true,
1815
+ isPlaying: context.state === "running"
1816
+ });
1817
+ await queueSync();
1818
+ } catch (error) {
1819
+ setState({
1820
+ error: error instanceof Error ? error.message : String(error),
1821
+ isActive: false,
1822
+ isPlaying: false
1823
+ });
1824
+ throw error;
1825
+ }
1826
+ },
1827
+ subscribe: (subscriber) => {
1828
+ subscribers.add(subscriber);
1829
+ return () => {
1830
+ subscribers.delete(subscriber);
1831
+ };
1832
+ }
1833
+ };
1834
+ return player;
1835
+ };
1836
+
1837
+ // src/client/bargeInMonitor.ts
1838
+ var DEFAULT_THRESHOLD_MS = 250;
1839
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1840
+ var summarize = (events, thresholdMs) => {
1841
+ const stopped = events.filter((event) => event.status === "stopped");
1842
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1843
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1844
+ const passed = stopped.length - failed;
1845
+ return {
1846
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1847
+ events: [...events],
1848
+ failed,
1849
+ lastEvent: events.at(-1),
1850
+ passed,
1851
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1852
+ thresholdMs,
1853
+ total: stopped.length
1854
+ };
1855
+ };
1856
+ var createVoiceBargeInMonitor = (options = {}) => {
1857
+ const listeners = new Set;
1858
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1859
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1860
+ const events = [];
1861
+ const emit = () => {
1862
+ for (const listener of listeners) {
1863
+ listener();
1864
+ }
1865
+ };
1866
+ const postEvent = (event) => {
1867
+ if (!options.path || typeof fetchImpl !== "function") {
1868
+ return;
1869
+ }
1870
+ fetchImpl(options.path, {
1871
+ body: JSON.stringify(event),
1872
+ headers: {
1873
+ "Content-Type": "application/json"
1874
+ },
1875
+ method: "POST"
1876
+ }).catch(() => {});
1877
+ };
1878
+ const record = (status, input) => {
1879
+ const event = {
1880
+ at: Date.now(),
1881
+ id: createEventId(),
1882
+ latencyMs: input.latencyMs,
1883
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1884
+ reason: input.reason,
1885
+ sessionId: input.sessionId,
1886
+ status,
1887
+ thresholdMs
1888
+ };
1889
+ events.push(event);
1890
+ postEvent(event);
1891
+ emit();
1892
+ return event;
1893
+ };
1894
+ return {
1895
+ getSnapshot: () => summarize(events, thresholdMs),
1896
+ recordRequested: (input) => record("requested", input),
1897
+ recordSkipped: (input) => record("skipped", input),
1898
+ recordStopped: (input) => record("stopped", input),
1899
+ subscribe: (subscriber) => {
1900
+ listeners.add(subscriber);
1901
+ return () => {
1902
+ listeners.delete(subscriber);
1903
+ };
1904
+ }
1905
+ };
1906
+ };
1907
+
1908
+ // src/client/duplex.ts
1909
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1910
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1911
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
1912
+ let lastPartial = controller.partial;
1913
+ const interruptIfPlaying = (reason) => {
1914
+ if (!player.isPlaying || options.enabled === false) {
1915
+ options.monitor?.recordSkipped({
1916
+ reason,
1917
+ sessionId: controller.sessionId
1918
+ });
1919
+ return;
1920
+ }
1921
+ options.monitor?.recordRequested({
1922
+ reason,
1923
+ sessionId: controller.sessionId
1924
+ });
1925
+ player.interrupt().then(() => {
1926
+ options.monitor?.recordStopped({
1927
+ latencyMs: player.lastInterruptLatencyMs,
1928
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1929
+ reason,
1930
+ sessionId: controller.sessionId
1931
+ });
1932
+ });
1933
+ };
1934
+ const unsubscribe = controller.subscribe(() => {
1935
+ if (options.interruptOnPartial === false) {
1936
+ lastPartial = controller.partial;
1937
+ return;
1938
+ }
1939
+ if (!lastPartial && controller.partial) {
1940
+ interruptIfPlaying("partial-transcript");
1941
+ }
1942
+ lastPartial = controller.partial;
1943
+ });
1944
+ return {
1945
+ close: () => {
1946
+ unsubscribe();
1947
+ },
1948
+ handleLevel: (level) => {
1949
+ if (shouldInterruptForLevel(level, options)) {
1950
+ interruptIfPlaying("input-level");
1951
+ }
1952
+ },
1953
+ sendAudio: (audio) => {
1954
+ interruptIfPlaying("manual-audio");
1955
+ controller.sendAudio(audio);
1956
+ }
1957
+ };
1958
+ };
1959
+
1107
1960
  // src/client/htmxBootstrap.ts
1108
1961
  var VOICE_WAVE_POINTS = 48;
1109
1962
  var VOICE_WAVE_WIDTH = 320;
@@ -1126,7 +1979,7 @@ var DEFAULT_GUIDED_PROMPTS = [
1126
1979
  "Now describe what you are trying to do or test.",
1127
1980
  "Finish with any detail that feels blocked, risky, or unclear."
1128
1981
  ];
1129
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1982
+ var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
1130
1983
  var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1131
1984
  var readErrorField = (value, key) => {
1132
1985
  const candidate = value[key];
@@ -1160,6 +2013,17 @@ var formatErrorMessage = (error) => {
1160
2013
  }
1161
2014
  return "Unexpected error";
1162
2015
  };
2016
+ var formatReconnectState = (reconnect) => {
2017
+ const pieces = [reconnect.status];
2018
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
2019
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
2020
+ }
2021
+ if (reconnect.nextAttemptAt) {
2022
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
2023
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
2024
+ }
2025
+ return pieces.join(" · ");
2026
+ };
1163
2027
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1164
2028
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1165
2029
  const next = levels.slice(-(count - 1));
@@ -1216,6 +2080,17 @@ var parsePromptList = (value) => {
1216
2080
  } catch {}
1217
2081
  return DEFAULT_GUIDED_PROMPTS;
1218
2082
  };
2083
+ var parseOptionalNumber = (value) => {
2084
+ if (!value) {
2085
+ return;
2086
+ }
2087
+ const parsed = Number(value);
2088
+ return Number.isFinite(parsed) ? parsed : undefined;
2089
+ };
2090
+ var resolveElement2 = (root, selector, ctor) => {
2091
+ const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
2092
+ return value instanceof ctor ? value : null;
2093
+ };
1219
2094
  var requireElement = (root, selector, ctor, name) => {
1220
2095
  const value = selector ? document.querySelector(selector) : null;
1221
2096
  if (value instanceof ctor) {
@@ -1266,11 +2141,20 @@ var initVoiceHTMXRoot = (root) => {
1266
2141
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
2142
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
2143
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
2144
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
2145
+ const bargeInPath = root.dataset.voiceBargeInPath;
2146
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
2147
+ path: bargeInPath,
2148
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
2149
+ }) : null;
2150
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
2151
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
2152
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
2153
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
2154
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1272
2155
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1273
2156
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
2157
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1274
2158
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1275
2159
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1276
2160
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1279,35 +2163,70 @@ var initVoiceHTMXRoot = (root) => {
1279
2163
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
2164
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
2165
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
2166
+ let activeMode = null;
2167
+ let hasStartedModes = {
2168
+ general: false,
2169
+ guided: false
2170
+ };
2171
+ let isCapturing = false;
2172
+ let micError = null;
2173
+ let waveLevels = createInitialVoiceWaveLevels();
2174
+ let guidedBargeInBinding = null;
2175
+ let generalBargeInBinding = null;
1282
2176
  const guidedVoice = createVoiceController(guidedPath, {
1283
2177
  capture: {
2178
+ onAudio: (audio, sendAudio) => {
2179
+ if (guidedBargeInBinding) {
2180
+ guidedBargeInBinding.sendAudio(audio);
2181
+ return;
2182
+ }
2183
+ sendAudio(audio);
2184
+ },
1284
2185
  onLevel: (level) => {
2186
+ guidedBargeInBinding?.handleLevel(level);
1285
2187
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
2188
  renderWave();
1287
2189
  }
1288
2190
  },
2191
+ connection: {
2192
+ reconnectReportPath
2193
+ },
1289
2194
  preset: "guided-intake"
1290
2195
  });
1291
2196
  const generalVoice = createVoiceController(generalPath, {
1292
2197
  capture: {
2198
+ onAudio: (audio, sendAudio) => {
2199
+ if (generalBargeInBinding) {
2200
+ generalBargeInBinding.sendAudio(audio);
2201
+ return;
2202
+ }
2203
+ sendAudio(audio);
2204
+ },
1293
2205
  onLevel: (level) => {
2206
+ generalBargeInBinding?.handleLevel(level);
1294
2207
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
2208
  renderWave();
1296
2209
  }
1297
2210
  },
2211
+ connection: {
2212
+ reconnectReportPath
2213
+ },
1298
2214
  preset: "dictation"
1299
2215
  });
1300
2216
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
2217
  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();
2218
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
2219
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
2220
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
2221
+ interruptThreshold: bargeInSpeechThreshold,
2222
+ monitor: bargeInMonitor ?? undefined
2223
+ });
2224
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
2225
+ interruptThreshold: bargeInSpeechThreshold,
2226
+ monitor: bargeInMonitor ?? undefined
2227
+ });
1310
2228
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
2229
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1311
2230
  const renderWave = () => {
1312
2231
  const path = createVoiceWavePath(waveLevels);
1313
2232
  voiceWaveGlow.setAttribute("d", path);
@@ -1322,6 +2241,9 @@ var initVoiceHTMXRoot = (root) => {
1322
2241
  const status = voice.status;
1323
2242
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1324
2243
  errorStatus.textContent = micError || voice.error || "None";
2244
+ if (reconnectStatus) {
2245
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2246
+ }
1325
2247
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1326
2248
  promptStatus.textContent = resolvePromptMessage({
1327
2249
  guidedPrompts,
@@ -1385,8 +2307,18 @@ var initVoiceHTMXRoot = (root) => {
1385
2307
  render();
1386
2308
  }
1387
2309
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
2310
+ guidedVoice.subscribe(() => {
2311
+ if (guidedVoice.assistantAudio.length > 0) {
2312
+ guidedAudioPlayer.start().catch(() => {});
2313
+ }
2314
+ render();
2315
+ });
2316
+ generalVoice.subscribe(() => {
2317
+ if (generalVoice.assistantAudio.length > 0) {
2318
+ generalAudioPlayer.start().catch(() => {});
2319
+ }
2320
+ render();
2321
+ });
1390
2322
  startGuidedButton.addEventListener("click", () => {
1391
2323
  startMode("guided");
1392
2324
  });
@@ -1399,6 +2331,10 @@ var initVoiceHTMXRoot = (root) => {
1399
2331
  window.addEventListener("beforeunload", () => {
1400
2332
  guidedVoice.stopRecording();
1401
2333
  generalVoice.stopRecording();
2334
+ guidedBargeInBinding?.close();
2335
+ generalBargeInBinding?.close();
2336
+ guidedAudioPlayer.close();
2337
+ generalAudioPlayer.close();
1402
2338
  stopGuidedBinding();
1403
2339
  stopGeneralBinding();
1404
2340
  guidedVoice.close();