@absolutejs/voice 0.0.22-beta.67 → 0.0.22-beta.670

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 (515) hide show
  1. package/LICENSE +88 -0
  2. package/README.md +4562 -1179
  3. package/dist/angular/index.d.ts +36 -8
  4. package/dist/angular/index.js +4362 -537
  5. package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
  6. package/dist/angular/voice-call-debugger.service.d.ts +12 -0
  7. package/dist/angular/voice-call-player.service.d.ts +19 -0
  8. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  9. package/dist/angular/voice-controller.service.d.ts +4 -1
  10. package/dist/angular/voice-cost-dashboard.service.d.ts +15 -0
  11. package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
  12. package/dist/angular/voice-live-agent-console.service.d.ts +16 -0
  13. package/dist/angular/voice-live-call-viewer.service.d.ts +16 -0
  14. package/dist/angular/voice-live-ops.service.d.ts +11 -0
  15. package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
  16. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  17. package/dist/angular/voice-platform-coverage.service.d.ts +12 -0
  18. package/dist/angular/voice-profile-comparison.service.d.ts +12 -0
  19. package/dist/angular/voice-proof-trends.service.d.ts +12 -0
  20. package/dist/angular/voice-provider-capabilities.service.d.ts +2 -2
  21. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  22. package/dist/angular/voice-provider-status.service.d.ts +2 -2
  23. package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
  24. package/dist/angular/voice-reconnect-profile-evidence.service.d.ts +12 -0
  25. package/dist/angular/voice-replay-timeline.service.d.ts +13 -0
  26. package/dist/angular/voice-routing-status.service.d.ts +1 -1
  27. package/dist/angular/voice-session-observability.service.d.ts +12 -0
  28. package/dist/angular/voice-session-snapshot.service.d.ts +13 -0
  29. package/dist/angular/voice-stream.service.d.ts +6 -2
  30. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  31. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  32. package/dist/angular/voice-turn-quality.service.d.ts +2 -2
  33. package/dist/angular/voice-widget.service.d.ts +18 -0
  34. package/dist/angular/voice-workflow-status.service.d.ts +2 -2
  35. package/dist/client/actions.d.ts +201 -2
  36. package/dist/client/agentSquadStatus.d.ts +37 -0
  37. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  38. package/dist/client/audioPlayer.d.ts +12 -2
  39. package/dist/client/bargeInMonitor.d.ts +7 -0
  40. package/dist/client/browserMedia.d.ts +8 -0
  41. package/dist/client/browserNoiseSuppression.d.ts +42 -0
  42. package/dist/client/browserVoiceSupport.d.ts +22 -0
  43. package/dist/client/callDebugger.d.ts +21 -0
  44. package/dist/client/callDebuggerWidget.d.ts +30 -0
  45. package/dist/client/callPlayer.d.ts +41 -0
  46. package/dist/client/campaignDialerProof.d.ts +25 -0
  47. package/dist/client/connection.d.ts +13 -4
  48. package/dist/client/controller.d.ts +1 -1
  49. package/dist/client/conversationAnalytics.d.ts +30 -0
  50. package/dist/client/costDashboard.d.ts +27 -0
  51. package/dist/client/createVoiceStream.d.ts +1 -1
  52. package/dist/client/deliveryRuntime.d.ts +36 -0
  53. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  54. package/dist/client/duplex.d.ts +2 -2
  55. package/dist/client/htmx.d.ts +1 -1
  56. package/dist/client/htmxAttributes.d.ts +24 -0
  57. package/dist/client/htmxBootstrap.js +1808 -159
  58. package/dist/client/htmxDashboardRenderers.d.ts +71 -0
  59. package/dist/client/index.d.ts +109 -33
  60. package/dist/client/index.js +10799 -646
  61. package/dist/client/liveAgentConsole.d.ts +28 -0
  62. package/dist/client/liveCallViewer.d.ts +42 -0
  63. package/dist/client/liveOps.d.ts +22 -0
  64. package/dist/client/liveOpsWidget.d.ts +23 -0
  65. package/dist/client/liveTurnLatency.d.ts +41 -0
  66. package/dist/client/microphone.d.ts +5 -4
  67. package/dist/client/opsActionCenter.d.ts +56 -0
  68. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  69. package/dist/client/opsActionHistory.d.ts +21 -0
  70. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  71. package/dist/client/opsStatus.d.ts +21 -0
  72. package/dist/client/opsStatusWidget.d.ts +10 -10
  73. package/dist/client/platformCoverage.d.ts +21 -0
  74. package/dist/client/platformCoverageWidget.d.ts +38 -0
  75. package/dist/client/profileComparison.d.ts +21 -0
  76. package/dist/client/profileComparisonWidget.d.ts +41 -0
  77. package/dist/client/profileSwitchRecommendation.d.ts +21 -0
  78. package/dist/client/profileSwitchRecommendationWidget.d.ts +12 -0
  79. package/dist/client/proofTrends.d.ts +21 -0
  80. package/dist/client/proofTrendsWidget.d.ts +38 -0
  81. package/dist/client/providerCapabilities.d.ts +5 -3
  82. package/dist/client/providerCapabilitiesWidget.d.ts +6 -6
  83. package/dist/client/providerContracts.d.ts +21 -0
  84. package/dist/client/providerContractsWidget.d.ts +37 -0
  85. package/dist/client/providerSimulationControls.d.ts +3 -3
  86. package/dist/client/providerSimulationControlsWidget.d.ts +5 -5
  87. package/dist/client/providerStatus.d.ts +5 -3
  88. package/dist/client/providerStatusWidget.d.ts +6 -6
  89. package/dist/client/reactiveSource.d.ts +8 -0
  90. package/dist/client/readinessFailures.d.ts +21 -0
  91. package/dist/client/readinessFailuresWidget.d.ts +42 -0
  92. package/dist/client/reconnectProfileEvidence.d.ts +21 -0
  93. package/dist/client/reconnectProfileEvidenceWidget.d.ts +39 -0
  94. package/dist/client/replayTimeline.d.ts +26 -0
  95. package/dist/client/routingStatus.d.ts +5 -3
  96. package/dist/client/routingStatusWidget.d.ts +10 -6
  97. package/dist/client/sessionObservability.d.ts +21 -0
  98. package/dist/client/sessionObservabilityWidget.d.ts +31 -0
  99. package/dist/client/sessionSnapshot.d.ts +23 -0
  100. package/dist/client/sessionSnapshotWidget.d.ts +33 -0
  101. package/dist/client/store.d.ts +1 -1
  102. package/dist/client/timeStretch.d.ts +5 -0
  103. package/dist/client/traceTimeline.d.ts +21 -0
  104. package/dist/client/traceTimelineWidget.d.ts +36 -0
  105. package/dist/client/turnLatency.d.ts +24 -0
  106. package/dist/client/turnLatencyWidget.d.ts +33 -0
  107. package/dist/client/turnQuality.d.ts +5 -3
  108. package/dist/client/turnQualityWidget.d.ts +6 -6
  109. package/dist/client/voiceWidgetView.d.ts +48 -0
  110. package/dist/client/workflowStatus.d.ts +5 -3
  111. package/dist/core/agent.d.ts +251 -0
  112. package/dist/core/agentPerformanceReport.d.ts +40 -0
  113. package/dist/core/agentSquadContract.d.ts +98 -0
  114. package/dist/core/agentState.d.ts +12 -0
  115. package/dist/core/agentTools.d.ts +132 -0
  116. package/dist/core/aiScorecard.d.ts +32 -0
  117. package/dist/core/aiVoiceModel.d.ts +15 -0
  118. package/dist/core/amdDetector.d.ts +25 -0
  119. package/dist/{assistant.d.ts → core/assistant.d.ts} +24 -14
  120. package/dist/core/assistantExperiment.d.ts +42 -0
  121. package/dist/{assistantHealth.d.ts → core/assistantHealth.d.ts} +9 -9
  122. package/dist/{assistantMemory.d.ts → core/assistantMemory.d.ts} +10 -10
  123. package/dist/core/assistantMode.d.ts +22 -0
  124. package/dist/{audioConditioning.d.ts → core/audioConditioning.d.ts} +2 -2
  125. package/dist/core/audit.d.ts +131 -0
  126. package/dist/core/auditDeliveryRoutes.d.ts +85 -0
  127. package/dist/core/auditExport.d.ts +34 -0
  128. package/dist/core/auditRoutes.d.ts +66 -0
  129. package/dist/core/auditSinks.d.ts +151 -0
  130. package/dist/core/backchannel.d.ts +25 -0
  131. package/dist/core/bargeInDetector.d.ts +51 -0
  132. package/dist/core/bargeInRoutes.d.ts +56 -0
  133. package/dist/core/bookingFlow.d.ts +43 -0
  134. package/dist/core/browserCallProfiles.d.ts +120 -0
  135. package/dist/core/browserMediaRoutes.d.ts +62 -0
  136. package/dist/core/cachedTTS.d.ts +54 -0
  137. package/dist/core/calendarAdapter.d.ts +47 -0
  138. package/dist/core/calendarSlots.d.ts +35 -0
  139. package/dist/core/callDebugger.d.ts +66 -0
  140. package/dist/core/callDisposition.d.ts +38 -0
  141. package/dist/core/callQuota.d.ts +54 -0
  142. package/dist/core/callScorecard.d.ts +53 -0
  143. package/dist/core/callerCRMLinker.d.ts +29 -0
  144. package/dist/core/callerMemory.d.ts +37 -0
  145. package/dist/core/callingWindow.d.ts +26 -0
  146. package/dist/core/campaign.d.ts +795 -0
  147. package/dist/core/campaignControls.d.ts +37 -0
  148. package/dist/core/campaignDialers.d.ts +111 -0
  149. package/dist/core/campaignTemplate.d.ts +16 -0
  150. package/dist/core/competitiveCoverage.d.ts +141 -0
  151. package/dist/core/conversationSimulator.d.ts +73 -0
  152. package/dist/{correction.d.ts → core/correction.d.ts} +5 -4
  153. package/dist/core/costAccounting.d.ts +90 -0
  154. package/dist/core/costPredictor.d.ts +74 -0
  155. package/dist/core/crmCallLogger.d.ts +37 -0
  156. package/dist/core/crmContract.d.ts +70 -0
  157. package/dist/core/dataControl.d.ts +180 -0
  158. package/dist/core/debugTiming.d.ts +11 -0
  159. package/dist/core/defineVoiceAssistant.d.ts +68 -0
  160. package/dist/core/deliveryRuntime.d.ts +159 -0
  161. package/dist/core/deliverySinkRoutes.d.ts +117 -0
  162. package/dist/core/demoReadyRoutes.d.ts +98 -0
  163. package/dist/{diagnosticsRoutes.d.ts → core/diagnosticsRoutes.d.ts} +2 -2
  164. package/dist/core/dncRegistry.d.ts +38 -0
  165. package/dist/core/dtmfCollector.d.ts +37 -0
  166. package/dist/{evalRoutes.d.ts → core/evalRoutes.d.ts} +26 -20
  167. package/dist/{fileStore.d.ts → core/fileStore.d.ts} +34 -20
  168. package/dist/core/guardrails.d.ts +128 -0
  169. package/dist/{handoff.d.ts → core/handoff.d.ts} +10 -10
  170. package/dist/{handoffHealth.d.ts → core/handoffHealth.d.ts} +9 -9
  171. package/dist/core/hardenedFetch.d.ts +1 -0
  172. package/dist/core/holdAudio.d.ts +23 -0
  173. package/dist/{htmx.d.ts → core/htmx.d.ts} +2 -2
  174. package/dist/core/htmxDashboardRoutes.d.ts +250 -0
  175. package/dist/core/iceServers.d.ts +34 -0
  176. package/dist/core/incidentBundle.d.ts +119 -0
  177. package/dist/core/incidentTimeline.d.ts +260 -0
  178. package/dist/core/ivrPlan.d.ts +40 -0
  179. package/dist/core/latencySlo.d.ts +56 -0
  180. package/dist/core/liveCoach.d.ts +43 -0
  181. package/dist/core/liveLatency.d.ts +78 -0
  182. package/dist/core/liveOps.d.ts +190 -0
  183. package/dist/core/llmJudge.d.ts +45 -0
  184. package/dist/{logger.d.ts → core/logger.d.ts} +1 -2
  185. package/dist/core/mcpToolset.d.ts +58 -0
  186. package/dist/core/mediaPipelineRoutes.d.ts +171 -0
  187. package/dist/core/mediaPipelineSurfaces.d.ts +48 -0
  188. package/dist/{memoryStore.d.ts → core/memoryStore.d.ts} +1 -1
  189. package/dist/core/midCallSummary.d.ts +27 -0
  190. package/dist/{modelAdapters.d.ts → core/modelAdapters.d.ts} +64 -7
  191. package/dist/core/monitor.d.ts +148 -0
  192. package/dist/core/multilingualProof.d.ts +77 -0
  193. package/dist/core/noShowPredictor.d.ts +46 -0
  194. package/dist/core/numberNormalizer.d.ts +1 -0
  195. package/dist/core/oauth2TokenSource.d.ts +21 -0
  196. package/dist/core/observabilityExport.d.ts +501 -0
  197. package/dist/core/openaiTTS.d.ts +18 -0
  198. package/dist/core/operationalStatus.d.ts +87 -0
  199. package/dist/core/operationsRecord.d.ts +371 -0
  200. package/dist/{ops.d.ts → core/ops.d.ts} +70 -70
  201. package/dist/core/opsActionAuditRoutes.d.ts +99 -0
  202. package/dist/{opsConsoleRoutes.d.ts → core/opsConsoleRoutes.d.ts} +11 -8
  203. package/dist/{opsPresets.d.ts → core/opsPresets.d.ts} +2 -2
  204. package/dist/core/opsRecovery.d.ts +137 -0
  205. package/dist/{opsRuntime.d.ts → core/opsRuntime.d.ts} +6 -6
  206. package/dist/{opsSinks.d.ts → core/opsSinks.d.ts} +19 -19
  207. package/dist/core/opsStatus.d.ts +76 -0
  208. package/dist/core/opsStatusRoutes.d.ts +33 -0
  209. package/dist/{opsWebhook.d.ts → core/opsWebhook.d.ts} +15 -15
  210. package/dist/core/otelExporter.d.ts +83 -0
  211. package/dist/core/outcomeContract.d.ts +146 -0
  212. package/dist/{outcomeRecipes.d.ts → core/outcomeRecipes.d.ts} +4 -4
  213. package/dist/core/pathway.d.ts +94 -0
  214. package/dist/core/pathwayCompiler.d.ts +31 -0
  215. package/dist/core/pathwayGenerator.d.ts +27 -0
  216. package/dist/core/pathwayRuntime.d.ts +57 -0
  217. package/dist/core/pathwaySlotCollector.d.ts +29 -0
  218. package/dist/core/pathwayVisualizer.d.ts +8 -0
  219. package/dist/core/phoneAgent.d.ts +139 -0
  220. package/dist/core/phoneAgentProductionSmoke.d.ts +115 -0
  221. package/dist/core/phoneProvisioning.d.ts +29 -0
  222. package/dist/core/platformCoverage.d.ts +91 -0
  223. package/dist/{plugin.d.ts → core/plugin.d.ts} +11 -8
  224. package/dist/core/postCallAnalysis.d.ts +98 -0
  225. package/dist/core/postCallSurvey.d.ts +41 -0
  226. package/dist/{postgresStore.d.ts → core/postgresStore.d.ts} +20 -9
  227. package/dist/{presets.d.ts → core/presets.d.ts} +3 -3
  228. package/dist/core/productionReadiness.d.ts +757 -0
  229. package/dist/core/profileSwitchRecommendation.d.ts +350 -0
  230. package/dist/core/promptInjectionGuard.d.ts +30 -0
  231. package/dist/core/proofAssertions.d.ts +32 -0
  232. package/dist/core/proofPack.d.ts +211 -0
  233. package/dist/core/proofRunner.d.ts +79 -0
  234. package/dist/core/proofTrends.d.ts +966 -0
  235. package/dist/{providerAdapters.d.ts → core/providerAdapters.d.ts} +5 -5
  236. package/dist/{providerCapabilities.d.ts → core/providerCapabilities.d.ts} +9 -9
  237. package/dist/core/providerDecisionTraces.d.ts +130 -0
  238. package/dist/{providerHealth.d.ts → core/providerHealth.d.ts} +15 -5
  239. package/dist/core/providerOrchestration.d.ts +109 -0
  240. package/dist/core/providerRouterTraces.d.ts +35 -0
  241. package/dist/core/providerRoutingContract.d.ts +71 -0
  242. package/dist/core/providerSlo.d.ts +142 -0
  243. package/dist/core/providerStackRecommendations.d.ts +188 -0
  244. package/dist/core/qualityDriftDetector.d.ts +44 -0
  245. package/dist/{qualityRoutes.d.ts → core/qualityRoutes.d.ts} +7 -7
  246. package/dist/{queue.d.ts → core/queue.d.ts} +48 -39
  247. package/dist/core/ragTool.d.ts +52 -0
  248. package/dist/core/readinessProfiles.d.ts +45 -0
  249. package/dist/core/realtimeChannel.d.ts +136 -0
  250. package/dist/core/realtimeProviderContracts.d.ts +133 -0
  251. package/dist/core/reconnectContract.d.ts +177 -0
  252. package/dist/core/recordingRedaction.d.ts +47 -0
  253. package/dist/core/recordingStore.d.ts +60 -0
  254. package/dist/core/redaction.d.ts +13 -0
  255. package/dist/core/reminderScheduler.d.ts +43 -0
  256. package/dist/{resilienceRoutes.d.ts → core/resilienceRoutes.d.ts} +34 -5
  257. package/dist/core/retention.d.ts +37 -0
  258. package/dist/core/retryPolicy.d.ts +38 -0
  259. package/dist/core/routeAuth.d.ts +58 -0
  260. package/dist/{routing.d.ts → core/routing.d.ts} +2 -2
  261. package/dist/{runtimeOps.d.ts → core/runtimeOps.d.ts} +3 -3
  262. package/dist/{s3Store.d.ts → core/s3Store.d.ts} +12 -3
  263. package/dist/core/scorecardCalibration.d.ts +31 -0
  264. package/dist/core/scribe.d.ts +50 -0
  265. package/dist/core/semanticTurn.d.ts +37 -0
  266. package/dist/{session.d.ts → core/session.d.ts} +1 -1
  267. package/dist/core/sessionObservability.d.ts +145 -0
  268. package/dist/{sessionReplay.d.ts → core/sessionReplay.d.ts} +31 -19
  269. package/dist/core/sessionSnapshot.d.ts +109 -0
  270. package/dist/core/simulationSuite.d.ts +144 -0
  271. package/dist/core/sloCalibration.d.ts +185 -0
  272. package/dist/{sqliteStore.d.ts → core/sqliteStore.d.ts} +20 -9
  273. package/dist/{store.d.ts → core/store.d.ts} +1 -1
  274. package/dist/core/supervisorPermissions.d.ts +33 -0
  275. package/dist/core/supervisorPresence.d.ts +49 -0
  276. package/dist/core/telephonyMediaRoutes.d.ts +72 -0
  277. package/dist/core/telephonyOutcome.d.ts +269 -0
  278. package/dist/{toolContract.d.ts → core/toolContract.d.ts} +46 -15
  279. package/dist/{toolRuntime.d.ts → core/toolRuntime.d.ts} +4 -4
  280. package/dist/{trace.d.ts → core/trace.d.ts} +61 -22
  281. package/dist/core/traceDeliveryRoutes.d.ts +86 -0
  282. package/dist/core/traceTimeline.d.ts +97 -0
  283. package/dist/core/transcriptAnnotator.d.ts +41 -0
  284. package/dist/{turnDetection.d.ts → core/turnDetection.d.ts} +2 -1
  285. package/dist/core/turnLatency.d.ts +95 -0
  286. package/dist/core/turnProfiles.d.ts +3 -0
  287. package/dist/{turnQuality.d.ts → core/turnQuality.d.ts} +9 -9
  288. package/dist/core/types.d.ts +1786 -0
  289. package/dist/core/vapiAdapter.d.ts +160 -0
  290. package/dist/core/variableAnalytics.d.ts +47 -0
  291. package/dist/core/voiceConfiguration.d.ts +8 -0
  292. package/dist/core/voiceMonitoring.d.ts +444 -0
  293. package/dist/core/webhookFanout.d.ts +48 -0
  294. package/dist/core/webhookVerification.d.ts +27 -0
  295. package/dist/core/whisperChannel.d.ts +50 -0
  296. package/dist/{workflowContract.d.ts → core/workflowContract.d.ts} +21 -21
  297. package/dist/core/writeBehindStore.d.ts +41 -0
  298. package/dist/core/zeroDataRetention.d.ts +31 -0
  299. package/dist/drizzle/assistantMemory.d.ts +100 -0
  300. package/dist/drizzle/eval.d.ts +55 -0
  301. package/dist/drizzle/handoff.d.ts +56 -0
  302. package/dist/drizzle/incidentBundle.d.ts +55 -0
  303. package/dist/drizzle/index.d.ts +991 -0
  304. package/dist/drizzle/index.js +3060 -0
  305. package/dist/drizzle/observabilityExport.d.ts +55 -0
  306. package/dist/drizzle/proofTrends.d.ts +114 -0
  307. package/dist/drizzle/runtimeStorage.d.ts +1183 -0
  308. package/dist/drizzle/shared.d.ts +69 -0
  309. package/dist/embed/index.d.ts +38 -0
  310. package/dist/embed/index.js +2100 -0
  311. package/dist/embed/voice-widget.js +10 -0
  312. package/dist/index.d.ts +379 -82
  313. package/dist/index.js +48514 -8241
  314. package/dist/internal/evidence.d.ts +10 -0
  315. package/dist/internal/html.d.ts +6 -0
  316. package/dist/internal/status.d.ts +7 -0
  317. package/dist/manifest.d.ts +17 -0
  318. package/dist/manifest.js +424 -0
  319. package/dist/manifest.json +427 -0
  320. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  321. package/dist/react/VoiceCallDebuggerLaunch.d.ts +6 -0
  322. package/dist/react/VoiceCallPlayer.d.ts +11 -0
  323. package/dist/react/VoiceCostDashboard.d.ts +10 -0
  324. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  325. package/dist/react/VoiceLiveAgentConsole.d.ts +11 -0
  326. package/dist/react/VoiceLiveCallViewer.d.ts +9 -0
  327. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  328. package/dist/react/VoiceOpsStatus.d.ts +1 -1
  329. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  330. package/dist/react/VoiceProfileComparison.d.ts +6 -0
  331. package/dist/react/VoiceProfileSwitchRecommendation.d.ts +6 -0
  332. package/dist/react/VoiceProofTrends.d.ts +6 -0
  333. package/dist/react/VoiceProviderCapabilities.d.ts +1 -1
  334. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  335. package/dist/react/VoiceProviderSimulationControls.d.ts +1 -1
  336. package/dist/react/VoiceProviderStatus.d.ts +1 -1
  337. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  338. package/dist/react/VoiceReconnectProfileEvidence.d.ts +6 -0
  339. package/dist/react/VoiceReplayTimeline.d.ts +6 -0
  340. package/dist/react/VoiceRoutingStatus.d.ts +1 -1
  341. package/dist/react/VoiceSessionObservability.d.ts +6 -0
  342. package/dist/react/VoiceSessionSnapshot.d.ts +6 -0
  343. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  344. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  345. package/dist/react/VoiceTurnQuality.d.ts +1 -1
  346. package/dist/react/VoiceWidget.d.ts +13 -0
  347. package/dist/react/index.d.ts +80 -15
  348. package/dist/react/index.js +13740 -2052
  349. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  350. package/dist/react/useVoiceCallDebugger.d.ts +8 -0
  351. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  352. package/dist/react/useVoiceController.d.ts +9 -2
  353. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  354. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  355. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  356. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  357. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  358. package/dist/react/useVoiceProfileComparison.d.ts +8 -0
  359. package/dist/react/useVoiceProfileSwitchRecommendation.d.ts +8 -0
  360. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  361. package/dist/react/useVoiceProviderCapabilities.d.ts +1 -1
  362. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  363. package/dist/react/useVoiceProviderSimulationControls.d.ts +1 -1
  364. package/dist/react/useVoiceProviderStatus.d.ts +1 -1
  365. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  366. package/dist/react/useVoiceReconnectProfileEvidence.d.ts +8 -0
  367. package/dist/react/useVoiceRoutingStatus.d.ts +1 -1
  368. package/dist/react/useVoiceSessionObservability.d.ts +8 -0
  369. package/dist/react/useVoiceSessionSnapshot.d.ts +9 -0
  370. package/dist/react/useVoiceStream.d.ts +9 -2
  371. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  372. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  373. package/dist/react/useVoiceTurnQuality.d.ts +1 -1
  374. package/dist/react/useVoiceWorkflowStatus.d.ts +1 -1
  375. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  376. package/dist/svelte/createVoiceCallDebugger.d.ts +10 -0
  377. package/dist/svelte/createVoiceCallPlayer.d.ts +33 -0
  378. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  379. package/dist/svelte/createVoiceCostDashboard.d.ts +13 -0
  380. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  381. package/dist/svelte/createVoiceLiveAgentConsole.d.ts +23 -0
  382. package/dist/svelte/createVoiceLiveCallViewer.d.ts +26 -0
  383. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  384. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  385. package/dist/svelte/createVoiceOpsStatus.d.ts +4 -4
  386. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  387. package/dist/svelte/createVoiceProfileComparison.d.ts +7 -0
  388. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  389. package/dist/svelte/createVoiceProviderCapabilities.d.ts +2 -2
  390. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  391. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +2 -2
  392. package/dist/svelte/createVoiceProviderStatus.d.ts +2 -2
  393. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  394. package/dist/svelte/createVoiceReconnectProfileEvidence.d.ts +7 -0
  395. package/dist/svelte/createVoiceReplayTimeline.d.ts +13 -0
  396. package/dist/svelte/createVoiceRoutingStatus.d.ts +2 -2
  397. package/dist/svelte/createVoiceSessionObservability.d.ts +10 -0
  398. package/dist/svelte/createVoiceSessionSnapshot.d.ts +11 -0
  399. package/dist/svelte/createVoiceStream.d.ts +1 -1
  400. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  401. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  402. package/dist/svelte/createVoiceTurnQuality.d.ts +2 -2
  403. package/dist/svelte/createVoiceWidget.d.ts +19 -0
  404. package/dist/svelte/createVoiceWorkflowStatus.d.ts +2 -2
  405. package/dist/svelte/index.d.ts +37 -10
  406. package/dist/svelte/index.js +7465 -1676
  407. package/dist/telephony/contract.d.ts +61 -0
  408. package/dist/telephony/matrix.d.ts +97 -0
  409. package/dist/telephony/plivo.d.ts +302 -0
  410. package/dist/telephony/response.d.ts +1 -1
  411. package/dist/telephony/security.d.ts +182 -0
  412. package/dist/telephony/telnyx.d.ts +290 -0
  413. package/dist/telephony/twilio.d.ts +164 -15
  414. package/dist/testing/accuracy.d.ts +18 -1
  415. package/dist/testing/audioMatrix.d.ts +22 -0
  416. package/dist/testing/benchmark.d.ts +24 -15
  417. package/dist/testing/confidenceCalibration.d.ts +19 -0
  418. package/dist/testing/conformance.d.ts +11 -0
  419. package/dist/testing/corrected.d.ts +9 -9
  420. package/dist/testing/criticalFields.d.ts +22 -0
  421. package/dist/testing/duplex.d.ts +5 -5
  422. package/dist/testing/fixtures.d.ts +7 -4
  423. package/dist/testing/index.d.ts +21 -13
  424. package/dist/testing/index.js +9171 -1290
  425. package/dist/testing/ioProviderSimulator.d.ts +5 -5
  426. package/dist/testing/outcomes.d.ts +12 -0
  427. package/dist/testing/provenance.d.ts +44 -0
  428. package/dist/testing/providerSimulator.d.ts +5 -5
  429. package/dist/testing/review.d.ts +8 -8
  430. package/dist/testing/routingBenchmark.d.ts +16 -0
  431. package/dist/testing/sessionBenchmark.d.ts +13 -13
  432. package/dist/testing/statistics.d.ts +30 -0
  433. package/dist/testing/stt.d.ts +3 -3
  434. package/dist/testing/telephony.d.ts +27 -2
  435. package/dist/testing/tts.d.ts +2 -2
  436. package/dist/vue/VoiceCallDebuggerLaunch.d.ts +72 -0
  437. package/dist/vue/VoiceCallPlayer.d.ts +40 -0
  438. package/dist/vue/VoiceCostDashboard.d.ts +57 -0
  439. package/dist/vue/VoiceDeliveryRuntime.d.ts +34 -0
  440. package/dist/vue/VoiceLiveAgentConsole.d.ts +50 -0
  441. package/dist/vue/VoiceLiveCallViewer.d.ts +35 -0
  442. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  443. package/dist/vue/VoiceOpsStatus.d.ts +4 -0
  444. package/dist/vue/VoicePlatformCoverage.d.ts +27 -0
  445. package/dist/vue/VoiceProofTrends.d.ts +25 -0
  446. package/dist/vue/VoiceProviderCapabilities.d.ts +4 -0
  447. package/dist/vue/VoiceProviderContracts.d.ts +25 -0
  448. package/dist/vue/VoiceProviderSimulationControls.d.ts +3 -3
  449. package/dist/vue/VoiceProviderStatus.d.ts +4 -0
  450. package/dist/vue/VoiceReadinessFailures.d.ts +25 -0
  451. package/dist/vue/VoiceReconnectProfileEvidence.d.ts +25 -0
  452. package/dist/vue/VoiceReplayTimeline.d.ts +17 -0
  453. package/dist/vue/VoiceRoutingStatus.d.ts +4 -0
  454. package/dist/vue/VoiceSessionObservability.d.ts +27 -0
  455. package/dist/vue/VoiceSessionSnapshot.d.ts +72 -0
  456. package/dist/vue/VoiceTurnLatency.d.ts +73 -0
  457. package/dist/vue/VoiceTurnQuality.d.ts +4 -0
  458. package/dist/vue/VoiceWidget.d.ts +77 -0
  459. package/dist/vue/index.d.ts +49 -15
  460. package/dist/vue/index.js +13097 -2219
  461. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  462. package/dist/vue/useVoiceCallDebugger.d.ts +10 -0
  463. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  464. package/dist/vue/useVoiceController.d.ts +10 -7
  465. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  466. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  467. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  468. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  469. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  470. package/dist/vue/useVoiceProfileComparison.d.ts +9 -0
  471. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  472. package/dist/vue/useVoiceProviderCapabilities.d.ts +3 -3
  473. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  474. package/dist/vue/useVoiceProviderSimulationControls.d.ts +2 -2
  475. package/dist/vue/useVoiceProviderStatus.d.ts +3 -3
  476. package/dist/vue/useVoiceReadinessFailures.d.ts +959 -0
  477. package/dist/vue/useVoiceReconnectProfileEvidence.d.ts +9 -0
  478. package/dist/vue/useVoiceRoutingStatus.d.ts +2 -2
  479. package/dist/vue/useVoiceSessionObservability.d.ts +9 -0
  480. package/dist/vue/useVoiceSessionSnapshot.d.ts +10 -0
  481. package/dist/vue/useVoiceStream.d.ts +10 -6
  482. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  483. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  484. package/dist/vue/useVoiceTurnQuality.d.ts +3 -3
  485. package/dist/vue/useVoiceWorkflowStatus.d.ts +3 -3
  486. package/package.json +247 -256
  487. package/dist/agent.d.ts +0 -115
  488. package/dist/angular/voice-app-kit-status.service.d.ts +0 -12
  489. package/dist/angular/voice-ops-status.component.d.ts +0 -15
  490. package/dist/appKit.d.ts +0 -94
  491. package/dist/client/appKitStatus.d.ts +0 -19
  492. package/dist/react/useVoiceAppKitStatus.d.ts +0 -8
  493. package/dist/svelte/createVoiceAppKitStatus.d.ts +0 -8
  494. package/dist/turnProfiles.d.ts +0 -6
  495. package/dist/types.d.ts +0 -968
  496. package/dist/vue/useVoiceAppKitStatus.d.ts +0 -9
  497. package/fixtures/README.md +0 -57
  498. package/fixtures/manifest.json +0 -199
  499. package/fixtures/pcm/dialogue-three-clean.pcm +0 -0
  500. package/fixtures/pcm/dialogue-three-mixed.pcm +0 -0
  501. package/fixtures/pcm/dialogue-two-clean.pcm +0 -0
  502. package/fixtures/pcm/dialogue-two-noisy.pcm +0 -0
  503. package/fixtures/pcm/multiturn-three-mixed.pcm +0 -0
  504. package/fixtures/pcm/multiturn-two-clean.pcm +0 -0
  505. package/fixtures/pcm/quietly-alone-clean.pcm +0 -0
  506. package/fixtures/pcm/rainstorms-noisy.pcm +0 -0
  507. package/fixtures/pcm/stella-bulgaria-bulgarian20.pcm +0 -0
  508. package/fixtures/pcm/stella-ghana-english507.pcm +0 -0
  509. package/fixtures/pcm/stella-india-english37.pcm +0 -0
  510. package/fixtures/pcm/stella-jamaica-jamaican-creole-english1.pcm +0 -0
  511. package/fixtures/pcm/stella-liberia-liberian-pidgin-english2.pcm +0 -0
  512. package/fixtures/pcm/stella-pakistan-english519.pcm +0 -0
  513. package/fixtures/pcm/stella-sierra-leone-krio5.pcm +0 -0
  514. package/fixtures/pcm/stella-singapore-english655.pcm +0 -0
  515. package/fixtures/pcm/traveled-back-route-clean.pcm +0 -0
@@ -1,3 +1,6 @@
1
+ // src/internal/html.ts
2
+ var escapeHtml = (value) => String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
3
+
1
4
  // src/client/htmx.ts
2
5
  var DEFAULT_EVENT_NAME = "voice-refresh";
3
6
  var DEFAULT_QUERY_PARAM = "sessionId";
@@ -100,37 +103,89 @@ var createMicrophoneCapture = (options) => {
100
103
  let sourceNode = null;
101
104
  let processorNode = null;
102
105
  let mediaStream = null;
103
- const start = async () => {
104
- if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
105
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
106
- }
107
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
108
- if (!AudioContextCtor) {
109
- throw new Error("Browser microphone capture requires AudioContext support.");
110
- }
111
- mediaStream = await navigator.mediaDevices.getUserMedia({
112
- audio: {
113
- channelCount: options.channelCount ?? 1
106
+ let lifecycleGeneration = 0;
107
+ let startPromise = null;
108
+ const release = (resources) => {
109
+ resources.processorNode?.disconnect();
110
+ resources.sourceNode?.disconnect();
111
+ resources.mediaStream?.getTracks().forEach((track) => track.stop());
112
+ resources.audioContext?.close();
113
+ };
114
+ const start = () => {
115
+ if (mediaStream)
116
+ return Promise.resolve();
117
+ if (startPromise)
118
+ return startPromise;
119
+ const generation = ++lifecycleGeneration;
120
+ const operation = async () => {
121
+ if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
122
+ throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
123
+ }
124
+ const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
125
+ if (!AudioContextCtor) {
126
+ throw new Error("Browser microphone capture requires AudioContext support.");
127
+ }
128
+ let pendingAudioContext = null;
129
+ let pendingMediaStream = null;
130
+ let pendingProcessorNode = null;
131
+ let pendingSourceNode = null;
132
+ try {
133
+ pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
134
+ audio: {
135
+ autoGainControl: true,
136
+ channelCount: options.channelCount ?? 1,
137
+ echoCancellation: true,
138
+ noiseSuppression: true
139
+ }
140
+ });
141
+ if (generation !== lifecycleGeneration) {
142
+ throw new Error("Microphone capture stopped during startup.");
143
+ }
144
+ pendingAudioContext = new AudioContextCtor;
145
+ if (pendingAudioContext.state === "suspended") {
146
+ await pendingAudioContext.resume();
147
+ }
148
+ if (generation !== lifecycleGeneration) {
149
+ throw new Error("Microphone capture stopped during startup.");
150
+ }
151
+ pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
152
+ pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
153
+ pendingProcessorNode.onaudioprocess = (event) => {
154
+ const channel = event.inputBuffer.getChannelData(0);
155
+ const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
156
+ const pcm = floatTo16BitPCM(downsampled);
157
+ options.onLevel?.(getPcmLevel(pcm));
158
+ options.onAudio(pcm);
159
+ };
160
+ pendingSourceNode.connect(pendingProcessorNode);
161
+ pendingProcessorNode.connect(pendingAudioContext.destination);
162
+ if (generation !== lifecycleGeneration) {
163
+ throw new Error("Microphone capture stopped during startup.");
164
+ }
165
+ audioContext = pendingAudioContext;
166
+ mediaStream = pendingMediaStream;
167
+ processorNode = pendingProcessorNode;
168
+ sourceNode = pendingSourceNode;
169
+ } catch (error) {
170
+ release({
171
+ audioContext: pendingAudioContext,
172
+ mediaStream: pendingMediaStream,
173
+ processorNode: pendingProcessorNode,
174
+ sourceNode: pendingSourceNode
175
+ });
176
+ throw error;
114
177
  }
115
- });
116
- audioContext = new AudioContextCtor;
117
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
118
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
119
- processorNode.onaudioprocess = (event) => {
120
- const channel = event.inputBuffer.getChannelData(0);
121
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
122
- const pcm = floatTo16BitPCM(downsampled);
123
- options.onLevel?.(getPcmLevel(pcm));
124
- options.onAudio(pcm);
125
178
  };
126
- sourceNode.connect(processorNode);
127
- processorNode.connect(audioContext.destination);
179
+ const pending = operation().finally(() => {
180
+ if (startPromise === pending)
181
+ startPromise = null;
182
+ });
183
+ startPromise = pending;
184
+ return pending;
128
185
  };
129
186
  const stop = () => {
130
- processorNode?.disconnect();
131
- sourceNode?.disconnect();
132
- mediaStream?.getTracks().forEach((track) => track.stop());
133
- audioContext?.close();
187
+ lifecycleGeneration += 1;
188
+ release({ audioContext, mediaStream, processorNode, sourceNode });
134
189
  options.onLevel?.(0);
135
190
  audioContext = null;
136
191
  mediaStream = null;
@@ -181,13 +236,25 @@ var serverMessageToAction = (message) => {
181
236
  case "assistant":
182
237
  return {
183
238
  text: message.text,
239
+ turnId: message.turnId,
184
240
  type: "assistant"
185
241
  };
242
+ case "assistant_delta":
243
+ return {
244
+ delta: message.delta,
245
+ turnId: message.turnId,
246
+ type: "assistant_delta"
247
+ };
186
248
  case "complete":
187
249
  return {
188
250
  sessionId: message.sessionId,
189
251
  type: "complete"
190
252
  };
253
+ case "connection":
254
+ return {
255
+ reconnect: message.reconnect,
256
+ type: "connection"
257
+ };
191
258
  case "call_lifecycle":
192
259
  return {
193
260
  event: message.event,
@@ -197,8 +264,11 @@ var serverMessageToAction = (message) => {
197
264
  case "error":
198
265
  return {
199
266
  message: normalizeErrorMessage(message.message),
267
+ recoverable: message.recoverable,
200
268
  type: "error"
201
269
  };
270
+ case "call_control_ack":
271
+ return null;
202
272
  case "final":
203
273
  return {
204
274
  transcript: message.transcript,
@@ -209,9 +279,29 @@ var serverMessageToAction = (message) => {
209
279
  transcript: message.transcript,
210
280
  type: "partial"
211
281
  };
282
+ case "playback_rate":
283
+ return {
284
+ rate: message.rate,
285
+ type: "playback_rate"
286
+ };
287
+ case "replay":
288
+ return {
289
+ assistantTexts: message.assistantTexts,
290
+ call: message.call,
291
+ partial: message.partial,
292
+ scenarioId: message.scenarioId,
293
+ sessionId: message.sessionId,
294
+ sessionMetadata: message.sessionMetadata,
295
+ status: message.status,
296
+ turns: message.turns,
297
+ type: "replay"
298
+ };
212
299
  case "session":
213
300
  return {
301
+ paused: message.paused,
302
+ pauseExpiresAt: message.pauseExpiresAt,
214
303
  sessionId: message.sessionId,
304
+ sessionMetadata: message.sessionMetadata,
215
305
  scenarioId: message.scenarioId,
216
306
  status: message.status,
217
307
  type: "session"
@@ -226,30 +316,276 @@ var serverMessageToAction = (message) => {
226
316
  }
227
317
  };
228
318
 
319
+ // node_modules/@absolutejs/media/dist/index.js
320
+ var TAU = Math.PI * 2;
321
+ var pushIssue = (issues, severity, code, message) => {
322
+ issues.push({ code, message, severity });
323
+ };
324
+ var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
325
+ var max = (values) => values.length === 0 ? undefined : Math.max(...values);
326
+ var numericStat = (stat, key) => {
327
+ const value = stat[key];
328
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
329
+ };
330
+ var booleanStat = (stat, key) => {
331
+ const value = stat[key];
332
+ return typeof value === "boolean" ? value : undefined;
333
+ };
334
+ var stringStat = (stat, key) => {
335
+ const value = stat[key];
336
+ return typeof value === "string" ? value : undefined;
337
+ };
338
+ var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
339
+ var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
340
+ var normalizeWebRTCStat = (stat) => {
341
+ const sample = {};
342
+ for (const [key, value] of Object.entries(stat)) {
343
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
344
+ sample[key] = value;
345
+ }
346
+ }
347
+ return sample;
348
+ };
349
+ var buildMediaWebRTCStatsReport = (input = {}) => {
350
+ const stats = input.stats ?? [];
351
+ const issues = [];
352
+ const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
353
+ const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
354
+ const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
355
+ const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
356
+ const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
357
+ const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
358
+ const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
359
+ const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
360
+ const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
361
+ const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
362
+ const packetLossDenominator = inboundPackets + packetsLost;
363
+ const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
364
+ const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
365
+ const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
366
+ const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
367
+ const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
368
+ const jitterBufferDelayMs = max(inbound.map((stat) => {
369
+ const delay = numericStat(stat, "jitterBufferDelay");
370
+ const emitted = numericStat(stat, "jitterBufferEmittedCount");
371
+ return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
372
+ }).filter((value) => value !== undefined));
373
+ const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
374
+ if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
375
+ pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
376
+ }
377
+ if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
378
+ pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
379
+ }
380
+ if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
381
+ pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
382
+ }
383
+ if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
384
+ pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
385
+ }
386
+ if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
387
+ pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
388
+ }
389
+ return {
390
+ activeCandidatePairs,
391
+ audioLevelAverage: average(audioLevels),
392
+ bytesReceived,
393
+ bytesSent,
394
+ checkedAt: Date.now(),
395
+ endedAudioTracks,
396
+ inboundPackets,
397
+ issues,
398
+ jitterBufferDelayMs,
399
+ jitterMs,
400
+ liveAudioTracks,
401
+ outboundPackets,
402
+ packetLossRatio,
403
+ packetsLost,
404
+ roundTripTimeMs,
405
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
406
+ totalStats: stats.length
407
+ };
408
+ };
409
+ var collectMediaWebRTCStats = async (input) => {
410
+ const report = await input.peerConnection.getStats(input.selector ?? null);
411
+ return [...report.values()].map(normalizeWebRTCStat);
412
+ };
413
+ var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
414
+ const stats = input.stats ?? [];
415
+ const previousStats = input.previousStats ?? [];
416
+ const issues = [];
417
+ const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
418
+ const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
419
+ const streams = audioRtp.map((stat) => {
420
+ const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
421
+ const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
422
+ const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
423
+ const previous = previousByKey.get(statKey(stat));
424
+ const currentPackets = numericStat(stat, packetsKey);
425
+ const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
426
+ const currentBytes = numericStat(stat, bytesKey);
427
+ const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
428
+ const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
429
+ return {
430
+ bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
431
+ currentPackets,
432
+ direction,
433
+ id: statKey(stat),
434
+ packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
435
+ previousPackets,
436
+ timeDeltaMs
437
+ };
438
+ });
439
+ const inbound = streams.filter((stream) => stream.direction === "inbound");
440
+ const outbound = streams.filter((stream) => stream.direction === "outbound");
441
+ const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
442
+ const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
443
+ const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
444
+ if (input.requireInboundAudio && inbound.length === 0) {
445
+ pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
446
+ }
447
+ if (input.requireOutboundAudio && outbound.length === 0) {
448
+ pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
449
+ }
450
+ if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
451
+ pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
452
+ }
453
+ if (stalledInboundStreams > 0) {
454
+ pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
455
+ }
456
+ if (stalledOutboundStreams > 0) {
457
+ pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
458
+ }
459
+ return {
460
+ checkedAt: Date.now(),
461
+ inboundAudioStreams: inbound.length,
462
+ issues,
463
+ maxObservedGapMs,
464
+ outboundAudioStreams: outbound.length,
465
+ stalledInboundStreams,
466
+ stalledOutboundStreams,
467
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
468
+ streams,
469
+ totalStats: stats.length
470
+ };
471
+ };
472
+
473
+ // src/client/browserMedia.ts
474
+ var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
475
+ var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
476
+ var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
477
+ var postBrowserMediaReport = async (payload, options) => {
478
+ const requestFetch = options.fetch ?? globalThis.fetch;
479
+ if (!requestFetch) {
480
+ return;
481
+ }
482
+ await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
483
+ body: JSON.stringify(payload),
484
+ headers: {
485
+ "Content-Type": "application/json"
486
+ },
487
+ keepalive: true,
488
+ method: "POST"
489
+ });
490
+ };
491
+ var createVoiceBrowserMediaReporter = (options) => {
492
+ let interval = null;
493
+ let previousStats = [];
494
+ const reportOnce = async () => {
495
+ const peerConnection = await resolvePeerConnection(options);
496
+ if (!peerConnection) {
497
+ return;
498
+ }
499
+ const stats = await collectMediaWebRTCStats({ peerConnection });
500
+ const report = buildMediaWebRTCStatsReport({
501
+ ...options,
502
+ stats
503
+ });
504
+ const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
505
+ ...options.continuity,
506
+ previousStats,
507
+ stats
508
+ });
509
+ const payload = {
510
+ at: Date.now(),
511
+ continuity,
512
+ report,
513
+ scenarioId: options.getScenarioId?.() ?? null,
514
+ sessionId: options.getSessionId?.() ?? null
515
+ };
516
+ previousStats = stats;
517
+ options.onReport?.(payload);
518
+ await postBrowserMediaReport(payload, options);
519
+ return payload;
520
+ };
521
+ const run = () => {
522
+ reportOnce().catch((error) => {
523
+ options.onError?.(error);
524
+ });
525
+ };
526
+ const stop = () => {
527
+ if (interval) {
528
+ clearInterval(interval);
529
+ interval = null;
530
+ }
531
+ };
532
+ return {
533
+ close: stop,
534
+ reportOnce,
535
+ stop,
536
+ start: () => {
537
+ if (interval) {
538
+ return;
539
+ }
540
+ run();
541
+ interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
542
+ }
543
+ };
544
+ };
545
+
229
546
  // src/client/connection.ts
230
547
  var WS_OPEN = 1;
231
548
  var WS_CLOSED = 3;
232
549
  var WS_NORMAL_CLOSURE = 1000;
233
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
550
+ var WS_UNAUTHORIZED = 4401;
551
+ var WS_FORBIDDEN = 4403;
552
+ var WS_INITIALIZATION_FAILED = 4500;
553
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
234
554
  var DEFAULT_PING_INTERVAL = 30000;
235
- var RECONNECT_DELAY_MS = 500;
555
+ var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
556
+ var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
557
+ var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
558
+ var MAX_PENDING_CONTROL_MESSAGES = 100;
559
+ var RECONNECT_BASE_DELAY_MS = 500;
560
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
561
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
236
562
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
563
+
564
+ class VoiceReconnectRejectedError extends Error {
565
+ constructor(message = "Voice authorization could not be renewed.") {
566
+ super(message);
567
+ this.name = "VoiceReconnectRejectedError";
568
+ }
569
+ }
237
570
  var noop = () => {};
571
+ var noopAsync = () => Promise.resolve();
238
572
  var noopUnsubscribe = () => noop;
239
573
  var NOOP_CONNECTION = {
240
- callControl: noop,
574
+ callControl: noopAsync,
241
575
  close: noop,
576
+ disconnect: noop,
242
577
  endTurn: noop,
578
+ send: noop,
579
+ sendAudio: noop,
580
+ simulateDisconnect: noop,
581
+ subscribe: noopUnsubscribe,
243
582
  getReadyState: () => WS_CLOSED,
244
583
  getScenarioId: () => "",
245
584
  getSessionId: () => "",
246
- send: noop,
247
- sendAudio: noop,
248
- start: () => {},
249
- subscribe: noopUnsubscribe
585
+ start: () => {}
250
586
  };
251
587
  var createSessionId = () => crypto.randomUUID();
252
- var buildWsUrl = (path, sessionId, scenarioId) => {
588
+ var buildWsUrl = (path, sessionId, scenarioId, query) => {
253
589
  const { hostname, port, protocol } = window.location;
254
590
  const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
255
591
  const portSuffix = port ? `:${port}` : "";
@@ -258,6 +594,11 @@ var buildWsUrl = (path, sessionId, scenarioId) => {
258
594
  if (scenarioId) {
259
595
  url.searchParams.set(DEFAULT_SCENARIO_QUERY_PARAM, scenarioId);
260
596
  }
597
+ for (const [key, value] of Object.entries(query)) {
598
+ if (key !== "sessionId" && key !== DEFAULT_SCENARIO_QUERY_PARAM && value) {
599
+ url.searchParams.set(key, value);
600
+ }
601
+ }
261
602
  return url.toString();
262
603
  };
263
604
  var isVoiceServerMessage = (value) => {
@@ -268,11 +609,15 @@ var isVoiceServerMessage = (value) => {
268
609
  case "audio":
269
610
  case "assistant":
270
611
  case "call_lifecycle":
612
+ case "call_control_ack":
271
613
  case "complete":
614
+ case "connection":
272
615
  case "error":
273
616
  case "final":
274
617
  case "partial":
618
+ case "playback_rate":
275
619
  case "pong":
620
+ case "replay":
276
621
  case "session":
277
622
  case "turn":
278
623
  return true;
@@ -296,19 +641,45 @@ var createVoiceConnection = (path, options = {}) => {
296
641
  return NOOP_CONNECTION;
297
642
  }
298
643
  const listeners = new Set;
644
+ const pendingCallControls = new Map;
299
645
  const shouldReconnect = options.reconnect !== false;
300
646
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
647
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
301
648
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
649
+ const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
650
+ let lifecycleGeneration = 0;
651
+ let disposed = false;
652
+ let reconnectPreparationAbort = null;
653
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
302
654
  const state = {
303
655
  isConnected: false,
304
656
  pendingMessages: [],
305
657
  scenarioId: options.scenarioId ?? null,
306
658
  pingInterval: null,
307
659
  reconnectAttempts: 0,
660
+ reconnectResetTimeout: null,
308
661
  reconnectTimeout: null,
309
662
  sessionId: options.sessionId ?? createSessionId(),
310
663
  ws: null
311
664
  };
665
+ const emitConnection = (reconnect) => {
666
+ listeners.forEach((listener) => listener(reconnect));
667
+ };
668
+ const emitTerminalFailure = (message) => {
669
+ listeners.forEach((listener) => listener({
670
+ message,
671
+ recoverable: false,
672
+ type: "error"
673
+ }));
674
+ emitConnection({
675
+ reconnect: {
676
+ attempts: state.reconnectAttempts,
677
+ maxAttempts: maxReconnectAttempts,
678
+ status: "exhausted"
679
+ },
680
+ type: "connection"
681
+ });
682
+ };
312
683
  const clearTimers = () => {
313
684
  if (state.pingInterval) {
314
685
  clearInterval(state.pingInterval);
@@ -318,6 +689,10 @@ var createVoiceConnection = (path, options = {}) => {
318
689
  clearTimeout(state.reconnectTimeout);
319
690
  state.reconnectTimeout = null;
320
691
  }
692
+ if (state.reconnectResetTimeout) {
693
+ clearTimeout(state.reconnectResetTimeout);
694
+ state.reconnectResetTimeout = null;
695
+ }
321
696
  };
322
697
  const flushPendingMessages = () => {
323
698
  if (state.ws?.readyState !== WS_OPEN) {
@@ -330,28 +705,111 @@ var createVoiceConnection = (path, options = {}) => {
330
705
  }
331
706
  }
332
707
  };
708
+ const rejectPendingCallControls = (message) => {
709
+ for (const pending of pendingCallControls.values()) {
710
+ clearTimeout(pending.timeout);
711
+ pending.reject(new Error(message));
712
+ }
713
+ pendingCallControls.clear();
714
+ };
333
715
  const scheduleReconnect = () => {
716
+ if (disposed)
717
+ return;
718
+ const scheduledGeneration = lifecycleGeneration;
334
719
  state.reconnectAttempts += 1;
335
- state.reconnectTimeout = setTimeout(() => {
720
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
721
+ const nextAttemptAt = Date.now() + delayMs;
722
+ emitConnection({
723
+ reconnect: {
724
+ attempts: state.reconnectAttempts,
725
+ lastDisconnectAt: Date.now(),
726
+ maxAttempts: maxReconnectAttempts,
727
+ nextAttemptAt,
728
+ status: "reconnecting"
729
+ },
730
+ type: "connection"
731
+ });
732
+ state.reconnectTimeout = setTimeout(async () => {
733
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
734
+ return;
336
735
  if (state.reconnectAttempts > maxReconnectAttempts) {
736
+ emitTerminalFailure("Voice connection could not be restored.");
737
+ return;
738
+ }
739
+ try {
740
+ if (options.prepareReconnect) {
741
+ const preparationAbort = new AbortController;
742
+ reconnectPreparationAbort = preparationAbort;
743
+ const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
744
+ try {
745
+ await Promise.race([
746
+ options.prepareReconnect({
747
+ attempt: state.reconnectAttempts,
748
+ path,
749
+ scenarioId: state.scenarioId,
750
+ sessionId: state.sessionId,
751
+ signal: preparationAbort.signal
752
+ }),
753
+ new Promise((_, reject) => {
754
+ preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
755
+ })
756
+ ]);
757
+ } finally {
758
+ clearTimeout(timeout);
759
+ if (reconnectPreparationAbort === preparationAbort) {
760
+ reconnectPreparationAbort = null;
761
+ }
762
+ }
763
+ }
764
+ } catch (error) {
765
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
766
+ return;
767
+ if (error instanceof VoiceReconnectRejectedError) {
768
+ emitTerminalFailure(error.message);
769
+ return;
770
+ }
771
+ if (state.reconnectAttempts < maxReconnectAttempts) {
772
+ scheduleReconnect();
773
+ } else {
774
+ emitTerminalFailure("Voice authorization could not be renewed for reconnect.");
775
+ }
337
776
  return;
338
777
  }
778
+ if (disposed || lifecycleGeneration !== scheduledGeneration)
779
+ return;
339
780
  connect();
340
- }, RECONNECT_DELAY_MS);
781
+ }, delayMs);
341
782
  };
342
783
  const connect = () => {
343
- const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
784
+ if (disposed)
785
+ return;
786
+ const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId, options.query ?? {}));
344
787
  ws.binaryType = "arraybuffer";
345
788
  ws.onopen = () => {
789
+ if (state.ws !== ws) {
790
+ ws.close(WS_NORMAL_CLOSURE);
791
+ return;
792
+ }
793
+ const wasReconnecting = state.reconnectAttempts > 0;
346
794
  state.isConnected = true;
347
- state.reconnectAttempts = 0;
348
795
  flushPendingMessages();
349
- listeners.forEach((listener) => listener({
350
- scenarioId: state.scenarioId ?? undefined,
351
- sessionId: state.sessionId,
352
- status: "active",
353
- type: "session"
354
- }));
796
+ if (wasReconnecting) {
797
+ emitConnection({
798
+ reconnect: {
799
+ attempts: state.reconnectAttempts,
800
+ lastResumedAt: Date.now(),
801
+ maxAttempts: maxReconnectAttempts,
802
+ status: "resumed"
803
+ },
804
+ type: "connection"
805
+ });
806
+ state.reconnectResetTimeout = setTimeout(() => {
807
+ if (state.ws === ws && ws.readyState === WS_OPEN) {
808
+ state.reconnectAttempts = 0;
809
+ }
810
+ state.reconnectResetTimeout = null;
811
+ }, reconnectResetAfterMs);
812
+ }
355
813
  state.pingInterval = setInterval(() => {
356
814
  if (ws.readyState === WS_OPEN) {
357
815
  ws.send(JSON.stringify({ type: "ping" }));
@@ -359,6 +817,8 @@ var createVoiceConnection = (path, options = {}) => {
359
817
  }, pingInterval);
360
818
  };
361
819
  ws.onmessage = (event) => {
820
+ if (state.ws !== ws)
821
+ return;
362
822
  const parsed = parseServerMessage(event);
363
823
  if (!parsed) {
364
824
  return;
@@ -367,29 +827,86 @@ var createVoiceConnection = (path, options = {}) => {
367
827
  state.sessionId = parsed.sessionId;
368
828
  state.scenarioId = parsed.scenarioId ?? state.scenarioId;
369
829
  }
830
+ if (parsed.type === "call_control_ack") {
831
+ const pending = pendingCallControls.get(parsed.requestId);
832
+ if (pending) {
833
+ clearTimeout(pending.timeout);
834
+ pendingCallControls.delete(parsed.requestId);
835
+ if (parsed.ok) {
836
+ pending.resolve();
837
+ } else {
838
+ pending.reject(new Error(parsed.message ?? `${parsed.action} was not applied`));
839
+ }
840
+ }
841
+ }
370
842
  listeners.forEach((listener) => listener(parsed));
371
843
  };
372
844
  ws.onclose = (event) => {
845
+ if (state.ws !== ws)
846
+ return;
847
+ state.ws = null;
373
848
  state.isConnected = false;
374
849
  clearTimers();
375
- const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
850
+ rejectPendingCallControls("Voice connection interrupted");
851
+ const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && event.code !== WS_UNAUTHORIZED && event.code !== WS_FORBIDDEN && event.code !== WS_INITIALIZATION_FAILED && state.reconnectAttempts < maxReconnectAttempts;
852
+ const authorizationFailure = event.code === WS_UNAUTHORIZED || event.code === WS_FORBIDDEN;
853
+ const initializationFailure = event.code === WS_INITIALIZATION_FAILED;
854
+ if (authorizationFailure) {
855
+ listeners.forEach((listener) => listener({
856
+ message: event.code === WS_UNAUTHORIZED ? "Voice authorization expired or was rejected." : "Voice authorization was forbidden.",
857
+ recoverable: false,
858
+ type: "error"
859
+ }));
860
+ }
861
+ if (initializationFailure) {
862
+ listeners.forEach((listener) => listener({
863
+ message: "Voice provider initialization failed.",
864
+ recoverable: false,
865
+ type: "error"
866
+ }));
867
+ }
376
868
  if (reconnectable) {
377
869
  scheduleReconnect();
870
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
871
+ if (authorizationFailure) {
872
+ emitConnection({
873
+ reconnect: {
874
+ attempts: state.reconnectAttempts,
875
+ lastDisconnectAt: Date.now(),
876
+ maxAttempts: maxReconnectAttempts,
877
+ status: "exhausted"
878
+ },
879
+ type: "connection"
880
+ });
881
+ } else if (!initializationFailure) {
882
+ emitTerminalFailure("Voice connection could not be restored.");
883
+ }
378
884
  }
379
885
  };
380
886
  state.ws = ws;
381
887
  };
382
888
  const sendSerialized = (value) => {
889
+ if (disposed)
890
+ return;
383
891
  if (state.ws?.readyState === WS_OPEN) {
384
892
  state.ws.send(value);
385
893
  return;
386
894
  }
895
+ if (typeof value !== "string") {
896
+ return;
897
+ }
898
+ if (state.pendingMessages.length >= MAX_PENDING_CONTROL_MESSAGES) {
899
+ state.pendingMessages.shift();
900
+ }
387
901
  state.pendingMessages.push(value);
388
902
  };
389
903
  const send = (message) => {
390
904
  sendSerialized(JSON.stringify(message));
391
905
  };
392
906
  const start = (input = {}) => {
907
+ if (input.sessionId && input.sessionId !== state.sessionId) {
908
+ throw new Error("Voice session switching requires a new connection instance.");
909
+ }
393
910
  if (input.sessionId) {
394
911
  state.sessionId = input.sessionId;
395
912
  }
@@ -397,9 +914,9 @@ var createVoiceConnection = (path, options = {}) => {
397
914
  state.scenarioId = input.scenarioId;
398
915
  }
399
916
  send({
400
- type: "start",
917
+ scenarioId: state.scenarioId ?? undefined,
401
918
  sessionId: state.sessionId,
402
- scenarioId: state.scenarioId ?? undefined
919
+ type: "start"
403
920
  });
404
921
  };
405
922
  const sendAudio = (audio) => {
@@ -409,20 +926,63 @@ var createVoiceConnection = (path, options = {}) => {
409
926
  send({ type: "end_turn" });
410
927
  };
411
928
  const callControl = (message) => {
412
- send({
929
+ if (state.ws?.readyState !== WS_OPEN) {
930
+ return Promise.reject(new Error(`${message.action} is unavailable while disconnected`));
931
+ }
932
+ const requestId = crypto.randomUUID();
933
+ const { promise, reject, resolve } = Promise.withResolvers();
934
+ const timeout = setTimeout(() => {
935
+ pendingCallControls.delete(requestId);
936
+ reject(new Error(`${message.action} acknowledgement timed out`));
937
+ }, DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS);
938
+ pendingCallControls.set(requestId, { reject, resolve, timeout });
939
+ state.ws.send(JSON.stringify({
413
940
  ...message,
941
+ requestId,
414
942
  type: "call_control"
415
- });
943
+ }));
944
+ return promise;
945
+ };
946
+ const close = (reason = "client-close") => {
947
+ disposed = true;
948
+ reconnectPreparationAbort?.abort();
949
+ reconnectPreparationAbort = null;
950
+ lifecycleGeneration += 1;
951
+ clearTimers();
952
+ rejectPendingCallControls("Voice connection closed");
953
+ state.pendingMessages.length = 0;
954
+ if (state.ws) {
955
+ const ws = state.ws;
956
+ state.ws = null;
957
+ if (ws.readyState === WS_OPEN) {
958
+ ws.send(JSON.stringify({ reason, type: "close" }));
959
+ }
960
+ ws.close(WS_NORMAL_CLOSURE);
961
+ }
962
+ state.isConnected = false;
963
+ listeners.clear();
416
964
  };
417
- const close = () => {
965
+ const disconnect = () => {
966
+ disposed = true;
967
+ reconnectPreparationAbort?.abort();
968
+ reconnectPreparationAbort = null;
969
+ lifecycleGeneration += 1;
418
970
  clearTimers();
971
+ rejectPendingCallControls("Voice connection disconnected");
972
+ state.pendingMessages.length = 0;
419
973
  if (state.ws) {
420
- state.ws.close(WS_NORMAL_CLOSURE);
974
+ const ws = state.ws;
421
975
  state.ws = null;
976
+ ws.close(WS_NORMAL_CLOSURE);
422
977
  }
423
978
  state.isConnected = false;
424
979
  listeners.clear();
425
980
  };
981
+ const simulateDisconnect = () => {
982
+ if (state.ws?.readyState === WS_OPEN) {
983
+ state.ws.close(4000, "absolutejs-voice-reconnect-proof");
984
+ }
985
+ };
426
986
  const subscribe = (callback) => {
427
987
  listeners.add(callback);
428
988
  return () => {
@@ -433,32 +993,66 @@ var createVoiceConnection = (path, options = {}) => {
433
993
  return {
434
994
  callControl,
435
995
  close,
996
+ disconnect,
436
997
  endTurn,
437
- getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
438
- getScenarioId: () => state.scenarioId ?? "",
439
- getSessionId: () => state.sessionId,
440
998
  send,
441
999
  sendAudio,
1000
+ simulateDisconnect,
442
1001
  start,
443
- subscribe
1002
+ subscribe,
1003
+ getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
1004
+ getScenarioId: () => state.scenarioId ?? "",
1005
+ getSessionId: () => state.sessionId
444
1006
  };
445
1007
  };
446
1008
 
447
1009
  // src/client/store.ts
1010
+ var createInitialReconnectState = () => ({
1011
+ attempts: 0,
1012
+ maxAttempts: 0,
1013
+ status: "idle"
1014
+ });
1015
+ var appendSegmentText = (accumulated, next) => {
1016
+ const nextText = next.trim().replace(/\s+/g, " ");
1017
+ if (!nextText)
1018
+ return accumulated;
1019
+ if (!accumulated)
1020
+ return nextText;
1021
+ if (accumulated === nextText || accumulated.endsWith(nextText)) {
1022
+ return accumulated;
1023
+ }
1024
+ if (nextText.includes(accumulated))
1025
+ return nextText;
1026
+ return `${accumulated} ${nextText}`;
1027
+ };
1028
+ var joinPartial = (finalized, interim) => {
1029
+ const interimText = interim.trim().replace(/\s+/g, " ");
1030
+ if (!finalized)
1031
+ return interimText;
1032
+ if (!interimText || finalized.endsWith(interimText))
1033
+ return finalized;
1034
+ return `${finalized} ${interimText}`;
1035
+ };
448
1036
  var createInitialState = () => ({
449
1037
  assistantAudio: [],
1038
+ assistantStreamingText: "",
450
1039
  assistantTexts: [],
451
1040
  call: null,
452
1041
  error: null,
453
1042
  isConnected: false,
454
- scenarioId: null,
1043
+ paused: false,
455
1044
  partial: "",
1045
+ playbackRate: null,
1046
+ reconnect: createInitialReconnectState(),
1047
+ scenarioId: null,
456
1048
  sessionId: null,
1049
+ sessionMetadata: null,
457
1050
  status: "idle",
458
1051
  turns: []
459
1052
  });
460
1053
  var createVoiceStreamStore = () => {
461
1054
  let state = createInitialState();
1055
+ let turnFinalText = "";
462
1056
  const subscribers = new Set;
463
1057
  const notify = () => {
464
1058
  subscribers.forEach((subscriber) => subscriber());
@@ -482,9 +1076,16 @@ var createVoiceStreamStore = () => {
482
1076
  case "assistant":
483
1077
  state = {
484
1078
  ...state,
1079
+ assistantStreamingText: "",
485
1080
  assistantTexts: [...state.assistantTexts, action.text]
486
1081
  };
487
1082
  break;
1083
+ case "assistant_delta":
1084
+ state = {
1085
+ ...state,
1086
+ assistantStreamingText: `${state.assistantStreamingText}${action.delta}`
1087
+ };
1088
+ break;
488
1089
  case "complete":
489
1090
  state = {
490
1091
  ...state,
@@ -509,7 +1110,19 @@ var createVoiceStreamStore = () => {
509
1110
  case "connected":
510
1111
  state = {
511
1112
  ...state,
512
- isConnected: true
1113
+ isConnected: true,
1114
+ reconnect: state.reconnect.status === "reconnecting" ? {
1115
+ ...state.reconnect,
1116
+ lastResumedAt: Date.now(),
1117
+ nextAttemptAt: undefined,
1118
+ status: "resumed"
1119
+ } : state.reconnect
1120
+ };
1121
+ break;
1122
+ case "connection":
1123
+ state = {
1124
+ ...state,
1125
+ reconnect: action.reconnect
513
1126
  };
514
1127
  break;
515
1128
  case "disconnected":
@@ -521,33 +1134,67 @@ var createVoiceStreamStore = () => {
521
1134
  case "error":
522
1135
  state = {
523
1136
  ...state,
524
- error: action.message
1137
+ error: action.message,
1138
+ ...action.recoverable === false ? { isConnected: false, status: "failed" } : {}
525
1139
  };
526
1140
  break;
527
1141
  case "final":
1142
+ turnFinalText = appendSegmentText(turnFinalText, action.transcript.text);
528
1143
  state = {
529
1144
  ...state,
530
- partial: action.transcript.text,
531
- turns: state.turns.map((turn) => turn)
1145
+ partial: turnFinalText
532
1146
  };
533
1147
  break;
534
1148
  case "partial":
535
1149
  state = {
536
1150
  ...state,
537
- partial: action.transcript.text
1151
+ partial: joinPartial(turnFinalText, action.transcript.text)
1152
+ };
1153
+ break;
1154
+ case "playback_rate":
1155
+ state = {
1156
+ ...state,
1157
+ playbackRate: Math.max(0.5, Math.min(2, action.rate))
1158
+ };
1159
+ break;
1160
+ case "replay":
1161
+ turnFinalText = action.partial;
1162
+ state = {
1163
+ ...state,
1164
+ assistantStreamingText: "",
1165
+ assistantTexts: [...action.assistantTexts],
1166
+ call: action.call ?? null,
1167
+ error: null,
1168
+ isConnected: action.status === "active",
1169
+ partial: action.partial,
1170
+ reconnect: state.reconnect.status === "reconnecting" ? {
1171
+ ...state.reconnect,
1172
+ lastResumedAt: Date.now(),
1173
+ nextAttemptAt: undefined,
1174
+ status: "resumed"
1175
+ } : state.reconnect,
1176
+ scenarioId: action.scenarioId ?? state.scenarioId,
1177
+ sessionId: action.sessionId,
1178
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
1179
+ status: action.status,
1180
+ turns: [...action.turns]
538
1181
  };
539
1182
  break;
540
1183
  case "session":
541
1184
  state = {
542
1185
  ...state,
543
1186
  error: null,
1187
+ paused: action.paused ?? false,
1188
+ pauseExpiresAt: action.pauseExpiresAt,
544
1189
  scenarioId: action.scenarioId ?? state.scenarioId,
545
1190
  isConnected: action.status === "active",
546
1191
  sessionId: action.sessionId,
1192
+ sessionMetadata: action.sessionMetadata ?? state.sessionMetadata,
547
1193
  status: action.status
548
1194
  };
549
1195
  break;
550
1196
  case "turn":
1197
+ turnFinalText = "";
551
1198
  state = {
552
1199
  ...state,
553
1200
  partial: "",
@@ -574,30 +1221,86 @@ var createVoiceStreamStore = () => {
574
1221
  var createVoiceStream = (path, options = {}) => {
575
1222
  const connection = createVoiceConnection(path, options);
576
1223
  const store = createVoiceStreamStore();
1224
+ const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
1225
+ ...options.browserMedia,
1226
+ getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
1227
+ getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
1228
+ }) : null;
577
1229
  const subscribers = new Set;
1230
+ let disposed = false;
578
1231
  const start = (input) => Promise.resolve().then(() => {
1232
+ if (disposed)
1233
+ return;
579
1234
  if (!input?.sessionId && !input?.scenarioId) {
580
1235
  return;
581
1236
  }
582
1237
  connection.start(input);
1238
+ browserMediaReporter?.start();
583
1239
  });
584
1240
  const notify = () => {
585
1241
  subscribers.forEach((subscriber) => subscriber());
586
1242
  };
1243
+ const reportReconnect = () => {
1244
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
1245
+ return;
1246
+ }
1247
+ const snapshot = store.getSnapshot();
1248
+ const body = JSON.stringify({
1249
+ at: Date.now(),
1250
+ reconnect: snapshot.reconnect,
1251
+ scenarioId: snapshot.scenarioId,
1252
+ sessionId: connection.getSessionId(),
1253
+ turnIds: snapshot.turns.map((turn) => turn.id)
1254
+ });
1255
+ fetch(options.reconnectReportPath, {
1256
+ body,
1257
+ headers: {
1258
+ "Content-Type": "application/json"
1259
+ },
1260
+ keepalive: true,
1261
+ method: "POST"
1262
+ }).catch(() => {});
1263
+ };
587
1264
  const unsubscribeConnection = connection.subscribe((message) => {
588
1265
  const action = serverMessageToAction(message);
589
1266
  if (action) {
590
1267
  store.dispatch(action);
1268
+ if (message.type === "connection") {
1269
+ reportReconnect();
1270
+ }
591
1271
  notify();
592
1272
  }
593
1273
  });
594
1274
  return {
1275
+ start,
1276
+ get assistantAudio() {
1277
+ return store.getSnapshot().assistantAudio;
1278
+ },
1279
+ get assistantTexts() {
1280
+ return store.getSnapshot().assistantTexts;
1281
+ },
1282
+ get assistantStreamingText() {
1283
+ return store.getSnapshot().assistantStreamingText;
1284
+ },
1285
+ get call() {
1286
+ return store.getSnapshot().call;
1287
+ },
595
1288
  callControl(message) {
596
- connection.callControl(message);
1289
+ return connection.callControl(message);
1290
+ },
1291
+ close(reason) {
1292
+ disposed = true;
1293
+ unsubscribeConnection();
1294
+ browserMediaReporter?.close();
1295
+ connection.close(reason);
1296
+ store.dispatch({ type: "disconnected" });
1297
+ notify();
597
1298
  },
598
- close() {
1299
+ disconnect() {
1300
+ disposed = true;
599
1301
  unsubscribeConnection();
600
- connection.close();
1302
+ browserMediaReporter?.close();
1303
+ connection.disconnect();
601
1304
  store.dispatch({ type: "disconnected" });
602
1305
  notify();
603
1306
  },
@@ -616,44 +1319,52 @@ var createVoiceStream = (path, options = {}) => {
616
1319
  get isConnected() {
617
1320
  return store.getSnapshot().isConnected;
618
1321
  },
619
- get scenarioId() {
620
- return store.getSnapshot().scenarioId;
621
- },
622
- start,
623
1322
  get partial() {
624
1323
  return store.getSnapshot().partial;
625
1324
  },
626
- get sessionId() {
627
- return connection.getSessionId();
628
- },
629
- get status() {
630
- return store.getSnapshot().status;
1325
+ get playbackRate() {
1326
+ return store.getSnapshot().playbackRate;
631
1327
  },
632
- get turns() {
633
- return store.getSnapshot().turns;
1328
+ get paused() {
1329
+ return store.getSnapshot().paused;
634
1330
  },
635
- get assistantTexts() {
636
- return store.getSnapshot().assistantTexts;
1331
+ get pauseExpiresAt() {
1332
+ return store.getSnapshot().pauseExpiresAt;
637
1333
  },
638
- get assistantAudio() {
639
- return store.getSnapshot().assistantAudio;
1334
+ get reconnect() {
1335
+ return store.getSnapshot().reconnect;
640
1336
  },
641
- get call() {
642
- return store.getSnapshot().call;
1337
+ get scenarioId() {
1338
+ return store.getSnapshot().scenarioId;
643
1339
  },
644
1340
  sendAudio(audio) {
645
1341
  connection.sendAudio(audio);
646
1342
  },
1343
+ get sessionId() {
1344
+ return connection.getSessionId();
1345
+ },
1346
+ get sessionMetadata() {
1347
+ return store.getSnapshot().sessionMetadata;
1348
+ },
1349
+ simulateDisconnect() {
1350
+ connection.simulateDisconnect();
1351
+ },
1352
+ get status() {
1353
+ return store.getSnapshot().status;
1354
+ },
647
1355
  subscribe(subscriber) {
648
1356
  subscribers.add(subscriber);
649
1357
  return () => {
650
1358
  subscribers.delete(subscriber);
651
1359
  };
1360
+ },
1361
+ get turns() {
1362
+ return store.getSnapshot().turns;
652
1363
  }
653
1364
  };
654
1365
  };
655
1366
 
656
- // src/audioConditioning.ts
1367
+ // src/core/audioConditioning.ts
657
1368
  var DEFAULT_TARGET_LEVEL = 0.08;
658
1369
  var DEFAULT_MAX_GAIN = 3;
659
1370
  var DEFAULT_NOISE_GATE_THRESHOLD = 0.006;
@@ -671,34 +1382,37 @@ var resolveAudioConditioningConfig = (config) => {
671
1382
  };
672
1383
  };
673
1384
 
674
- // src/turnProfiles.ts
1385
+ // src/core/turnProfiles.ts
675
1386
  var TURN_PROFILE_DEFAULTS = {
676
1387
  balanced: {
677
1388
  qualityProfile: "general",
1389
+ minSilenceMs: 400,
678
1390
  silenceMs: 1400,
679
1391
  speechThreshold: 0.012,
680
1392
  transcriptStabilityMs: 1000
681
1393
  },
682
1394
  fast: {
683
1395
  qualityProfile: "general",
1396
+ minSilenceMs: 300,
684
1397
  silenceMs: 700,
685
1398
  speechThreshold: 0.015,
686
1399
  transcriptStabilityMs: 450
687
1400
  },
688
1401
  "long-form": {
689
1402
  qualityProfile: "general",
1403
+ minSilenceMs: 600,
690
1404
  silenceMs: 2200,
691
1405
  speechThreshold: 0.01,
692
1406
  transcriptStabilityMs: 1500
693
1407
  }
694
1408
  };
695
1409
  var QUALITY_PROFILE_DEFAULTS = {
696
- general: {},
697
1410
  "accent-heavy": {
698
1411
  silenceMs: 1200,
699
1412
  speechThreshold: 0.01,
700
1413
  transcriptStabilityMs: 1200
701
1414
  },
1415
+ general: {},
702
1416
  "noisy-room": {
703
1417
  silenceMs: 2000,
704
1418
  speechThreshold: 0.02,
@@ -717,16 +1431,18 @@ var resolveTurnDetectionConfig = (config) => {
717
1431
  const qualityProfile = config?.qualityProfile ?? DEFAULT_QUALITY_PROFILE;
718
1432
  const preset = TURN_PROFILE_DEFAULTS[profile];
719
1433
  const quality = QUALITY_PROFILE_DEFAULTS[qualityProfile];
1434
+ const silenceMs = config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs;
720
1435
  return {
721
1436
  profile,
722
1437
  qualityProfile,
723
- silenceMs: config?.silenceMs ?? quality.silenceMs ?? preset.silenceMs,
1438
+ minSilenceMs: Math.min(silenceMs, config?.minSilenceMs ?? quality.minSilenceMs ?? preset.minSilenceMs),
1439
+ silenceMs,
724
1440
  speechThreshold: config?.speechThreshold ?? quality.speechThreshold ?? preset.speechThreshold,
725
1441
  transcriptStabilityMs: config?.transcriptStabilityMs ?? quality.transcriptStabilityMs ?? preset.transcriptStabilityMs
726
1442
  };
727
1443
  };
728
1444
 
729
- // src/presets.ts
1445
+ // src/core/presets.ts
730
1446
  var PRESET_INPUTS = {
731
1447
  chat: {
732
1448
  audioConditioning: {
@@ -747,8 +1463,8 @@ var PRESET_INPUTS = {
747
1463
  },
748
1464
  sttLifecycle: "continuous",
749
1465
  turnDetection: {
750
- qualityProfile: "short-command",
751
- profile: "balanced"
1466
+ profile: "balanced",
1467
+ qualityProfile: "short-command"
752
1468
  }
753
1469
  },
754
1470
  default: {
@@ -763,8 +1479,8 @@ var PRESET_INPUTS = {
763
1479
  },
764
1480
  sttLifecycle: "continuous",
765
1481
  turnDetection: {
766
- qualityProfile: "general",
767
- profile: "fast"
1482
+ profile: "fast",
1483
+ qualityProfile: "general"
768
1484
  }
769
1485
  },
770
1486
  dictation: {
@@ -786,8 +1502,8 @@ var PRESET_INPUTS = {
786
1502
  },
787
1503
  sttLifecycle: "continuous",
788
1504
  turnDetection: {
789
- qualityProfile: "accent-heavy",
790
- profile: "long-form"
1505
+ profile: "long-form",
1506
+ qualityProfile: "accent-heavy"
791
1507
  }
792
1508
  },
793
1509
  "guided-intake": {
@@ -809,8 +1525,8 @@ var PRESET_INPUTS = {
809
1525
  },
810
1526
  sttLifecycle: "turn-scoped",
811
1527
  turnDetection: {
812
- qualityProfile: "accent-heavy",
813
- profile: "long-form"
1528
+ profile: "long-form",
1529
+ qualityProfile: "accent-heavy"
814
1530
  }
815
1531
  },
816
1532
  "noisy-room": {
@@ -832,8 +1548,8 @@ var PRESET_INPUTS = {
832
1548
  },
833
1549
  sttLifecycle: "continuous",
834
1550
  turnDetection: {
835
- qualityProfile: "noisy-room",
836
1551
  profile: "long-form",
1552
+ qualityProfile: "noisy-room",
837
1553
  silenceMs: 2100,
838
1554
  speechThreshold: 0.02,
839
1555
  transcriptStabilityMs: 1650
@@ -858,8 +1574,8 @@ var PRESET_INPUTS = {
858
1574
  },
859
1575
  sttLifecycle: "continuous",
860
1576
  turnDetection: {
861
- qualityProfile: "noisy-room",
862
1577
  profile: "long-form",
1578
+ qualityProfile: "noisy-room",
863
1579
  silenceMs: 660,
864
1580
  speechThreshold: 0.012,
865
1581
  transcriptStabilityMs: 300
@@ -884,8 +1600,8 @@ var PRESET_INPUTS = {
884
1600
  },
885
1601
  sttLifecycle: "continuous",
886
1602
  turnDetection: {
887
- qualityProfile: "noisy-room",
888
1603
  profile: "long-form",
1604
+ qualityProfile: "noisy-room",
889
1605
  silenceMs: 620,
890
1606
  speechThreshold: 0.012,
891
1607
  transcriptStabilityMs: 280
@@ -910,8 +1626,8 @@ var PRESET_INPUTS = {
910
1626
  },
911
1627
  sttLifecycle: "continuous",
912
1628
  turnDetection: {
913
- qualityProfile: "noisy-room",
914
- profile: "long-form"
1629
+ profile: "long-form",
1630
+ qualityProfile: "noisy-room"
915
1631
  }
916
1632
  }
917
1633
  };
@@ -935,14 +1651,20 @@ var resolveVoiceRuntimePreset = (name = "default") => {
935
1651
  // src/client/controller.ts
936
1652
  var createInitialState2 = (stream) => ({
937
1653
  assistantAudio: [...stream.assistantAudio],
1654
+ assistantStreamingText: stream.assistantStreamingText,
938
1655
  assistantTexts: [...stream.assistantTexts],
939
1656
  call: stream.call,
940
1657
  error: stream.error,
941
1658
  isConnected: stream.isConnected,
942
1659
  isRecording: false,
1660
+ paused: stream.paused,
1661
+ pauseExpiresAt: stream.pauseExpiresAt,
943
1662
  partial: stream.partial,
1663
+ playbackRate: stream.playbackRate,
1664
+ reconnect: stream.reconnect,
944
1665
  recordingError: null,
945
1666
  sessionId: stream.sessionId,
1667
+ sessionMetadata: stream.sessionMetadata,
946
1668
  scenarioId: stream.scenarioId,
947
1669
  status: stream.status,
948
1670
  turns: [...stream.turns]
@@ -954,6 +1676,9 @@ var createVoiceController = (path, options = {}) => {
954
1676
  ...options.connection
955
1677
  });
956
1678
  let capture = null;
1679
+ let captureGeneration = 0;
1680
+ let disposed = false;
1681
+ let startRecordingPromise = null;
957
1682
  let state = createInitialState2(stream);
958
1683
  const subscribers = new Set;
959
1684
  const notify = () => {
@@ -965,12 +1690,18 @@ var createVoiceController = (path, options = {}) => {
965
1690
  state = {
966
1691
  ...state,
967
1692
  assistantAudio: [...stream.assistantAudio],
1693
+ assistantStreamingText: stream.assistantStreamingText,
968
1694
  assistantTexts: [...stream.assistantTexts],
969
1695
  call: stream.call,
970
1696
  error: stream.error,
971
1697
  isConnected: stream.isConnected,
1698
+ paused: stream.paused,
1699
+ pauseExpiresAt: stream.pauseExpiresAt,
972
1700
  partial: stream.partial,
1701
+ playbackRate: stream.playbackRate,
1702
+ reconnect: stream.reconnect,
973
1703
  sessionId: stream.sessionId,
1704
+ sessionMetadata: stream.sessionMetadata,
974
1705
  scenarioId: stream.scenarioId,
975
1706
  status: stream.status,
976
1707
  turns: [...stream.turns]
@@ -994,12 +1725,20 @@ var createVoiceController = (path, options = {}) => {
994
1725
  capture = createMicrophoneCapture({
995
1726
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
1727
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
998
- sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
1728
+ onAudio: (audio) => {
1729
+ if (options.capture?.onAudio) {
1730
+ options.capture.onAudio(audio, stream.sendAudio);
1731
+ return;
1732
+ }
1733
+ stream.sendAudio(audio);
1734
+ },
1735
+ sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz,
1736
+ ...options.capture?.stream ? { stream: options.capture.stream } : {}
999
1737
  });
1000
1738
  return capture;
1001
1739
  };
1002
1740
  const stopRecording = () => {
1741
+ captureGeneration += 1;
1003
1742
  capture?.stop();
1004
1743
  capture = null;
1005
1744
  state = {
@@ -1009,43 +1748,88 @@ var createVoiceController = (path, options = {}) => {
1009
1748
  notify();
1010
1749
  };
1011
1750
  const startRecording = async () => {
1751
+ if (disposed) {
1752
+ throw new Error("Voice controller is closed.");
1753
+ }
1012
1754
  if (state.isRecording) {
1013
1755
  return;
1014
1756
  }
1015
- try {
1016
- state = {
1017
- ...state,
1018
- recordingError: null
1019
- };
1020
- notify();
1021
- await ensureCapture().start();
1022
- state = {
1023
- ...state,
1024
- isRecording: true
1025
- };
1026
- notify();
1027
- } catch (error) {
1028
- capture = null;
1029
- state = {
1030
- ...state,
1031
- isRecording: false,
1032
- recordingError: error instanceof Error ? error.message : String(error)
1033
- };
1034
- notify();
1035
- throw error;
1036
- }
1037
- };
1038
- const close = () => {
1039
- unsubscribeStream();
1040
- stopRecording();
1041
- stream.close();
1042
- };
1043
- return {
1044
- bindHTMX(bindingOptions) {
1045
- return bindVoiceHTMX(stream, bindingOptions);
1046
- },
1047
- callControl: (message) => stream.callControl(message),
1048
- close,
1757
+ if (startRecordingPromise)
1758
+ return startRecordingPromise;
1759
+ const generation = ++captureGeneration;
1760
+ const activeCapture = ensureCapture();
1761
+ const operation = (async () => {
1762
+ try {
1763
+ state = {
1764
+ ...state,
1765
+ recordingError: null
1766
+ };
1767
+ notify();
1768
+ await activeCapture.start();
1769
+ if (disposed || generation !== captureGeneration || capture !== activeCapture) {
1770
+ activeCapture.stop();
1771
+ return;
1772
+ }
1773
+ state = {
1774
+ ...state,
1775
+ isRecording: true
1776
+ };
1777
+ notify();
1778
+ } catch (error) {
1779
+ activeCapture.stop();
1780
+ if (generation !== captureGeneration || capture !== activeCapture) {
1781
+ throw error;
1782
+ }
1783
+ capture = null;
1784
+ state = {
1785
+ ...state,
1786
+ isRecording: false,
1787
+ recordingError: error instanceof Error ? error.message : String(error)
1788
+ };
1789
+ notify();
1790
+ throw error;
1791
+ }
1792
+ })();
1793
+ const pending = operation.finally(() => {
1794
+ if (startRecordingPromise === pending)
1795
+ startRecordingPromise = null;
1796
+ });
1797
+ startRecordingPromise = pending;
1798
+ return startRecordingPromise;
1799
+ };
1800
+ const close = (reason) => {
1801
+ disposed = true;
1802
+ unsubscribeStream();
1803
+ stopRecording();
1804
+ stream.close(reason);
1805
+ };
1806
+ const disconnect = () => {
1807
+ disposed = true;
1808
+ unsubscribeStream();
1809
+ stopRecording();
1810
+ stream.disconnect();
1811
+ };
1812
+ return {
1813
+ close,
1814
+ disconnect,
1815
+ startRecording,
1816
+ stopRecording,
1817
+ get assistantAudio() {
1818
+ return state.assistantAudio;
1819
+ },
1820
+ get assistantTexts() {
1821
+ return state.assistantTexts;
1822
+ },
1823
+ get assistantStreamingText() {
1824
+ return state.assistantStreamingText;
1825
+ },
1826
+ bindHTMX(bindingOptions) {
1827
+ return bindVoiceHTMX(stream, bindingOptions);
1828
+ },
1829
+ get call() {
1830
+ return state.call;
1831
+ },
1832
+ callControl: (message) => stream.callControl(message),
1049
1833
  endTurn: () => stream.endTurn(),
1050
1834
  get error() {
1051
1835
  return state.error;
@@ -1061,21 +1845,35 @@ var createVoiceController = (path, options = {}) => {
1061
1845
  get partial() {
1062
1846
  return state.partial;
1063
1847
  },
1848
+ get playbackRate() {
1849
+ return state.playbackRate;
1850
+ },
1851
+ get paused() {
1852
+ return state.paused;
1853
+ },
1854
+ get pauseExpiresAt() {
1855
+ return state.pauseExpiresAt;
1856
+ },
1857
+ get reconnect() {
1858
+ return state.reconnect;
1859
+ },
1064
1860
  get recordingError() {
1065
1861
  return state.recordingError;
1066
1862
  },
1863
+ get scenarioId() {
1864
+ return state.scenarioId;
1865
+ },
1067
1866
  sendAudio: (audio) => stream.sendAudio(audio),
1068
1867
  get sessionId() {
1069
1868
  return state.sessionId;
1070
1869
  },
1071
- get scenarioId() {
1072
- return state.scenarioId;
1870
+ get sessionMetadata() {
1871
+ return state.sessionMetadata;
1073
1872
  },
1074
- startRecording,
1873
+ simulateDisconnect: () => stream.simulateDisconnect(),
1075
1874
  get status() {
1076
1875
  return state.status;
1077
1876
  },
1078
- stopRecording,
1079
1877
  subscribe: (subscriber) => {
1080
1878
  subscribers.add(subscriber);
1081
1879
  return () => {
@@ -1091,15 +1889,778 @@ var createVoiceController = (path, options = {}) => {
1091
1889
  },
1092
1890
  get turns() {
1093
1891
  return state.turns;
1892
+ }
1893
+ };
1894
+ };
1895
+
1896
+ // src/client/timeStretch.ts
1897
+ var HOP_MS = 10;
1898
+ var SEEK_MS = 5;
1899
+ var ENERGY_EPSILON = 0.000001;
1900
+ var HALF = 0.5;
1901
+ var MS_PER_SECOND = 1000;
1902
+ var makeHann = (length) => {
1903
+ const weights = new Float32Array(length);
1904
+ for (let index = 0;index < length; index += 1) {
1905
+ weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
1906
+ }
1907
+ return weights;
1908
+ };
1909
+ var correlationScore = (base, start, ref, length) => {
1910
+ let dot = 0;
1911
+ let energy = 0;
1912
+ for (let index = 0;index < length; index += 1) {
1913
+ const sample = base[start + index] ?? 0;
1914
+ dot += sample * (ref[index] ?? 0);
1915
+ energy += sample * sample;
1916
+ }
1917
+ return dot / Math.sqrt(energy + ENERGY_EPSILON);
1918
+ };
1919
+ var overlapAddGrain = (src, off, tail, weights, hop) => {
1920
+ const out = new Float32Array(hop);
1921
+ const nextTail = new Float32Array(hop);
1922
+ for (let index = 0;index < hop; index += 1) {
1923
+ out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
1924
+ nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
1925
+ }
1926
+ return { nextTail, out };
1927
+ };
1928
+ var createTimeStretcher = () => {
1929
+ let sampleRate = 0;
1930
+ let channelCount = 0;
1931
+ let hop = 0;
1932
+ let frameLen = 0;
1933
+ let seek = 0;
1934
+ let weights = new Float32Array(0);
1935
+ let buffers = [];
1936
+ let inputStart = 0;
1937
+ let analysisPos = 0;
1938
+ let olaTail = [];
1939
+ let naturalRef = null;
1940
+ const init = (rate, channels) => {
1941
+ sampleRate = rate;
1942
+ channelCount = channels;
1943
+ hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
1944
+ frameLen = hop * 2;
1945
+ seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
1946
+ weights = makeHann(frameLen);
1947
+ buffers = Array.from({ length: channels }, () => new Float32Array(0));
1948
+ olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
1949
+ inputStart = 0;
1950
+ analysisPos = seek;
1951
+ naturalRef = null;
1952
+ };
1953
+ const reset = () => {
1954
+ buffers = buffers.map(() => new Float32Array(0));
1955
+ olaTail = olaTail.map(() => new Float32Array(hop));
1956
+ inputStart = 0;
1957
+ analysisPos = seek;
1958
+ naturalRef = null;
1959
+ };
1960
+ const append = (input) => {
1961
+ for (let channel = 0;channel < channelCount; channel += 1) {
1962
+ const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
1963
+ const existing = buffers[channel] ?? new Float32Array(0);
1964
+ const merged = new Float32Array(existing.length + incoming.length);
1965
+ merged.set(existing, 0);
1966
+ merged.set(incoming, existing.length);
1967
+ buffers[channel] = merged;
1968
+ }
1969
+ };
1970
+ const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
1971
+ const compact = () => {
1972
+ const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
1973
+ if (keepFrom <= inputStart)
1974
+ return;
1975
+ const drop = keepFrom - inputStart;
1976
+ for (let channel = 0;channel < channelCount; channel += 1) {
1977
+ buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
1978
+ }
1979
+ inputStart = keepFrom;
1980
+ };
1981
+ const bestOffset = (center) => {
1982
+ if (!naturalRef)
1983
+ return 0;
1984
+ const [base] = buffers;
1985
+ if (!base)
1986
+ return 0;
1987
+ let bestDelta = 0;
1988
+ let bestScore = -Infinity;
1989
+ for (let delta = -seek;delta <= seek; delta += 1) {
1990
+ const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
1991
+ if (score <= bestScore)
1992
+ continue;
1993
+ bestScore = score;
1994
+ bestDelta = delta;
1995
+ }
1996
+ return bestDelta;
1997
+ };
1998
+ const process = (input, speed, rate) => {
1999
+ const channels = Math.max(1, input.length);
2000
+ if (sampleRate !== rate || channelCount !== channels)
2001
+ init(rate, channels);
2002
+ append(input);
2003
+ const analysisHop = hop * speed;
2004
+ const segments = Array.from({ length: channelCount }, () => []);
2005
+ const emitGrain = (pos) => {
2006
+ const off = pos - inputStart;
2007
+ for (let channel = 0;channel < channelCount; channel += 1) {
2008
+ const src = buffers[channel];
2009
+ const tail = olaTail[channel];
2010
+ if (!src || !tail)
2011
+ continue;
2012
+ const grain = overlapAddGrain(src, off, tail, weights, hop);
2013
+ olaTail[channel] = grain.nextTail;
2014
+ segments[channel]?.push(grain.out);
2015
+ }
2016
+ };
2017
+ const captureRef = (pos) => {
2018
+ const ref = new Float32Array(frameLen);
2019
+ const refOff = pos + hop - inputStart;
2020
+ const [base] = buffers;
2021
+ if (base)
2022
+ ref.set(base.subarray(refOff, refOff + frameLen));
2023
+ naturalRef = ref;
2024
+ };
2025
+ const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
2026
+ while (canEmit()) {
2027
+ const center = Math.round(analysisPos);
2028
+ const pos = center + bestOffset(center);
2029
+ emitGrain(pos);
2030
+ captureRef(pos);
2031
+ analysisPos += analysisHop;
2032
+ }
2033
+ compact();
2034
+ return segments.map((channelSegments) => {
2035
+ const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
2036
+ const merged = new Float32Array(total);
2037
+ let offset = 0;
2038
+ for (const seg of channelSegments) {
2039
+ merged.set(seg, offset);
2040
+ offset += seg.length;
2041
+ }
2042
+ return merged;
2043
+ });
2044
+ };
2045
+ return { process, reset };
2046
+ };
2047
+
2048
+ // src/client/audioPlayer.ts
2049
+ var activeAudioPlayers = new Set;
2050
+ var DEFAULT_LOOKAHEAD_MS = 15;
2051
+ var DEFAULT_VOLUME = 1;
2052
+ var DEFAULT_PLAYBACK_RATE = 1;
2053
+ var MIN_PLAYBACK_RATE = 0.5;
2054
+ var MAX_PLAYBACK_RATE = 2;
2055
+ var STRETCH_BYPASS_EPSILON = 0.01;
2056
+ var ANALYSER_FFT_SIZE = 256;
2057
+ var PCM_BYTE_MIDPOINT = 128;
2058
+ var GAP_EPSILON_S = 0.03;
2059
+ var createInitialState3 = () => ({
2060
+ activeSourceCount: 0,
2061
+ error: null,
2062
+ isActive: false,
2063
+ isPlaying: false,
2064
+ lastInterruptLatencyMs: undefined,
2065
+ lastPlaybackStopLatencyMs: undefined,
2066
+ processedChunkCount: 0,
2067
+ queuedChunkCount: 0
2068
+ });
2069
+ var getAudioContextCtor = () => {
2070
+ if (typeof window === "undefined") {
2071
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
2072
+ }
2073
+ return window.AudioContext ?? window.webkitAudioContext;
2074
+ };
2075
+ var clampVolume = (volume) => {
2076
+ if (typeof volume !== "number" || !Number.isFinite(volume)) {
2077
+ return DEFAULT_VOLUME;
2078
+ }
2079
+ return Math.min(1, Math.max(0, volume));
2080
+ };
2081
+ var clampPlaybackRate = (rate) => {
2082
+ if (typeof rate !== "number" || !Number.isFinite(rate)) {
2083
+ return DEFAULT_PLAYBACK_RATE;
2084
+ }
2085
+ return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, rate));
2086
+ };
2087
+ var decodePCM16LEChunk = (audioContext, chunk) => {
2088
+ const { format } = chunk;
2089
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
2090
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
2091
+ }
2092
+ const bytes = chunk.chunk;
2093
+ const channels = Math.max(1, format.channels);
2094
+ const sampleCount = Math.floor(bytes.byteLength / 2);
2095
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
2096
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
2097
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2098
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
2099
+ const channelData = audioBuffer.getChannelData(channelIndex);
2100
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
2101
+ const sampleIndex = frameIndex * channels + channelIndex;
2102
+ const sampleOffset = sampleIndex * 2;
2103
+ if (sampleOffset + 1 >= bytes.byteLength) {
2104
+ channelData[frameIndex] = 0;
2105
+ continue;
2106
+ }
2107
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
2108
+ }
2109
+ }
2110
+ return audioBuffer;
2111
+ };
2112
+ var createVoiceAudioPlayer = (source, options = {}) => {
2113
+ const subscribers = new Set;
2114
+ const sourceNodes = new Set;
2115
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
2116
+ let state = createInitialState3();
2117
+ let audioContext = null;
2118
+ let outputNode = null;
2119
+ let analyserNode = null;
2120
+ let analyserBuffer = null;
2121
+ let volume = clampVolume(options.volume);
2122
+ let playbackRate = clampPlaybackRate(options.playbackRate);
2123
+ let stretcher = null;
2124
+ let queueEndTime = 0;
2125
+ let syncPromise = Promise.resolve();
2126
+ let interruptStartedAt = null;
2127
+ let interruptPromise = null;
2128
+ let resolveInterruptPromise = null;
2129
+ let interruptFallbackTimer = null;
2130
+ let concurrencyCounted = false;
2131
+ let observedPeakConcurrency = 0;
2132
+ let scheduledDurationMs = 0;
2133
+ let gapCount = 0;
2134
+ let totalGapMs = 0;
2135
+ let maxGapMs = 0;
2136
+ let errorCount = 0;
2137
+ let hasScheduledAnyChunk = false;
2138
+ const sampleConcurrency = () => {
2139
+ if (activeAudioPlayers.size > observedPeakConcurrency) {
2140
+ observedPeakConcurrency = activeAudioPlayers.size;
2141
+ }
2142
+ };
2143
+ const markPlayerActive = () => {
2144
+ if (concurrencyCounted) {
2145
+ return;
2146
+ }
2147
+ concurrencyCounted = true;
2148
+ activeAudioPlayers.add(sampleConcurrency);
2149
+ for (const sample of activeAudioPlayers) {
2150
+ sample();
2151
+ }
2152
+ };
2153
+ const markPlayerInactive = () => {
2154
+ if (!concurrencyCounted) {
2155
+ return;
2156
+ }
2157
+ concurrencyCounted = false;
2158
+ activeAudioPlayers.delete(sampleConcurrency);
2159
+ };
2160
+ const notify = () => {
2161
+ for (const subscriber of subscribers) {
2162
+ subscriber();
2163
+ }
2164
+ };
2165
+ const setState = (next) => {
2166
+ state = {
2167
+ ...state,
2168
+ ...next
2169
+ };
2170
+ notify();
2171
+ };
2172
+ const clearError = () => {
2173
+ if (state.error !== null) {
2174
+ setState({ error: null });
2175
+ }
2176
+ };
2177
+ const clearInterruptTimer = () => {
2178
+ if (interruptFallbackTimer !== null) {
2179
+ clearTimeout(interruptFallbackTimer);
2180
+ interruptFallbackTimer = null;
2181
+ }
2182
+ };
2183
+ const resolveInterrupt = (latencyMs) => {
2184
+ clearInterruptTimer();
2185
+ interruptStartedAt = null;
2186
+ stretcher?.reset();
2187
+ setState({
2188
+ activeSourceCount: sourceNodes.size,
2189
+ isPlaying: false,
2190
+ lastInterruptLatencyMs: latencyMs,
2191
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
2192
+ });
2193
+ resolveInterruptPromise?.();
2194
+ resolveInterruptPromise = null;
2195
+ interruptPromise = null;
2196
+ };
2197
+ const estimateOutputStopLatencyMs = (context) => {
2198
+ if (!context) {
2199
+ return 0;
2200
+ }
2201
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
2202
+ };
2203
+ const applyOutputGain = (context) => {
2204
+ if (!outputNode) {
2205
+ return;
2206
+ }
2207
+ const gainValue = volume;
2208
+ if (outputNode.gain.setValueAtTime) {
2209
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
2210
+ return;
2211
+ }
2212
+ outputNode.gain.value = gainValue;
2213
+ };
2214
+ const muteOutputGain = (context) => {
2215
+ if (!outputNode) {
2216
+ return;
2217
+ }
2218
+ const gainValue = 0;
2219
+ if (outputNode.gain.setValueAtTime) {
2220
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
2221
+ return;
2222
+ }
2223
+ outputNode.gain.value = gainValue;
2224
+ };
2225
+ const maybeResolveInterrupt = () => {
2226
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
2227
+ return;
2228
+ }
2229
+ resolveInterrupt(Date.now() - interruptStartedAt);
2230
+ };
2231
+ const ensureAudioContext = async () => {
2232
+ if (audioContext) {
2233
+ return audioContext;
2234
+ }
2235
+ if (options.createAudioContext) {
2236
+ audioContext = options.createAudioContext();
2237
+ } else {
2238
+ const AudioContextCtor = getAudioContextCtor();
2239
+ if (!AudioContextCtor) {
2240
+ throw new Error("Assistant audio playback requires AudioContext support.");
2241
+ }
2242
+ audioContext = new AudioContextCtor;
2243
+ }
2244
+ if (audioContext.createGain) {
2245
+ outputNode = audioContext.createGain();
2246
+ outputNode.connect?.(audioContext.destination);
2247
+ if (audioContext.createAnalyser) {
2248
+ analyserNode = audioContext.createAnalyser();
2249
+ analyserNode.fftSize = ANALYSER_FFT_SIZE;
2250
+ analyserBuffer = new Uint8Array(analyserNode.fftSize);
2251
+ outputNode.connect?.(analyserNode);
2252
+ }
2253
+ }
2254
+ queueEndTime = audioContext.currentTime;
2255
+ return audioContext;
2256
+ };
2257
+ const scheduleBuffer = (context, buffer, rate) => {
2258
+ const node = context.createBufferSource();
2259
+ node.buffer = buffer;
2260
+ if (node.playbackRate) {
2261
+ node.playbackRate.value = rate;
2262
+ }
2263
+ node.connect(outputNode ?? context.destination);
2264
+ node.onended = () => {
2265
+ sourceNodes.delete(node);
2266
+ node.disconnect?.();
2267
+ setState({
2268
+ activeSourceCount: sourceNodes.size,
2269
+ isPlaying: sourceNodes.size > 0 && state.isActive
2270
+ });
2271
+ maybeResolveInterrupt();
2272
+ };
2273
+ const earliestStart = context.currentTime + lookaheadSeconds;
2274
+ const startAt = Math.max(earliestStart, queueEndTime);
2275
+ if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
2276
+ const gapMs = (earliestStart - queueEndTime) * 1000;
2277
+ gapCount += 1;
2278
+ totalGapMs += gapMs;
2279
+ maxGapMs = Math.max(maxGapMs, gapMs);
2280
+ }
2281
+ hasScheduledAnyChunk = true;
2282
+ scheduledDurationMs += buffer.duration / rate * 1000;
2283
+ sampleConcurrency();
2284
+ queueEndTime = startAt + buffer.duration / rate;
2285
+ sourceNodes.add(node);
2286
+ setState({
2287
+ activeSourceCount: sourceNodes.size,
2288
+ isPlaying: true
2289
+ });
2290
+ node.start(startAt);
2291
+ };
2292
+ const scheduleChunk = async (chunk) => {
2293
+ const context = await ensureAudioContext();
2294
+ const buffer = decodePCM16LEChunk(context, chunk);
2295
+ if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
2296
+ stretcher?.reset();
2297
+ scheduleBuffer(context, buffer, playbackRate);
2298
+ return;
2299
+ }
2300
+ const channels = Math.max(1, chunk.format.channels);
2301
+ const input = [];
2302
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
2303
+ input.push(buffer.getChannelData(channelIndex));
2304
+ }
2305
+ stretcher ??= createTimeStretcher();
2306
+ const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
2307
+ const outLength = stretched[0]?.length ?? 0;
2308
+ if (outLength === 0) {
2309
+ return;
2310
+ }
2311
+ const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
2312
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
2313
+ const channelOut = stretched[channelIndex];
2314
+ if (!channelOut)
2315
+ continue;
2316
+ outBuffer.getChannelData(channelIndex).set(channelOut);
2317
+ }
2318
+ scheduleBuffer(context, outBuffer, 1);
2319
+ };
2320
+ const stopQueuedPlayback = (options2) => {
2321
+ for (const node of [...sourceNodes]) {
2322
+ node.stop?.();
2323
+ }
2324
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
2325
+ if (options2?.forceClear) {
2326
+ for (const node of sourceNodes) {
2327
+ node.disconnect?.();
2328
+ }
2329
+ sourceNodes.clear();
2330
+ maybeResolveInterrupt();
2331
+ }
2332
+ };
2333
+ const sync = async () => {
2334
+ if (!state.isActive) {
2335
+ return;
2336
+ }
2337
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
2338
+ if (nextChunks.length === 0) {
2339
+ return;
2340
+ }
2341
+ try {
2342
+ clearError();
2343
+ for (const chunk of nextChunks) {
2344
+ await scheduleChunk(chunk);
2345
+ }
2346
+ setState({
2347
+ processedChunkCount: source.assistantAudio.length,
2348
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
2349
+ });
2350
+ } catch (error) {
2351
+ errorCount += 1;
2352
+ setState({
2353
+ error: error instanceof Error ? error.message : String(error)
2354
+ });
2355
+ }
2356
+ };
2357
+ const queueSync = () => {
2358
+ syncPromise = syncPromise.then(() => sync(), () => sync());
2359
+ return syncPromise;
2360
+ };
2361
+ const unsubscribeSource = source.subscribe(() => {
2362
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
2363
+ player.start();
2364
+ return;
2365
+ }
2366
+ if (state.isActive) {
2367
+ queueSync();
2368
+ }
2369
+ });
2370
+ const player = {
2371
+ get activeSourceCount() {
2372
+ return state.activeSourceCount;
1094
2373
  },
1095
- get assistantTexts() {
1096
- return state.assistantTexts;
2374
+ close: async () => {
2375
+ markPlayerInactive();
2376
+ unsubscribeSource();
2377
+ stopQueuedPlayback({ forceClear: true });
2378
+ clearInterruptTimer();
2379
+ resolveInterruptPromise?.();
2380
+ resolveInterruptPromise = null;
2381
+ interruptPromise = null;
2382
+ interruptStartedAt = null;
2383
+ if (audioContext && audioContext.state !== "closed") {
2384
+ await audioContext.close();
2385
+ }
2386
+ audioContext = null;
2387
+ outputNode?.disconnect?.();
2388
+ outputNode = null;
2389
+ analyserNode?.disconnect?.();
2390
+ analyserNode = null;
2391
+ analyserBuffer = null;
2392
+ queueEndTime = 0;
2393
+ setState({
2394
+ activeSourceCount: 0,
2395
+ isActive: false,
2396
+ isPlaying: false
2397
+ });
1097
2398
  },
1098
- get assistantAudio() {
1099
- return state.assistantAudio;
2399
+ get error() {
2400
+ return state.error;
1100
2401
  },
1101
- get call() {
1102
- return state.call;
2402
+ getOutputLevel: () => {
2403
+ if (!analyserNode || !analyserBuffer) {
2404
+ return 0;
2405
+ }
2406
+ analyserNode.getByteTimeDomainData(analyserBuffer);
2407
+ let sumSquares = 0;
2408
+ for (const sample of analyserBuffer) {
2409
+ const centered = (sample - PCM_BYTE_MIDPOINT) / PCM_BYTE_MIDPOINT;
2410
+ sumSquares += centered * centered;
2411
+ }
2412
+ return Math.sqrt(sumSquares / analyserBuffer.length);
2413
+ },
2414
+ getSnapshot: () => state,
2415
+ getIntegritySummary: () => {
2416
+ const chunksReceived = source.assistantAudio.length;
2417
+ const chunksScheduled = state.processedChunkCount;
2418
+ return {
2419
+ chunksReceived,
2420
+ chunksScheduled,
2421
+ errorCount,
2422
+ gapCount,
2423
+ maxConcurrentPlayers: observedPeakConcurrency,
2424
+ maxGapMs: Math.round(maxGapMs),
2425
+ ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived,
2426
+ scheduledDurationMs: Math.round(scheduledDurationMs),
2427
+ totalGapMs: Math.round(totalGapMs)
2428
+ };
2429
+ },
2430
+ interrupt: async () => {
2431
+ const startedAt = Date.now();
2432
+ const context = await ensureAudioContext();
2433
+ interruptStartedAt = startedAt;
2434
+ muteOutputGain(context);
2435
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
2436
+ setState({
2437
+ isActive: false,
2438
+ isPlaying: sourceNodes.size > 0,
2439
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
2440
+ });
2441
+ if (sourceNodes.size === 0) {
2442
+ resolveInterrupt(playbackStopLatencyMs);
2443
+ return;
2444
+ }
2445
+ if (!interruptPromise) {
2446
+ interruptPromise = new Promise((resolve) => {
2447
+ resolveInterruptPromise = resolve;
2448
+ });
2449
+ }
2450
+ clearInterruptTimer();
2451
+ interruptFallbackTimer = setTimeout(() => {
2452
+ for (const node of sourceNodes) {
2453
+ node.disconnect?.();
2454
+ }
2455
+ sourceNodes.clear();
2456
+ resolveInterrupt(Date.now() - startedAt);
2457
+ }, 250);
2458
+ stopQueuedPlayback();
2459
+ await interruptPromise;
2460
+ },
2461
+ get isActive() {
2462
+ return state.isActive;
2463
+ },
2464
+ get isPlaying() {
2465
+ return state.isPlaying;
2466
+ },
2467
+ get lastInterruptLatencyMs() {
2468
+ return state.lastInterruptLatencyMs;
2469
+ },
2470
+ get lastPlaybackStopLatencyMs() {
2471
+ return state.lastPlaybackStopLatencyMs;
2472
+ },
2473
+ pause: async () => {
2474
+ if (!audioContext) {
2475
+ setState({
2476
+ activeSourceCount: 0,
2477
+ isActive: false,
2478
+ isPlaying: false
2479
+ });
2480
+ return;
2481
+ }
2482
+ await audioContext.suspend();
2483
+ markPlayerInactive();
2484
+ setState({
2485
+ activeSourceCount: sourceNodes.size,
2486
+ isActive: false,
2487
+ isPlaying: false
2488
+ });
2489
+ },
2490
+ get playbackRate() {
2491
+ return playbackRate;
2492
+ },
2493
+ get processedChunkCount() {
2494
+ return state.processedChunkCount;
2495
+ },
2496
+ get queuedChunkCount() {
2497
+ return state.queuedChunkCount;
2498
+ },
2499
+ setPlaybackRate: (nextRate) => {
2500
+ playbackRate = clampPlaybackRate(nextRate);
2501
+ },
2502
+ setVolume: (nextVolume) => {
2503
+ volume = clampVolume(nextVolume);
2504
+ applyOutputGain(audioContext);
2505
+ },
2506
+ start: async () => {
2507
+ try {
2508
+ clearError();
2509
+ const context = await ensureAudioContext();
2510
+ applyOutputGain(context);
2511
+ if (context.state === "suspended") {
2512
+ await context.resume();
2513
+ }
2514
+ markPlayerActive();
2515
+ setState({
2516
+ activeSourceCount: sourceNodes.size,
2517
+ isActive: true,
2518
+ isPlaying: context.state === "running"
2519
+ });
2520
+ await queueSync();
2521
+ } catch (error) {
2522
+ errorCount += 1;
2523
+ markPlayerInactive();
2524
+ setState({
2525
+ error: error instanceof Error ? error.message : String(error),
2526
+ isActive: false,
2527
+ isPlaying: false
2528
+ });
2529
+ throw error;
2530
+ }
2531
+ },
2532
+ subscribe: (subscriber) => {
2533
+ subscribers.add(subscriber);
2534
+ return () => {
2535
+ subscribers.delete(subscriber);
2536
+ };
2537
+ },
2538
+ get volume() {
2539
+ return volume;
2540
+ }
2541
+ };
2542
+ return player;
2543
+ };
2544
+
2545
+ // src/client/bargeInMonitor.ts
2546
+ var DEFAULT_THRESHOLD_MS = 250;
2547
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
2548
+ var summarize = (events, thresholdMs) => {
2549
+ const stopped = events.filter((event) => event.status === "stopped");
2550
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
2551
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
2552
+ const passed = stopped.length - failed;
2553
+ return {
2554
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
2555
+ events: [...events],
2556
+ failed,
2557
+ lastEvent: events.at(-1),
2558
+ passed,
2559
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
2560
+ thresholdMs,
2561
+ total: stopped.length
2562
+ };
2563
+ };
2564
+ var createVoiceBargeInMonitor = (options = {}) => {
2565
+ const listeners = new Set;
2566
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
2567
+ const fetchImpl = options.fetch ?? globalThis.fetch;
2568
+ const events = [];
2569
+ const emit = () => {
2570
+ for (const listener of listeners) {
2571
+ listener();
2572
+ }
2573
+ };
2574
+ const postEvent = (event) => {
2575
+ if (!options.path || typeof fetchImpl !== "function") {
2576
+ return;
2577
+ }
2578
+ fetchImpl(options.path, {
2579
+ body: JSON.stringify(event),
2580
+ headers: {
2581
+ "Content-Type": "application/json"
2582
+ },
2583
+ method: "POST"
2584
+ }).catch(() => {});
2585
+ };
2586
+ const record = (status, input) => {
2587
+ const event = {
2588
+ at: Date.now(),
2589
+ id: createEventId(),
2590
+ latencyMs: input.latencyMs,
2591
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
2592
+ reason: input.reason,
2593
+ sessionId: input.sessionId,
2594
+ status,
2595
+ thresholdMs
2596
+ };
2597
+ events.push(event);
2598
+ postEvent(event);
2599
+ emit();
2600
+ return event;
2601
+ };
2602
+ return {
2603
+ getSnapshot: () => summarize(events, thresholdMs),
2604
+ recordRequested: (input) => record("requested", input),
2605
+ recordSkipped: (input) => record("skipped", input),
2606
+ recordStopped: (input) => record("stopped", input),
2607
+ subscribe: (subscriber) => {
2608
+ listeners.add(subscriber);
2609
+ return () => {
2610
+ listeners.delete(subscriber);
2611
+ };
2612
+ }
2613
+ };
2614
+ };
2615
+
2616
+ // src/client/duplex.ts
2617
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
2618
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
2619
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
2620
+ let lastPartial = controller.partial;
2621
+ const interruptIfPlaying = (reason) => {
2622
+ if (!player.isPlaying || options.enabled === false) {
2623
+ options.monitor?.recordSkipped({
2624
+ reason,
2625
+ sessionId: controller.sessionId
2626
+ });
2627
+ return;
2628
+ }
2629
+ options.monitor?.recordRequested({
2630
+ reason,
2631
+ sessionId: controller.sessionId
2632
+ });
2633
+ player.interrupt().then(() => {
2634
+ options.monitor?.recordStopped({
2635
+ latencyMs: player.lastInterruptLatencyMs,
2636
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
2637
+ reason,
2638
+ sessionId: controller.sessionId
2639
+ });
2640
+ });
2641
+ };
2642
+ const unsubscribe = controller.subscribe(() => {
2643
+ if (options.interruptOnPartial === false) {
2644
+ lastPartial = controller.partial;
2645
+ return;
2646
+ }
2647
+ if (!lastPartial && controller.partial) {
2648
+ interruptIfPlaying("partial-transcript");
2649
+ }
2650
+ lastPartial = controller.partial;
2651
+ });
2652
+ return {
2653
+ close: () => {
2654
+ unsubscribe();
2655
+ },
2656
+ handleLevel: (level) => {
2657
+ if (shouldInterruptForLevel(level, options)) {
2658
+ interruptIfPlaying("input-level");
2659
+ }
2660
+ },
2661
+ sendAudio: (audio) => {
2662
+ interruptIfPlaying("manual-audio");
2663
+ controller.sendAudio(audio);
1103
2664
  }
1104
2665
  };
1105
2666
  };
@@ -1126,8 +2687,7 @@ var DEFAULT_GUIDED_PROMPTS = [
1126
2687
  "Now describe what you are trying to do or test.",
1127
2688
  "Finish with any detail that feels blocked, risky, or unclear."
1128
2689
  ];
1129
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1130
- var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
2690
+ var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
1131
2691
  var readErrorField = (value, key) => {
1132
2692
  const candidate = value[key];
1133
2693
  if (typeof candidate === "string" && candidate.trim()) {
@@ -1160,6 +2720,17 @@ var formatErrorMessage = (error) => {
1160
2720
  }
1161
2721
  return "Unexpected error";
1162
2722
  };
2723
+ var formatReconnectState = (reconnect) => {
2724
+ const pieces = [reconnect.status];
2725
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
2726
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
2727
+ }
2728
+ if (reconnect.nextAttemptAt) {
2729
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
2730
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
2731
+ }
2732
+ return pieces.join(" · ");
2733
+ };
1163
2734
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1164
2735
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1165
2736
  const next = levels.slice(-(count - 1));
@@ -1216,6 +2787,20 @@ var parsePromptList = (value) => {
1216
2787
  } catch {}
1217
2788
  return DEFAULT_GUIDED_PROMPTS;
1218
2789
  };
2790
+ var parseOptionalNumber = (value) => {
2791
+ if (!value) {
2792
+ return;
2793
+ }
2794
+ const parsed = Number(value);
2795
+ return Number.isFinite(parsed) ? parsed : undefined;
2796
+ };
2797
+ var resolveElement2 = (root, selector, ctor) => {
2798
+ if (!selector) {
2799
+ return null;
2800
+ }
2801
+ const value = document.querySelector(selector);
2802
+ return value instanceof ctor ? value : null;
2803
+ };
1219
2804
  var requireElement = (root, selector, ctor, name) => {
1220
2805
  const value = selector ? document.querySelector(selector) : null;
1221
2806
  if (value instanceof ctor) {
@@ -1266,11 +2851,20 @@ var initVoiceHTMXRoot = (root) => {
1266
2851
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
2852
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
2853
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
2854
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
2855
+ const bargeInPath = root.dataset.voiceBargeInPath;
2856
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
2857
+ path: bargeInPath,
2858
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
2859
+ }) : null;
2860
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
2861
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
2862
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
2863
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
2864
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1272
2865
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1273
2866
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
2867
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1274
2868
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1275
2869
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1276
2870
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1279,35 +2873,70 @@ var initVoiceHTMXRoot = (root) => {
1279
2873
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
2874
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
2875
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
2876
+ let activeMode = null;
2877
+ let hasStartedModes = {
2878
+ general: false,
2879
+ guided: false
2880
+ };
2881
+ let isCapturing = false;
2882
+ let micError = null;
2883
+ let waveLevels = createInitialVoiceWaveLevels();
2884
+ let guidedBargeInBinding = null;
2885
+ let generalBargeInBinding = null;
1282
2886
  const guidedVoice = createVoiceController(guidedPath, {
1283
2887
  capture: {
2888
+ onAudio: (audio, sendAudio) => {
2889
+ if (guidedBargeInBinding) {
2890
+ guidedBargeInBinding.sendAudio(audio);
2891
+ return;
2892
+ }
2893
+ sendAudio(audio);
2894
+ },
1284
2895
  onLevel: (level) => {
2896
+ guidedBargeInBinding?.handleLevel(level);
1285
2897
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
2898
  renderWave();
1287
2899
  }
1288
2900
  },
2901
+ connection: {
2902
+ reconnectReportPath
2903
+ },
1289
2904
  preset: "guided-intake"
1290
2905
  });
1291
2906
  const generalVoice = createVoiceController(generalPath, {
1292
2907
  capture: {
2908
+ onAudio: (audio, sendAudio) => {
2909
+ if (generalBargeInBinding) {
2910
+ generalBargeInBinding.sendAudio(audio);
2911
+ return;
2912
+ }
2913
+ sendAudio(audio);
2914
+ },
1293
2915
  onLevel: (level) => {
2916
+ generalBargeInBinding?.handleLevel(level);
1294
2917
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
2918
  renderWave();
1296
2919
  }
1297
2920
  },
2921
+ connection: {
2922
+ reconnectReportPath
2923
+ },
1298
2924
  preset: "dictation"
1299
2925
  });
1300
2926
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
2927
  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();
2928
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
2929
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
2930
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
2931
+ interruptThreshold: bargeInSpeechThreshold,
2932
+ monitor: bargeInMonitor ?? undefined
2933
+ });
2934
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
2935
+ interruptThreshold: bargeInSpeechThreshold,
2936
+ monitor: bargeInMonitor ?? undefined
2937
+ });
1310
2938
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
2939
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1311
2940
  const renderWave = () => {
1312
2941
  const path = createVoiceWavePath(waveLevels);
1313
2942
  voiceWaveGlow.setAttribute("d", path);
@@ -1319,9 +2948,12 @@ var initVoiceHTMXRoot = (root) => {
1319
2948
  const render = () => {
1320
2949
  const voice = currentVoice();
1321
2950
  const hasStarted = (activeMode ? hasStartedModes[activeMode] : false) || voice.turns.length > 0;
1322
- const status = voice.status;
2951
+ const { status } = voice;
1323
2952
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1324
2953
  errorStatus.textContent = micError || voice.error || "None";
2954
+ if (reconnectStatus) {
2955
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2956
+ }
1325
2957
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1326
2958
  promptStatus.textContent = resolvePromptMessage({
1327
2959
  guidedPrompts,
@@ -1385,8 +3017,18 @@ var initVoiceHTMXRoot = (root) => {
1385
3017
  render();
1386
3018
  }
1387
3019
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
3020
+ guidedVoice.subscribe(() => {
3021
+ if (guidedVoice.assistantAudio.length > 0) {
3022
+ guidedAudioPlayer.start().catch(() => {});
3023
+ }
3024
+ render();
3025
+ });
3026
+ generalVoice.subscribe(() => {
3027
+ if (generalVoice.assistantAudio.length > 0) {
3028
+ generalAudioPlayer.start().catch(() => {});
3029
+ }
3030
+ render();
3031
+ });
1390
3032
  startGuidedButton.addEventListener("click", () => {
1391
3033
  startMode("guided");
1392
3034
  });
@@ -1396,9 +3038,16 @@ var initVoiceHTMXRoot = (root) => {
1396
3038
  stopButton.addEventListener("click", () => {
1397
3039
  stopMic();
1398
3040
  });
3041
+ root.addEventListener("absolute-voice-simulate-disconnect", () => {
3042
+ currentVoice().simulateDisconnect();
3043
+ });
1399
3044
  window.addEventListener("beforeunload", () => {
1400
3045
  guidedVoice.stopRecording();
1401
3046
  generalVoice.stopRecording();
3047
+ guidedBargeInBinding?.close();
3048
+ generalBargeInBinding?.close();
3049
+ guidedAudioPlayer.close();
3050
+ generalAudioPlayer.close();
1402
3051
  stopGuidedBinding();
1403
3052
  stopGeneralBinding();
1404
3053
  guidedVoice.close();