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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -574,9 +574,11 @@ var createVoiceBrowserMediaReporter = (options) => {
574
574
  var WS_OPEN = 1;
575
575
  var WS_CLOSED = 3;
576
576
  var WS_NORMAL_CLOSURE = 1000;
577
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
577
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
578
578
  var DEFAULT_PING_INTERVAL = 30000;
579
- var RECONNECT_DELAY_MS = 500;
579
+ var RECONNECT_BASE_DELAY_MS = 500;
580
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
581
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
580
582
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
581
583
  var noop = () => {};
582
584
  var noopUnsubscribe = () => noop;
@@ -645,7 +647,9 @@ var createVoiceConnection = (path, options = {}) => {
645
647
  const listeners = new Set;
646
648
  const shouldReconnect = options.reconnect !== false;
647
649
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
650
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
648
651
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
652
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
649
653
  const state = {
650
654
  isConnected: false,
651
655
  pendingMessages: [],
@@ -681,8 +685,9 @@ var createVoiceConnection = (path, options = {}) => {
681
685
  }
682
686
  };
683
687
  const scheduleReconnect = () => {
684
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
685
688
  state.reconnectAttempts += 1;
689
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
690
+ const nextAttemptAt = Date.now() + delayMs;
686
691
  emitConnection({
687
692
  reconnect: {
688
693
  attempts: state.reconnectAttempts,
@@ -706,7 +711,7 @@ var createVoiceConnection = (path, options = {}) => {
706
711
  return;
707
712
  }
708
713
  connect();
709
- }, RECONNECT_DELAY_MS);
714
+ }, delayMs);
710
715
  };
711
716
  const connect = () => {
712
717
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -1,4 +1,8 @@
1
1
  import type { VoiceClientMessage, VoiceConnectionOptions, VoiceServerMessage } from "../core/types";
2
+ /** Exponential reconnect backoff for attempt N (1-based): baseMs doubles each
3
+ * attempt, capped at maxDelayMs. Exported so the backoff window is unit-tested
4
+ * without a DOM/WebSocket harness. */
5
+ export declare const computeVoiceReconnectDelayMs: (attempt: number, baseMs: number, maxDelayMs: number) => number;
2
6
  type VoiceConnectionHandle = {
3
7
  callControl: (message: Omit<VoiceClientMessage & {
4
8
  type: "call_control";
@@ -491,9 +491,11 @@ var createVoiceBrowserMediaReporter = (options) => {
491
491
  var WS_OPEN = 1;
492
492
  var WS_CLOSED = 3;
493
493
  var WS_NORMAL_CLOSURE = 1000;
494
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
494
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
495
495
  var DEFAULT_PING_INTERVAL = 30000;
496
- var RECONNECT_DELAY_MS = 500;
496
+ var RECONNECT_BASE_DELAY_MS = 500;
497
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
498
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
497
499
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
498
500
  var noop = () => {};
499
501
  var noopUnsubscribe = () => noop;
@@ -562,7 +564,9 @@ var createVoiceConnection = (path, options = {}) => {
562
564
  const listeners = new Set;
563
565
  const shouldReconnect = options.reconnect !== false;
564
566
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
567
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
565
568
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
569
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
566
570
  const state = {
567
571
  isConnected: false,
568
572
  pendingMessages: [],
@@ -598,8 +602,9 @@ var createVoiceConnection = (path, options = {}) => {
598
602
  }
599
603
  };
600
604
  const scheduleReconnect = () => {
601
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
602
605
  state.reconnectAttempts += 1;
606
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
607
+ const nextAttemptAt = Date.now() + delayMs;
603
608
  emitConnection({
604
609
  reconnect: {
605
610
  attempts: state.reconnectAttempts,
@@ -623,7 +628,7 @@ var createVoiceConnection = (path, options = {}) => {
623
628
  return;
624
629
  }
625
630
  connect();
626
- }, RECONNECT_DELAY_MS);
631
+ }, delayMs);
627
632
  };
628
633
  const connect = () => {
629
634
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -104,9 +104,11 @@ var voiceSseReactiveSource = (topic, options = {}) => (refresh) => {
104
104
  var WS_OPEN = 1;
105
105
  var WS_CLOSED = 3;
106
106
  var WS_NORMAL_CLOSURE = 1000;
107
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
107
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
108
108
  var DEFAULT_PING_INTERVAL = 30000;
109
- var RECONNECT_DELAY_MS = 500;
109
+ var RECONNECT_BASE_DELAY_MS = 500;
110
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
111
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
110
112
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
111
113
  var noop = () => {};
112
114
  var noopUnsubscribe = () => noop;
@@ -175,7 +177,9 @@ var createVoiceConnection = (path, options = {}) => {
175
177
  const listeners = new Set;
176
178
  const shouldReconnect = options.reconnect !== false;
177
179
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
180
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
178
181
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
182
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
179
183
  const state = {
180
184
  isConnected: false,
181
185
  pendingMessages: [],
@@ -211,8 +215,9 @@ var createVoiceConnection = (path, options = {}) => {
211
215
  }
212
216
  };
213
217
  const scheduleReconnect = () => {
214
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
215
218
  state.reconnectAttempts += 1;
219
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
220
+ const nextAttemptAt = Date.now() + delayMs;
216
221
  emitConnection({
217
222
  reconnect: {
218
223
  attempts: state.reconnectAttempts,
@@ -236,7 +241,7 @@ var createVoiceConnection = (path, options = {}) => {
236
241
  return;
237
242
  }
238
243
  connect();
239
- }, RECONNECT_DELAY_MS);
244
+ }, delayMs);
240
245
  };
241
246
  const connect = () => {
242
247
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -1113,6 +1113,12 @@ export type VoiceConnectionOptions = {
1113
1113
  reconnect?: boolean;
1114
1114
  reconnectReportPath?: string;
1115
1115
  maxReconnectAttempts?: number;
1116
+ /** Cap on the exponential reconnect backoff (ms). The delay doubles from 500ms
1117
+ * up to this ceiling each attempt, so the total retry window is roughly
1118
+ * maxReconnectAttempts spread across it. Default 8000 — with the default 15
1119
+ * attempts that's a ~95s window, enough to ride out a server redeploy without
1120
+ * the caller losing the call. */
1121
+ reconnectMaxDelayMs?: number;
1116
1122
  pingInterval?: number;
1117
1123
  sessionId?: string;
1118
1124
  };
@@ -0,0 +1,41 @@
1
+ import type { VoiceLogger, VoiceSessionRecord, VoiceSessionStore } from "./types";
2
+ export type VoiceWriteBehindStore<TSession extends VoiceSessionRecord = VoiceSessionRecord> = VoiceSessionStore<TSession> & {
3
+ /** Force every pending snapshot out to the persistent store. Call on graceful
4
+ * shutdown (after the deploy drain) so an in-flight call survives the restart
5
+ * even if it was killed at the drain ceiling. Resolves once the persistent
6
+ * store has the latest snapshot of every dirty session. */
7
+ flush: () => Promise<void>;
8
+ /** Stop the debounce timer. Call after the final flush() on shutdown. */
9
+ dispose: () => void;
10
+ };
11
+ export type VoiceWriteBehindStoreOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = {
12
+ /** The durable store snapshots are written back to (Postgres / SQLite / file).
13
+ * This is what a fresh process reads from to resume a call after a restart. */
14
+ persistent: VoiceSessionStore<TSession>;
15
+ /** The synchronous hot-path store. Defaults to an in-memory Map store — the
16
+ * config that keeps up with high-frequency STT callbacks without dropping
17
+ * transcripts. Override only for tests. */
18
+ memory?: VoiceSessionStore<TSession>;
19
+ /** How long a write may sit in memory before it's flushed to the persistent
20
+ * store. Bounds the worst-case state lost on a hard crash. Default 750ms. A
21
+ * turn commit (turns array grows) bypasses this and flushes immediately. */
22
+ flushDebounceMs?: number;
23
+ logger?: VoiceLogger;
24
+ };
25
+ /**
26
+ * A session store that is fast AND durable.
27
+ *
28
+ * The in-memory store is authoritative for the live call: get/set are
29
+ * synchronous, so the hot path (STT partials/finals firing every ~100ms) never
30
+ * waits on the database and concurrent writes can't race. Every write is ALSO
31
+ * recorded as a pending snapshot and flushed to the persistent store on a
32
+ * debounce (coalescing a burst of partials into ~one DB write), with a turn
33
+ * commit flushing immediately so completed turns are durable right away.
34
+ *
35
+ * On a cold `get`/`getOrCreate` miss — e.g. a fresh process after a deploy — it
36
+ * hydrates the session from the persistent store and repopulates memory. That's
37
+ * the resume path: the client reconnects with the same session id, the server
38
+ * finds the persisted session, so it does NOT re-fire the greeting and replays
39
+ * the prior turns instead of starting over.
40
+ */
41
+ export declare const createVoiceWriteBehindStore: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceWriteBehindStoreOptions<TSession>) => VoiceWriteBehindStore<TSession>;
@@ -488,9 +488,11 @@ var createVoiceBrowserMediaReporter = (options) => {
488
488
  var WS_OPEN = 1;
489
489
  var WS_CLOSED = 3;
490
490
  var WS_NORMAL_CLOSURE = 1000;
491
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
491
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
492
492
  var DEFAULT_PING_INTERVAL = 30000;
493
- var RECONNECT_DELAY_MS = 500;
493
+ var RECONNECT_BASE_DELAY_MS = 500;
494
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
495
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
494
496
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
495
497
  var noop = () => {};
496
498
  var noopUnsubscribe = () => noop;
@@ -559,7 +561,9 @@ var createVoiceConnection = (path, options = {}) => {
559
561
  const listeners = new Set;
560
562
  const shouldReconnect = options.reconnect !== false;
561
563
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
564
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
562
565
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
566
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
563
567
  const state = {
564
568
  isConnected: false,
565
569
  pendingMessages: [],
@@ -595,8 +599,9 @@ var createVoiceConnection = (path, options = {}) => {
595
599
  }
596
600
  };
597
601
  const scheduleReconnect = () => {
598
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
599
602
  state.reconnectAttempts += 1;
603
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
604
+ const nextAttemptAt = Date.now() + delayMs;
600
605
  emitConnection({
601
606
  reconnect: {
602
607
  attempts: state.reconnectAttempts,
@@ -620,7 +625,7 @@ var createVoiceConnection = (path, options = {}) => {
620
625
  return;
621
626
  }
622
627
  connect();
623
- }, RECONNECT_DELAY_MS);
628
+ }, delayMs);
624
629
  };
625
630
  const connect = () => {
626
631
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -1,10 +1,10 @@
1
- (()=>{var{defineProperty:q,getOwnPropertyNames:Vc,getOwnPropertyDescriptor:Ic}=Object,Rc=Object.prototype.hasOwnProperty;function _c(c){return this[c]}var yc=(c)=>{var g=(z??=new WeakMap).get(c),C;if(g)return g;if(g=q({},"__esModule",{value:!0}),c&&typeof c==="object"||typeof c==="function"){for(var A of Vc(c))if(!Rc.call(g,A))q(g,A,{get:_c.bind(c,A),enumerable:!(C=Ic(c,A))||C.enumerable})}return z.set(c,g),g},z;var Sc=(c)=>c;function wc(c,g){this[c]=Sc.bind(null,g)}var Lc=(c,g)=>{for(var C in g)q(c,C,{get:g[C],enumerable:!0,configurable:!0,set:wc.bind(g,C)})};var vc={};Lc(vc,{mount:()=>cc,default:()=>sc,VOICE_EMBED_VERSION:()=>gc});var Uc=(c)=>{if(typeof c!=="string")return c;return document.querySelector(c)},Oc=(c,g,C,A)=>{let _=g??c.getAttribute("hx-get")??"";if(!_)return"";let V=new URL(_,window.location.origin);if(A)V.searchParams.set(C,A);else V.searchParams.delete(C);return`${V.pathname}${V.search}${V.hash}`},k=(c,g)=>{if(typeof window>"u"||typeof document>"u")return()=>{};let C=Uc(g.element);if(!C)return()=>{};let A=g.eventName??"voice-refresh",_=g.sessionQueryParam??"sessionId",V=()=>{let w=window,L=Oc(C,g.route,_,c.sessionId);if(L)C.setAttribute("hx-get",L);w.htmx?.process?.(C),w.htmx?.trigger?.(C,A)},R=c.subscribe(V);return V(),()=>{R()}};var $c=(c)=>Math.max(-1,Math.min(1,c)),hc=(c)=>{let g=new Int16Array(c.length);for(let C=0;C<c.length;C+=1){let A=$c(c[C]??0);g[C]=A<0?A*32768:A*32767}return new Uint8Array(g.buffer)},Dc=(c)=>{let g=c instanceof Uint8Array?c:new Uint8Array(c);if(g.byteLength<2)return 0;let C=new Int16Array(g.buffer,g.byteOffset,Math.floor(g.byteLength/2));if(C.length===0)return 0;let A=0;for(let _ of C){let V=_/32768;A+=V*V}return Math.min(1,Math.max(0,Math.sqrt(A/C.length)*5.5))},Tc=(c,g,C)=>{if(g===C)return c;let A=g/C,_=Math.round(c.length/A),V=new Float32Array(_),R=0,w=0;while(R<V.length){let L=Math.round((R+1)*A),$=0,S=0;for(let O=w;O<L&&O<c.length;O+=1)$+=c[O]??0,S+=1;V[R]=S>0?$/S:0,R+=1,w=L}return V},l=(c)=>{let g=null,C=null,A=null,_=null;return{start:async()=>{if(!c.stream&&(typeof navigator>"u"||!navigator.mediaDevices?.getUserMedia))throw Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");let w=(typeof window<"u"?window.AudioContext??window.webkitAudioContext:void 0)??AudioContext;if(!w)throw Error("Browser microphone capture requires AudioContext support.");if(_=c.stream??await navigator.mediaDevices.getUserMedia({audio:{autoGainControl:!0,channelCount:c.channelCount??1,echoCancellation:!0,noiseSuppression:!0}}),g=new w,g.state==="suspended")await g.resume();C=g.createMediaStreamSource(_),A=g.createScriptProcessor(4096,1,1),A.onaudioprocess=(L)=>{let $=L.inputBuffer.getChannelData(0),S=Tc($,g?.sampleRate??48000,c.sampleRateHz??16000),O=hc(S);c.onLevel?.(Dc(O)),c.onAudio(O)},C.connect(A),A.connect(g.destination)},stop:()=>{A?.disconnect(),C?.disconnect(),_?.getTracks().forEach((w)=>w.stop()),g?.close(),c.onLevel?.(0),g=null,_=null,A=null,C=null}}};var B=(c)=>{if(typeof c==="string"&&c.trim())return c;if(c instanceof Error&&c.message.trim())return c.message;if(c&&typeof c==="object"){let g=c;for(let C of["message","reason","description"]){let A=g[C];if(typeof A==="string"&&A.trim())return A}if("error"in g)return B(g.error);if("cause"in g)return B(g.cause);try{return JSON.stringify(c)}catch{}}return"Unexpected error"},x=(c)=>{switch(c.type){case"audio":return{chunk:Uint8Array.from(atob(c.chunkBase64),(g)=>g.charCodeAt(0)),format:c.format,receivedAt:c.receivedAt,turnId:c.turnId,type:"audio"};case"assistant":return{text:c.text,turnId:c.turnId,type:"assistant"};case"assistant_delta":return{delta:c.delta,turnId:c.turnId,type:"assistant_delta"};case"complete":return{sessionId:c.sessionId,type:"complete"};case"connection":return{reconnect:c.reconnect,type:"connection"};case"call_lifecycle":return{event:c.event,sessionId:c.sessionId,type:"call_lifecycle"};case"error":return{message:B(c.message),type:"error"};case"final":return{transcript:c.transcript,type:"final"};case"partial":return{transcript:c.transcript,type:"partial"};case"replay":return{assistantTexts:c.assistantTexts,call:c.call,partial:c.partial,scenarioId:c.scenarioId,sessionId:c.sessionId,sessionMetadata:c.sessionMetadata,status:c.status,turns:c.turns,type:"replay"};case"session":return{sessionId:c.sessionId,sessionMetadata:c.sessionMetadata,scenarioId:c.scenarioId,status:c.status,type:"session"};case"turn":return{turn:c.turn,type:"turn"};default:return null}};var ec=Math.PI*2;var M=(c,g,C,A)=>{c.push({code:C,message:A,severity:g})};var Hc=(c)=>c.length===0?void 0:c.reduce((g,C)=>g+C,0)/c.length,Q=(c)=>c.length===0?void 0:Math.max(...c);var D=(c,g)=>{let C=c[g];return typeof C==="number"&&Number.isFinite(C)?C:void 0},Z=(c,g)=>{let C=c[g];return typeof C==="boolean"?C:void 0},T=(c,g)=>{let C=c[g];return typeof C==="string"?C:void 0},j=(c)=>String(c.id??T(c,"ssrc")??D(c,"ssrc")??T(c,"trackIdentifier")??T(c,"mid")??"unknown"),d=(c)=>c===void 0?void 0:c*1000;var Gc=(c)=>{let g={};for(let[C,A]of Object.entries(c))if(A===null||typeof A==="boolean"||typeof A==="number"||typeof A==="string")g[C]=A;return g};var f=(c={})=>{let g=c.stats??[],C=[],A=g.filter((I)=>I.type==="inbound-rtp"&&T(I,"kind")!=="video"),_=g.filter((I)=>I.type==="outbound-rtp"&&T(I,"kind")!=="video"),V=g.filter((I)=>I.type==="candidate-pair"),R=g.filter((I)=>(I.type==="track"||I.type==="media-source")&&T(I,"kind")==="audio"),w=V.filter((I)=>Z(I,"selected")===!0||Z(I,"nominated")===!0||T(I,"state")==="succeeded").length,L=R.filter((I)=>T(I,"readyState")!=="ended"&&T(I,"trackState")!=="ended"&&Z(I,"ended")!==!0).length,$=R.filter((I)=>T(I,"readyState")==="ended"||T(I,"trackState")==="ended"||Z(I,"ended")===!0).length,S=A.reduce((I,h)=>I+(D(h,"packetsReceived")??0),0),O=_.reduce((I,h)=>I+(D(h,"packetsSent")??0),0),y=[...A,..._].reduce((I,h)=>I+Math.max(0,D(h,"packetsLost")??0),0),H=S+y,U=H===0?0:y/H,J=A.reduce((I,h)=>I+(D(h,"bytesReceived")??0),0),N=_.reduce((I,h)=>I+(D(h,"bytesSent")??0),0),W=Q(V.map((I)=>d(D(I,"currentRoundTripTime")??D(I,"roundTripTime"))).filter((I)=>I!==void 0)),E=Q([...A,..._].map((I)=>d(D(I,"jitter"))).filter((I)=>I!==void 0)),P=Q(A.map((I)=>{let h=D(I,"jitterBufferDelay"),G=D(I,"jitterBufferEmittedCount");return h!==void 0&&G!==void 0&&G>0?h/G*1000:void 0}).filter((I)=>I!==void 0)),b=R.map((I)=>D(I,"audioLevel")).filter((I)=>I!==void 0);if(c.requireConnectedCandidatePair&&V.length>0&&w===0)M(C,"error","media.webrtc_candidate_pair_missing","No active WebRTC candidate pair was observed.");if(c.requireLiveAudioTrack&&L===0)M(C,"error","media.webrtc_audio_track_missing","No live WebRTC audio track was observed.");if(c.maxPacketLossRatio!==void 0&&U>c.maxPacketLossRatio)M(C,"warning","media.webrtc_packet_loss",`Observed WebRTC packet loss ratio ${String(U)} above ${String(c.maxPacketLossRatio)}.`);if(c.maxRoundTripTimeMs!==void 0&&W!==void 0&&W>c.maxRoundTripTimeMs)M(C,"warning","media.webrtc_round_trip_time",`Observed WebRTC RTT ${String(W)}ms above ${String(c.maxRoundTripTimeMs)}ms.`);if(c.maxJitterMs!==void 0&&E!==void 0&&E>c.maxJitterMs)M(C,"warning","media.webrtc_jitter",`Observed WebRTC jitter ${String(E)}ms above ${String(c.maxJitterMs)}ms.`);return{activeCandidatePairs:w,audioLevelAverage:Hc(b),bytesReceived:J,bytesSent:N,checkedAt:Date.now(),endedAudioTracks:$,inboundPackets:S,issues:C,jitterBufferDelayMs:P,jitterMs:E,liveAudioTracks:L,outboundPackets:O,packetLossRatio:U,packetsLost:y,roundTripTimeMs:W,status:C.some((I)=>I.severity==="error")?"fail":C.length>0?"warn":"pass",totalStats:g.length}},F=async(c)=>{return[...(await c.peerConnection.getStats(c.selector??null)).values()].map(Gc)};var n=(c={})=>{let g=c.stats??[],C=c.previousStats??[],A=[],_=new Map(C.map((y)=>[j(y),y])),R=g.filter((y)=>(y.type==="inbound-rtp"||y.type==="outbound-rtp")&&T(y,"kind")!=="video"&&T(y,"mediaType")!=="video").map((y)=>{let H=y.type==="outbound-rtp"?"outbound":"inbound",U=H==="outbound"?"packetsSent":"packetsReceived",J=H==="outbound"?"bytesSent":"bytesReceived",N=_.get(j(y)),W=D(y,U),E=N?D(N,U):void 0,P=D(y,J),b=N?D(N,J):void 0,I=y.timestamp!==void 0&&N?.timestamp!==void 0?y.timestamp-N.timestamp:void 0;return{bytesDelta:P!==void 0&&b!==void 0?P-b:void 0,currentPackets:W,direction:H,id:j(y),packetDelta:W!==void 0&&E!==void 0?W-E:void 0,previousPackets:E,timeDeltaMs:I}}),w=R.filter((y)=>y.direction==="inbound"),L=R.filter((y)=>y.direction==="outbound"),$=Q(R.map((y)=>y.timeDeltaMs).filter((y)=>y!==void 0)),S=w.filter((y)=>c.maxInboundPacketStallMs!==void 0&&y.timeDeltaMs!==void 0&&y.timeDeltaMs>=c.maxInboundPacketStallMs&&y.packetDelta!==void 0&&y.packetDelta<=0).length,O=L.filter((y)=>c.maxOutboundPacketStallMs!==void 0&&y.timeDeltaMs!==void 0&&y.timeDeltaMs>=c.maxOutboundPacketStallMs&&y.packetDelta!==void 0&&y.packetDelta<=0).length;if(c.requireInboundAudio&&w.length===0)M(A,"error","media.webrtc_inbound_audio_missing","No inbound WebRTC audio RTP stream was observed.");if(c.requireOutboundAudio&&L.length===0)M(A,"error","media.webrtc_outbound_audio_missing","No outbound WebRTC audio RTP stream was observed.");if(c.maxGapMs!==void 0&&$!==void 0&&$>c.maxGapMs)M(A,"warning","media.webrtc_stream_gap",`Observed WebRTC stream sample gap ${String($)}ms above ${String(c.maxGapMs)}ms.`);if(S>0)M(A,"error","media.webrtc_inbound_stalled",`${String(S)} inbound WebRTC audio stream(s) stopped receiving packets.`);if(O>0)M(A,"error","media.webrtc_outbound_stalled",`${String(O)} outbound WebRTC audio stream(s) stopped sending packets.`);return{checkedAt:Date.now(),inboundAudioStreams:w.length,issues:A,maxObservedGapMs:$,outboundAudioStreams:L.length,stalledInboundStreams:S,stalledOutboundStreams:O,status:A.some((y)=>y.severity==="error")?"fail":A.length>0?"warn":"pass",streams:R,totalStats:g.length}};var Nc="/api/voice/browser-media",Wc=5000,Ec=async(c)=>c.peerConnection??await c.getPeerConnection?.()??null,Mc=async(c,g)=>{let C=g.fetch??globalThis.fetch;if(!C)return;await C(g.path??Nc,{body:JSON.stringify(c),headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"})},o=(c)=>{let g=null,C=[],A=async()=>{let R=await Ec(c);if(!R)return;let w=await F({peerConnection:R}),L=f({...c,stats:w}),$=c.continuity===!1?void 0:n({...c.continuity,previousStats:C,stats:w}),S={at:Date.now(),continuity:$,report:L,scenarioId:c.getScenarioId?.()??null,sessionId:c.getSessionId?.()??null};return C=w,c.onReport?.(S),await Mc(S,c),S},_=()=>{A().catch((R)=>{c.onError?.(R)})},V=()=>{if(g)clearInterval(g),g=null};return{close:V,reportOnce:A,stop:V,start:()=>{if(g)return;_(),g=setInterval(_,c.intervalMs??Wc)}}};var Y=()=>{},Xc=()=>Y,Yc={callControl:Y,close:Y,endTurn:Y,send:Y,sendAudio:Y,simulateDisconnect:Y,subscribe:Xc,getReadyState:()=>3,getScenarioId:()=>"",getSessionId:()=>"",start:()=>{}},Jc=()=>crypto.randomUUID(),Pc=(c,g,C)=>{let{hostname:A,port:_,protocol:V}=window.location,R=V==="https:"?"wss:":"ws:",w=_?`:${_}`:"",L=new URL(`${R}//${A}${w}${c}`);if(L.searchParams.set("sessionId",g),C)L.searchParams.set("scenarioId",C);return L.toString()},bc=(c)=>{if(!c||typeof c!=="object"||!("type"in c))return!1;switch(c.type){case"audio":case"assistant":case"call_lifecycle":case"complete":case"connection":case"error":case"final":case"partial":case"pong":case"replay":case"session":case"turn":return!0;default:return!1}},Zc=(c)=>{if(typeof c.data!=="string")return null;try{let g=JSON.parse(c.data);return bc(g)?g:null}catch{return null}},i=(c,g={})=>{if(typeof window>"u")return Yc;let C=new Set,A=g.reconnect!==!1,_=g.maxReconnectAttempts??10,V=g.pingInterval??30000,R={isConnected:!1,pendingMessages:[],scenarioId:g.scenarioId??null,pingInterval:null,reconnectAttempts:0,reconnectTimeout:null,sessionId:g.sessionId??Jc(),ws:null},w=(I)=>{C.forEach((h)=>h(I))},L=()=>{if(R.pingInterval)clearInterval(R.pingInterval),R.pingInterval=null;if(R.reconnectTimeout)clearTimeout(R.reconnectTimeout),R.reconnectTimeout=null},$=()=>{if(R.ws?.readyState!==1)return;while(R.pendingMessages.length>0){let I=R.pendingMessages.shift();if(I!==void 0)R.ws.send(I)}},S=()=>{let I=Date.now()+500;R.reconnectAttempts+=1,w({reconnect:{attempts:R.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:_,nextAttemptAt:I,status:"reconnecting"},type:"connection"}),R.reconnectTimeout=setTimeout(()=>{if(R.reconnectAttempts>_){w({reconnect:{attempts:R.reconnectAttempts,maxAttempts:_,status:"exhausted"},type:"connection"});return}O()},500)},O=()=>{let I=new WebSocket(Pc(c,R.sessionId,R.scenarioId));I.binaryType="arraybuffer",I.onopen=()=>{let h=R.reconnectAttempts>0;if(R.isConnected=!0,$(),h)w({reconnect:{attempts:R.reconnectAttempts,lastResumedAt:Date.now(),maxAttempts:_,status:"resumed"},type:"connection"}),R.reconnectAttempts=0;C.forEach((G)=>G({scenarioId:R.scenarioId??void 0,sessionId:R.sessionId,status:"active",type:"session"})),R.pingInterval=setInterval(()=>{if(I.readyState===1)I.send(JSON.stringify({type:"ping"}))},V)},I.onmessage=(h)=>{let G=Zc(h);if(!G)return;if(G.type==="session")R.sessionId=G.sessionId,R.scenarioId=G.scenarioId??R.scenarioId;C.forEach((Cc)=>Cc(G))},I.onclose=(h)=>{if(R.isConnected=!1,L(),A&&h.code!==1000&&R.reconnectAttempts<_)S();else if(A&&h.code!==1000)w({reconnect:{attempts:R.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:_,status:"exhausted"},type:"connection"})},R.ws=I},y=(I)=>{if(R.ws?.readyState===1){R.ws.send(I);return}R.pendingMessages.push(I)},H=(I)=>{y(JSON.stringify(I))},U=(I={})=>{if(I.sessionId)R.sessionId=I.sessionId;if(I.scenarioId)R.scenarioId=I.scenarioId;H({scenarioId:R.scenarioId??void 0,sessionId:R.sessionId,type:"start"})},J=(I)=>{y(I)},N=()=>{H({type:"end_turn"})},W=(I)=>{H({...I,type:"call_control"})},E=()=>{if(L(),R.ws)R.ws.close(1000),R.ws=null;R.isConnected=!1,C.clear()},P=()=>{if(R.ws?.readyState===1)R.ws.close(4000,"absolutejs-voice-reconnect-proof")},b=(I)=>{return C.add(I),()=>{C.delete(I)}};return O(),{callControl:W,close:E,endTurn:N,send:H,sendAudio:J,simulateDisconnect:P,start:U,subscribe:b,getReadyState:()=>R.ws?.readyState??3,getScenarioId:()=>R.scenarioId??"",getSessionId:()=>R.sessionId}};var Qc=()=>({attempts:0,maxAttempts:0,status:"idle"}),Kc=(c,g)=>{let C=g.trim().replace(/\s+/g," ");if(!C)return c;if(!c)return C;if(c===C||c.endsWith(C))return c;if(C.includes(c))return C;return`${c} ${C}`},qc=(c,g)=>{let C=g.trim().replace(/\s+/g," ");if(!c)return C;if(!C||c.endsWith(C))return c;return`${c} ${C}`},Bc=()=>({assistantAudio:[],assistantStreamingText:"",assistantTexts:[],call:null,error:null,isConnected:!1,partial:"",reconnect:Qc(),scenarioId:null,sessionId:null,sessionMetadata:null,status:"idle",turns:[]}),v=()=>{let c=Bc(),g="",C=new Set,A=()=>{C.forEach((V)=>V())};return{dispatch:(V)=>{switch(V.type){case"audio":c={...c,assistantAudio:[...c.assistantAudio,{chunk:V.chunk,format:V.format,receivedAt:V.receivedAt,turnId:V.turnId}]};break;case"assistant":c={...c,assistantStreamingText:"",assistantTexts:[...c.assistantTexts,V.text]};break;case"assistant_delta":c={...c,assistantStreamingText:`${c.assistantStreamingText}${V.delta}`};break;case"complete":c={...c,sessionId:V.sessionId,status:"completed"};break;case"call_lifecycle":c={...c,call:{...c.call,disposition:V.event.type==="end"?V.event.disposition:c.call?.disposition,endedAt:V.event.type==="end"?V.event.at:c.call?.endedAt,events:[...c.call?.events??[],V.event],lastEventAt:V.event.at,startedAt:c.call?.startedAt??V.event.at},sessionId:V.sessionId};break;case"connected":c={...c,isConnected:!0,reconnect:c.reconnect.status==="reconnecting"?{...c.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:c.reconnect};break;case"connection":c={...c,reconnect:V.reconnect};break;case"disconnected":c={...c,isConnected:!1};break;case"error":c={...c,error:V.message};break;case"final":g=Kc(g,V.transcript.text),c={...c,partial:g};break;case"partial":c={...c,partial:qc(g,V.transcript.text)};break;case"replay":g=V.partial,c={...c,assistantStreamingText:"",assistantTexts:[...V.assistantTexts],call:V.call??null,error:null,isConnected:V.status==="active",partial:V.partial,reconnect:c.reconnect.status==="reconnecting"?{...c.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:c.reconnect,scenarioId:V.scenarioId??c.scenarioId,sessionId:V.sessionId,sessionMetadata:V.sessionMetadata??c.sessionMetadata,status:V.status,turns:[...V.turns]};break;case"session":c={...c,error:null,scenarioId:V.scenarioId??c.scenarioId,isConnected:V.status==="active",sessionId:V.sessionId,sessionMetadata:V.sessionMetadata??c.sessionMetadata,status:V.status};break;case"turn":g="",c={...c,partial:"",turns:[...c.turns,V.turn]};break}A()},getServerSnapshot:()=>c,getSnapshot:()=>c,subscribe:(V)=>{return C.add(V),()=>{C.delete(V)}}}};var s=(c,g={})=>{let C=i(c,g),A=v(),_=g.browserMedia&&typeof window<"u"?o({...g.browserMedia,getScenarioId:()=>g.browserMedia?g.browserMedia.getScenarioId?.()??C.getScenarioId():C.getScenarioId(),getSessionId:()=>g.browserMedia?g.browserMedia.getSessionId?.()??C.getSessionId():C.getSessionId()}):null,V=new Set,R=(S)=>Promise.resolve().then(()=>{if(!S?.sessionId&&!S?.scenarioId)return;C.start(S),_?.start()}),w=()=>{V.forEach((S)=>S())},L=()=>{if(!g.reconnectReportPath||typeof fetch>"u")return;let S=A.getSnapshot(),O=JSON.stringify({at:Date.now(),reconnect:S.reconnect,scenarioId:S.scenarioId,sessionId:C.getSessionId(),turnIds:S.turns.map((y)=>y.id)});fetch(g.reconnectReportPath,{body:O,headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"}).catch(()=>{})},$=C.subscribe((S)=>{let O=x(S);if(O){if(A.dispatch(O),S.type==="connection")L();w()}});return{start:R,get assistantAudio(){return A.getSnapshot().assistantAudio},get assistantTexts(){return A.getSnapshot().assistantTexts},get assistantStreamingText(){return A.getSnapshot().assistantStreamingText},get call(){return A.getSnapshot().call},callControl(S){C.callControl(S)},close(){$(),_?.close(),C.close(),A.dispatch({type:"disconnected"}),w()},endTurn(){C.endTurn()},get error(){return A.getSnapshot().error},getServerSnapshot(){return A.getServerSnapshot()},getSnapshot(){return A.getSnapshot()},get isConnected(){return A.getSnapshot().isConnected},get partial(){return A.getSnapshot().partial},get reconnect(){return A.getSnapshot().reconnect},get scenarioId(){return A.getSnapshot().scenarioId},sendAudio(S){C.sendAudio(S)},get sessionId(){return C.getSessionId()},get sessionMetadata(){return A.getSnapshot().sessionMetadata},simulateDisconnect(){C.simulateDisconnect()},get status(){return A.getSnapshot().status},subscribe(S){return V.add(S),()=>{V.delete(S)}},get turns(){return A.getSnapshot().turns}}};var m=(c)=>{if(!c||c.enabled===!1)return;return{enabled:!0,maxGain:c.maxGain??3,noiseGateAttenuation:c.noiseGateAttenuation??0.15,noiseGateThreshold:c.noiseGateThreshold??0.006,targetLevel:c.targetLevel??0.08}};var K=1200;var jc={balanced:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:K,silenceMs:1400,speechThreshold:0.012,transcriptStabilityMs:1000},fast:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:K,silenceMs:700,speechThreshold:0.015,transcriptStabilityMs:450},"long-form":{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:K,silenceMs:2200,speechThreshold:0.01,transcriptStabilityMs:1500}},zc={"accent-heavy":{silenceMs:1200,speechThreshold:0.01,transcriptStabilityMs:1200},general:{},"noisy-room":{silenceMs:2000,speechThreshold:0.02,transcriptStabilityMs:1600},"short-command":{silenceMs:500,speechThreshold:0.016,transcriptStabilityMs:420}},kc="fast",lc="general",r=(c)=>{let g=c?.profile??kc,C=c?.qualityProfile??lc,A=jc[g],_=zc[C];return{profile:g,qualityProfile:C,semanticVetoMaxMs:c?.semanticVetoMaxMs??A.semanticVetoMaxMs,semanticVetoRecheckMs:c?.semanticVetoRecheckMs??A.semanticVetoRecheckMs,silenceMs:c?.silenceMs??_.silenceMs??A.silenceMs,speechThreshold:c?.speechThreshold??_.speechThreshold??A.speechThreshold,transcriptStabilityMs:c?.transcriptStabilityMs??_.transcriptStabilityMs??A.transcriptStabilityMs}};var xc={chat:{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"balanced",qualityProfile:"short-command"}},default:{capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"fast",qualityProfile:"general"}},dictation:{audioConditioning:{enabled:!0,maxGain:2.25,noiseGateAttenuation:0.05,noiseGateThreshold:0.003,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"guided-intake":{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"turn-scoped",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"noisy-room":{audioConditioning:{enabled:!0,maxGain:3,noiseGateAttenuation:0.12,noiseGateThreshold:0.006,targetLevel:0.085},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:2100,speechThreshold:0.02,transcriptStabilityMs:1650}},"pstn-balanced":{audioConditioning:{enabled:!0,maxGain:2.8,noiseGateAttenuation:0.07,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:660,speechThreshold:0.012,transcriptStabilityMs:300}},"pstn-fast":{audioConditioning:{enabled:!0,maxGain:2.75,noiseGateAttenuation:0.06,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:620,speechThreshold:0.012,transcriptStabilityMs:280}},reliability:{audioConditioning:{enabled:!0,maxGain:2.9,noiseGateAttenuation:0.08,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room"}}},a=(c="default")=>{let g=xc[c];return{audioConditioning:m(g.audioConditioning),capture:{channelCount:g.capture?.channelCount??1,sampleRateHz:g.capture?.sampleRateHz??16000},connection:{...g.connection},name:c,sttLifecycle:g.sttLifecycle??"continuous",turnDetection:r(g.turnDetection)}};var dc=(c)=>({assistantAudio:[...c.assistantAudio],assistantStreamingText:c.assistantStreamingText,assistantTexts:[...c.assistantTexts],call:c.call,error:c.error,isConnected:c.isConnected,isRecording:!1,partial:c.partial,reconnect:c.reconnect,recordingError:null,sessionId:c.sessionId,sessionMetadata:c.sessionMetadata,scenarioId:c.scenarioId,status:c.status,turns:[...c.turns]}),u=(c,g={})=>{let C=a(g.preset),A=s(c,{...C.connection,...g.connection}),_=null,V=dc(A),R=new Set,w=()=>{for(let U of R)U()},L=()=>{if(V={...V,assistantAudio:[...A.assistantAudio],assistantStreamingText:A.assistantStreamingText,assistantTexts:[...A.assistantTexts],call:A.call,error:A.error,isConnected:A.isConnected,partial:A.partial,reconnect:A.reconnect,sessionId:A.sessionId,sessionMetadata:A.sessionMetadata,scenarioId:A.scenarioId,status:A.status,turns:[...A.turns]},g.autoStopOnComplete!==!1&&V.status==="completed"&&V.isRecording)_?.stop(),_=null,V={...V,isRecording:!1};w()},$=A.subscribe(L);L();let S=()=>{if(_)return _;return _=l({channelCount:g.capture?.channelCount??C.capture.channelCount,onLevel:g.capture?.onLevel,onAudio:(U)=>{if(g.capture?.onAudio){g.capture.onAudio(U,A.sendAudio);return}A.sendAudio(U)},sampleRateHz:g.capture?.sampleRateHz??C.capture.sampleRateHz,...g.capture?.stream?{stream:g.capture.stream}:{}}),_},O=()=>{_?.stop(),_=null,V={...V,isRecording:!1},w()},y=async()=>{if(V.isRecording)return;try{V={...V,recordingError:null},w(),await S().start(),V={...V,isRecording:!0},w()}catch(U){throw _=null,V={...V,isRecording:!1,recordingError:U instanceof Error?U.message:String(U)},w(),U}};return{close:()=>{$(),O(),A.close()},startRecording:y,stopRecording:O,get assistantAudio(){return V.assistantAudio},get assistantTexts(){return V.assistantTexts},get assistantStreamingText(){return V.assistantStreamingText},bindHTMX(U){return k(A,U)},get call(){return V.call},callControl:(U)=>A.callControl(U),endTurn:()=>A.endTurn(),get error(){return V.error},getServerSnapshot:()=>V,getSnapshot:()=>V,get isConnected(){return V.isConnected},get isRecording(){return V.isRecording},get partial(){return V.partial},get reconnect(){return V.reconnect},get recordingError(){return V.recordingError},get scenarioId(){return V.scenarioId},sendAudio:(U)=>A.sendAudio(U),get sessionId(){return V.sessionId},get sessionMetadata(){return V.sessionMetadata},simulateDisconnect:()=>A.simulateDisconnect(),get status(){return V.status},subscribe:(U)=>{return R.add(U),()=>{R.delete(U)}},toggleRecording:async()=>{if(V.isRecording){O();return}await y()},get turns(){return V.turns}}};var X=(c)=>String(c).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;");var e=(c)=>{if(!c.isConnected)return"idle";if(c.isPlaying)return"speaking";if(c.isRecording&&c.hasActivePartial)return"listening";if(c.isRecording)return"listening";if(c.lastTranscriptAt&&!c.lastAssistantAt)return"thinking";if(c.lastTranscriptAt&&c.lastAssistantAt&&c.lastTranscriptAt>c.lastAssistantAt)return"thinking";return"idle"};var fc={accent:"#3b82f6",background:"#0f172a",errorAccent:"#ef4444",fontFamily:'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',foreground:"#f8fafc",radius:16},Fc={callEnded:"Call ended",connecting:"Connecting…",endCall:"End call",idle:"Idle",listening:"Listening",mute:"Mute",speaking:"Speaking",startCall:"Start call",thinking:"Thinking",unmute:"Unmute"},nc=(c,g)=>{switch(c){case"listening":return g.listening;case"speaking":return g.speaking;case"thinking":return g.thinking;case"idle":return g.idle}},p=(c)=>{let g={...fc,...c.theme},C={...Fc,...c.labels},A=c.state.assistantAudio.at(-1)?.receivedAt,_=c.state.turns.at(-1)?.committedAt,V=e({hasActivePartial:c.state.partial.length>0,isConnected:c.state.isConnected,isPlaying:!1,isRecording:c.state.isRecording,lastAssistantAt:A,lastTranscriptAt:_}),R=!c.state.isConnected&&c.state.status!=="idle"&&!c.state.error,w=c.state.error?"Error":R?C.connecting:c.state.status==="completed"?C.callEnded:nc(V,C);return{agentState:V,classes:{container:`absolute-voice-widget absolute-voice-widget--${V}`,dot:`absolute-voice-widget__dot${c.state.error?" absolute-voice-widget__dot--error":""}`},controls:{canEnd:c.state.isConnected,canMute:c.state.isRecording,canStart:!c.state.isRecording&&c.state.status!=="completed"},errorMessage:c.state.error??void 0,labels:C,partial:c.state.partial||void 0,statusLabel:w,theme:g,title:c.title??"Voice"}},oc=(c)=>typeof c==="number"?`${c}px`:c,t=(c)=>{let g=c.theme,C=`background:${g.background};border-radius:${oc(g.radius)};color:${g.foreground};font-family:${g.fontFamily};min-width:240px;padding:20px 22px;`,A=`background:${c.errorMessage?g.errorAccent:c.agentState==="idle"?"rgba(148,163,184,0.6)":g.accent};border-radius:50%;height:10px;width:10px;`,_=[];if(c.controls.canStart)_.push(`<button type="button" data-action="start" style="background:${g.accent};border:none;border-radius:12px;color:${g.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${X(c.labels.startCall)}</button>`);if(c.controls.canMute)_.push(`<button type="button" data-action="mute" style="background:transparent;border:1px solid rgba(255,255,255,0.18);border-radius:12px;color:${g.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${X(c.labels.mute)}</button>`);if(c.controls.canEnd)_.push(`<button type="button" data-action="end" style="background:${g.errorAccent};border:none;border-radius:12px;color:${g.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${X(c.labels.endCall)}</button>`);return`<div role="region" aria-live="polite" data-agent-state="${c.agentState}" class="${X(c.classes.container)}" style="${C}">
1
+ (()=>{var{defineProperty:E,getOwnPropertyNames:I0,getOwnPropertyDescriptor:H0}=Object,_0=Object.prototype.hasOwnProperty;function G0(g){return this[g]}var L0=(g)=>{var A=(x??=new WeakMap).get(g),C;if(A)return A;if(A=E({},"__esModule",{value:!0}),g&&typeof g==="object"||typeof g==="function"){for(var V of I0(g))if(!_0.call(A,V))E(A,V,{get:G0.bind(g,V),enumerable:!(C=H0(g,V))||C.enumerable})}return x.set(g,A),A},x;var U0=(g)=>g;function w0(g,A){this[g]=U0.bind(null,A)}var y0=(g,A)=>{for(var C in A)E(g,C,{get:A[C],enumerable:!0,configurable:!0,set:w0.bind(A,C)})};var r0={};y0(r0,{mount:()=>V0,default:()=>s0,VOICE_EMBED_VERSION:()=>C0});var X0=(g)=>{if(typeof g!=="string")return g;return document.querySelector(g)},h0=(g,A,C,V)=>{let I=A??g.getAttribute("hx-get")??"";if(!I)return"";let R=new URL(I,window.location.origin);if(V)R.searchParams.set(C,V);else R.searchParams.delete(C);return`${R.pathname}${R.search}${R.hash}`},F=(g,A)=>{if(typeof window>"u"||typeof document>"u")return()=>{};let C=X0(A.element);if(!C)return()=>{};let V=A.eventName??"voice-refresh",I=A.sessionQueryParam??"sessionId",R=()=>{let L=window,$=h0(C,A.route,I,g.sessionId);if($)C.setAttribute("hx-get",$);L.htmx?.process?.(C),L.htmx?.trigger?.(C,V)},w=g.subscribe(R);return R(),()=>{w()}};var D0=(g)=>Math.max(-1,Math.min(1,g)),J0=(g)=>{let A=new Int16Array(g.length);for(let C=0;C<g.length;C+=1){let V=D0(g[C]??0);A[C]=V<0?V*32768:V*32767}return new Uint8Array(A.buffer)},Y0=(g)=>{let A=g instanceof Uint8Array?g:new Uint8Array(g);if(A.byteLength<2)return 0;let C=new Int16Array(A.buffer,A.byteOffset,Math.floor(A.byteLength/2));if(C.length===0)return 0;let V=0;for(let I of C){let R=I/32768;V+=R*R}return Math.min(1,Math.max(0,Math.sqrt(V/C.length)*5.5))},Z0=(g,A,C)=>{if(A===C)return g;let V=A/C,I=Math.round(g.length/V),R=new Float32Array(I),w=0,L=0;while(w<R.length){let $=Math.round((w+1)*V),X=0,G=0;for(let h=L;h<$&&h<g.length;h+=1)X+=g[h]??0,G+=1;R[w]=G>0?X/G:0,w+=1,L=$}return R},f=(g)=>{let A=null,C=null,V=null,I=null;return{start:async()=>{if(!g.stream&&(typeof navigator>"u"||!navigator.mediaDevices?.getUserMedia))throw Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");let L=(typeof window<"u"?window.AudioContext??window.webkitAudioContext:void 0)??AudioContext;if(!L)throw Error("Browser microphone capture requires AudioContext support.");if(I=g.stream??await navigator.mediaDevices.getUserMedia({audio:{autoGainControl:!0,channelCount:g.channelCount??1,echoCancellation:!0,noiseSuppression:!0}}),A=new L,A.state==="suspended")await A.resume();C=A.createMediaStreamSource(I),V=A.createScriptProcessor(4096,1,1),V.onaudioprocess=($)=>{let X=$.inputBuffer.getChannelData(0),G=Z0(X,A?.sampleRate??48000,g.sampleRateHz??16000),h=J0(G);g.onLevel?.(Y0(h)),g.onAudio(h)},C.connect(V),V.connect(A.destination)},stop:()=>{V?.disconnect(),C?.disconnect(),I?.getTracks().forEach((L)=>L.stop()),A?.close(),g.onLevel?.(0),A=null,I=null,V=null,C=null}}};var M=(g)=>{if(typeof g==="string"&&g.trim())return g;if(g instanceof Error&&g.message.trim())return g.message;if(g&&typeof g==="object"){let A=g;for(let C of["message","reason","description"]){let V=A[C];if(typeof V==="string"&&V.trim())return V}if("error"in A)return M(A.error);if("cause"in A)return M(A.cause);try{return JSON.stringify(g)}catch{}}return"Unexpected error"},l=(g)=>{switch(g.type){case"audio":return{chunk:Uint8Array.from(atob(g.chunkBase64),(A)=>A.charCodeAt(0)),format:g.format,receivedAt:g.receivedAt,turnId:g.turnId,type:"audio"};case"assistant":return{text:g.text,turnId:g.turnId,type:"assistant"};case"assistant_delta":return{delta:g.delta,turnId:g.turnId,type:"assistant_delta"};case"complete":return{sessionId:g.sessionId,type:"complete"};case"connection":return{reconnect:g.reconnect,type:"connection"};case"call_lifecycle":return{event:g.event,sessionId:g.sessionId,type:"call_lifecycle"};case"error":return{message:M(g.message),type:"error"};case"final":return{transcript:g.transcript,type:"final"};case"partial":return{transcript:g.transcript,type:"partial"};case"replay":return{assistantTexts:g.assistantTexts,call:g.call,partial:g.partial,scenarioId:g.scenarioId,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,status:g.status,turns:g.turns,type:"replay"};case"session":return{sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,type:"session"};case"turn":return{turn:g.turn,type:"turn"};default:return null}};var gg=Math.PI*2;var T=(g,A,C,V)=>{g.push({code:C,message:V,severity:A})};var O0=(g)=>g.length===0?void 0:g.reduce((A,C)=>A+C,0)/g.length,z=(g)=>g.length===0?void 0:Math.max(...g);var D=(g,A)=>{let C=g[A];return typeof C==="number"&&Number.isFinite(C)?C:void 0},B=(g,A)=>{let C=g[A];return typeof C==="boolean"?C:void 0},Y=(g,A)=>{let C=g[A];return typeof C==="string"?C:void 0},k=(g)=>String(g.id??Y(g,"ssrc")??D(g,"ssrc")??Y(g,"trackIdentifier")??Y(g,"mid")??"unknown"),d=(g)=>g===void 0?void 0:g*1000;var S0=(g)=>{let A={};for(let[C,V]of Object.entries(g))if(V===null||typeof V==="boolean"||typeof V==="number"||typeof V==="string")A[C]=V;return A};var v=(g={})=>{let A=g.stats??[],C=[],V=A.filter((H)=>H.type==="inbound-rtp"&&Y(H,"kind")!=="video"),I=A.filter((H)=>H.type==="outbound-rtp"&&Y(H,"kind")!=="video"),R=A.filter((H)=>H.type==="candidate-pair"),w=A.filter((H)=>(H.type==="track"||H.type==="media-source")&&Y(H,"kind")==="audio"),L=R.filter((H)=>B(H,"selected")===!0||B(H,"nominated")===!0||Y(H,"state")==="succeeded").length,$=w.filter((H)=>Y(H,"readyState")!=="ended"&&Y(H,"trackState")!=="ended"&&B(H,"ended")!==!0).length,X=w.filter((H)=>Y(H,"readyState")==="ended"||Y(H,"trackState")==="ended"||B(H,"ended")===!0).length,G=V.reduce((H,J)=>H+(D(J,"packetsReceived")??0),0),h=I.reduce((H,J)=>H+(D(J,"packetsSent")??0),0),_=[...V,...I].reduce((H,J)=>H+Math.max(0,D(J,"packetsLost")??0),0),Z=G+_,y=Z===0?0:_/Z,K=V.reduce((H,J)=>H+(D(J,"bytesReceived")??0),0),O=I.reduce((H,J)=>H+(D(J,"bytesSent")??0),0),S=z(R.map((H)=>d(D(H,"currentRoundTripTime")??D(H,"roundTripTime"))).filter((H)=>H!==void 0)),W=z([...V,...I].map((H)=>d(D(H,"jitter"))).filter((H)=>H!==void 0)),N=z(V.map((H)=>{let J=D(H,"jitterBufferDelay"),U=D(H,"jitterBufferEmittedCount");return J!==void 0&&U!==void 0&&U>0?J/U*1000:void 0}).filter((H)=>H!==void 0)),j=w.map((H)=>D(H,"audioLevel")).filter((H)=>H!==void 0);if(g.requireConnectedCandidatePair&&R.length>0&&L===0)T(C,"error","media.webrtc_candidate_pair_missing","No active WebRTC candidate pair was observed.");if(g.requireLiveAudioTrack&&$===0)T(C,"error","media.webrtc_audio_track_missing","No live WebRTC audio track was observed.");if(g.maxPacketLossRatio!==void 0&&y>g.maxPacketLossRatio)T(C,"warning","media.webrtc_packet_loss",`Observed WebRTC packet loss ratio ${String(y)} above ${String(g.maxPacketLossRatio)}.`);if(g.maxRoundTripTimeMs!==void 0&&S!==void 0&&S>g.maxRoundTripTimeMs)T(C,"warning","media.webrtc_round_trip_time",`Observed WebRTC RTT ${String(S)}ms above ${String(g.maxRoundTripTimeMs)}ms.`);if(g.maxJitterMs!==void 0&&W!==void 0&&W>g.maxJitterMs)T(C,"warning","media.webrtc_jitter",`Observed WebRTC jitter ${String(W)}ms above ${String(g.maxJitterMs)}ms.`);return{activeCandidatePairs:L,audioLevelAverage:O0(j),bytesReceived:K,bytesSent:O,checkedAt:Date.now(),endedAudioTracks:X,inboundPackets:G,issues:C,jitterBufferDelayMs:N,jitterMs:W,liveAudioTracks:$,outboundPackets:h,packetLossRatio:y,packetsLost:_,roundTripTimeMs:S,status:C.some((H)=>H.severity==="error")?"fail":C.length>0?"warn":"pass",totalStats:A.length}},i=async(g)=>{return[...(await g.peerConnection.getStats(g.selector??null)).values()].map(S0)};var o=(g={})=>{let A=g.stats??[],C=g.previousStats??[],V=[],I=new Map(C.map((_)=>[k(_),_])),w=A.filter((_)=>(_.type==="inbound-rtp"||_.type==="outbound-rtp")&&Y(_,"kind")!=="video"&&Y(_,"mediaType")!=="video").map((_)=>{let Z=_.type==="outbound-rtp"?"outbound":"inbound",y=Z==="outbound"?"packetsSent":"packetsReceived",K=Z==="outbound"?"bytesSent":"bytesReceived",O=I.get(k(_)),S=D(_,y),W=O?D(O,y):void 0,N=D(_,K),j=O?D(O,K):void 0,H=_.timestamp!==void 0&&O?.timestamp!==void 0?_.timestamp-O.timestamp:void 0;return{bytesDelta:N!==void 0&&j!==void 0?N-j:void 0,currentPackets:S,direction:Z,id:k(_),packetDelta:S!==void 0&&W!==void 0?S-W:void 0,previousPackets:W,timeDeltaMs:H}}),L=w.filter((_)=>_.direction==="inbound"),$=w.filter((_)=>_.direction==="outbound"),X=z(w.map((_)=>_.timeDeltaMs).filter((_)=>_!==void 0)),G=L.filter((_)=>g.maxInboundPacketStallMs!==void 0&&_.timeDeltaMs!==void 0&&_.timeDeltaMs>=g.maxInboundPacketStallMs&&_.packetDelta!==void 0&&_.packetDelta<=0).length,h=$.filter((_)=>g.maxOutboundPacketStallMs!==void 0&&_.timeDeltaMs!==void 0&&_.timeDeltaMs>=g.maxOutboundPacketStallMs&&_.packetDelta!==void 0&&_.packetDelta<=0).length;if(g.requireInboundAudio&&L.length===0)T(V,"error","media.webrtc_inbound_audio_missing","No inbound WebRTC audio RTP stream was observed.");if(g.requireOutboundAudio&&$.length===0)T(V,"error","media.webrtc_outbound_audio_missing","No outbound WebRTC audio RTP stream was observed.");if(g.maxGapMs!==void 0&&X!==void 0&&X>g.maxGapMs)T(V,"warning","media.webrtc_stream_gap",`Observed WebRTC stream sample gap ${String(X)}ms above ${String(g.maxGapMs)}ms.`);if(G>0)T(V,"error","media.webrtc_inbound_stalled",`${String(G)} inbound WebRTC audio stream(s) stopped receiving packets.`);if(h>0)T(V,"error","media.webrtc_outbound_stalled",`${String(h)} outbound WebRTC audio stream(s) stopped sending packets.`);return{checkedAt:Date.now(),inboundAudioStreams:L.length,issues:V,maxObservedGapMs:X,outboundAudioStreams:$.length,stalledInboundStreams:G,stalledOutboundStreams:h,status:V.some((_)=>_.severity==="error")?"fail":V.length>0?"warn":"pass",streams:w,totalStats:A.length}};var W0="/api/voice/browser-media",Q0=5000,T0=async(g)=>g.peerConnection??await g.getPeerConnection?.()??null,c0=async(g,A)=>{let C=A.fetch??globalThis.fetch;if(!C)return;await C(A.path??W0,{body:JSON.stringify(g),headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"})},n=(g)=>{let A=null,C=[],V=async()=>{let w=await T0(g);if(!w)return;let L=await i({peerConnection:w}),$=v({...g,stats:L}),X=g.continuity===!1?void 0:o({...g.continuity,previousStats:C,stats:L}),G={at:Date.now(),continuity:X,report:$,scenarioId:g.getScenarioId?.()??null,sessionId:g.getSessionId?.()??null};return C=L,g.onReport?.(G),await c0(G,g),G},I=()=>{V().catch((w)=>{g.onError?.(w)})},R=()=>{if(A)clearInterval(A),A=null};return{close:R,reportOnce:V,stop:R,start:()=>{if(A)return;I(),A=setInterval(I,g.intervalMs??Q0)}}};var K0=(g,A,C)=>Math.min(C,A*2**(Math.max(1,g)-1));var q=()=>{},P0=()=>q,q0={callControl:q,close:q,endTurn:q,send:q,sendAudio:q,simulateDisconnect:q,subscribe:P0,getReadyState:()=>3,getScenarioId:()=>"",getSessionId:()=>"",start:()=>{}},N0=()=>crypto.randomUUID(),j0=(g,A,C)=>{let{hostname:V,port:I,protocol:R}=window.location,w=R==="https:"?"wss:":"ws:",L=I?`:${I}`:"",$=new URL(`${w}//${V}${L}${g}`);if($.searchParams.set("sessionId",A),C)$.searchParams.set("scenarioId",C);return $.toString()},B0=(g)=>{if(!g||typeof g!=="object"||!("type"in g))return!1;switch(g.type){case"audio":case"assistant":case"call_lifecycle":case"complete":case"connection":case"error":case"final":case"partial":case"pong":case"replay":case"session":case"turn":return!0;default:return!1}},z0=(g)=>{if(typeof g.data!=="string")return null;try{let A=JSON.parse(g.data);return B0(A)?A:null}catch{return null}},m=(g,A={})=>{if(typeof window>"u")return q0;let C=new Set,V=A.reconnect!==!1,I=A.maxReconnectAttempts??15,R=A.reconnectMaxDelayMs??8000,w=A.pingInterval??30000,L=(U)=>K0(U,500,R),$={isConnected:!1,pendingMessages:[],scenarioId:A.scenarioId??null,pingInterval:null,reconnectAttempts:0,reconnectTimeout:null,sessionId:A.sessionId??N0(),ws:null},X=(U)=>{C.forEach((Q)=>Q(U))},G=()=>{if($.pingInterval)clearInterval($.pingInterval),$.pingInterval=null;if($.reconnectTimeout)clearTimeout($.reconnectTimeout),$.reconnectTimeout=null},h=()=>{if($.ws?.readyState!==1)return;while($.pendingMessages.length>0){let U=$.pendingMessages.shift();if(U!==void 0)$.ws.send(U)}},_=()=>{$.reconnectAttempts+=1;let U=L($.reconnectAttempts),Q=Date.now()+U;X({reconnect:{attempts:$.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:I,nextAttemptAt:Q,status:"reconnecting"},type:"connection"}),$.reconnectTimeout=setTimeout(()=>{if($.reconnectAttempts>I){X({reconnect:{attempts:$.reconnectAttempts,maxAttempts:I,status:"exhausted"},type:"connection"});return}Z()},U)},Z=()=>{let U=new WebSocket(j0(g,$.sessionId,$.scenarioId));U.binaryType="arraybuffer",U.onopen=()=>{let Q=$.reconnectAttempts>0;if($.isConnected=!0,h(),Q)X({reconnect:{attempts:$.reconnectAttempts,lastResumedAt:Date.now(),maxAttempts:I,status:"resumed"},type:"connection"}),$.reconnectAttempts=0;C.forEach((P)=>P({scenarioId:$.scenarioId??void 0,sessionId:$.sessionId,status:"active",type:"session"})),$.pingInterval=setInterval(()=>{if(U.readyState===1)U.send(JSON.stringify({type:"ping"}))},w)},U.onmessage=(Q)=>{let P=z0(Q);if(!P)return;if(P.type==="session")$.sessionId=P.sessionId,$.scenarioId=P.scenarioId??$.scenarioId;C.forEach(($0)=>$0(P))},U.onclose=(Q)=>{if($.isConnected=!1,G(),V&&Q.code!==1000&&$.reconnectAttempts<I)_();else if(V&&Q.code!==1000)X({reconnect:{attempts:$.reconnectAttempts,lastDisconnectAt:Date.now(),maxAttempts:I,status:"exhausted"},type:"connection"})},$.ws=U},y=(U)=>{if($.ws?.readyState===1){$.ws.send(U);return}$.pendingMessages.push(U)},K=(U)=>{y(JSON.stringify(U))},O=(U={})=>{if(U.sessionId)$.sessionId=U.sessionId;if(U.scenarioId)$.scenarioId=U.scenarioId;K({scenarioId:$.scenarioId??void 0,sessionId:$.sessionId,type:"start"})},S=(U)=>{y(U)},W=()=>{K({type:"end_turn"})},N=(U)=>{K({...U,type:"call_control"})},j=()=>{if(G(),$.ws)$.ws.close(1000),$.ws=null;$.isConnected=!1,C.clear()},H=()=>{if($.ws?.readyState===1)$.ws.close(4000,"absolutejs-voice-reconnect-proof")},J=(U)=>{return C.add(U),()=>{C.delete(U)}};return Z(),{callControl:N,close:j,endTurn:W,send:K,sendAudio:S,simulateDisconnect:H,start:O,subscribe:J,getReadyState:()=>$.ws?.readyState??3,getScenarioId:()=>$.scenarioId??"",getSessionId:()=>$.sessionId}};var b0=()=>({attempts:0,maxAttempts:0,status:"idle"}),E0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!C)return g;if(!g)return C;if(g===C||g.endsWith(C))return g;if(C.includes(g))return C;return`${g} ${C}`},M0=(g,A)=>{let C=A.trim().replace(/\s+/g," ");if(!g)return C;if(!C||g.endsWith(C))return g;return`${g} ${C}`},k0=()=>({assistantAudio:[],assistantStreamingText:"",assistantTexts:[],call:null,error:null,isConnected:!1,partial:"",reconnect:b0(),scenarioId:null,sessionId:null,sessionMetadata:null,status:"idle",turns:[]}),u=()=>{let g=k0(),A="",C=new Set,V=()=>{C.forEach((R)=>R())};return{dispatch:(R)=>{switch(R.type){case"audio":g={...g,assistantAudio:[...g.assistantAudio,{chunk:R.chunk,format:R.format,receivedAt:R.receivedAt,turnId:R.turnId}]};break;case"assistant":g={...g,assistantStreamingText:"",assistantTexts:[...g.assistantTexts,R.text]};break;case"assistant_delta":g={...g,assistantStreamingText:`${g.assistantStreamingText}${R.delta}`};break;case"complete":g={...g,sessionId:R.sessionId,status:"completed"};break;case"call_lifecycle":g={...g,call:{...g.call,disposition:R.event.type==="end"?R.event.disposition:g.call?.disposition,endedAt:R.event.type==="end"?R.event.at:g.call?.endedAt,events:[...g.call?.events??[],R.event],lastEventAt:R.event.at,startedAt:g.call?.startedAt??R.event.at},sessionId:R.sessionId};break;case"connected":g={...g,isConnected:!0,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect};break;case"connection":g={...g,reconnect:R.reconnect};break;case"disconnected":g={...g,isConnected:!1};break;case"error":g={...g,error:R.message};break;case"final":A=E0(A,R.transcript.text),g={...g,partial:A};break;case"partial":g={...g,partial:M0(A,R.transcript.text)};break;case"replay":A=R.partial,g={...g,assistantStreamingText:"",assistantTexts:[...R.assistantTexts],call:R.call??null,error:null,isConnected:R.status==="active",partial:R.partial,reconnect:g.reconnect.status==="reconnecting"?{...g.reconnect,lastResumedAt:Date.now(),nextAttemptAt:void 0,status:"resumed"}:g.reconnect,scenarioId:R.scenarioId??g.scenarioId,sessionId:R.sessionId,sessionMetadata:R.sessionMetadata??g.sessionMetadata,status:R.status,turns:[...R.turns]};break;case"session":g={...g,error:null,scenarioId:R.scenarioId??g.scenarioId,isConnected:R.status==="active",sessionId:R.sessionId,sessionMetadata:R.sessionMetadata??g.sessionMetadata,status:R.status};break;case"turn":A="",g={...g,partial:"",turns:[...g.turns,R.turn]};break}V()},getServerSnapshot:()=>g,getSnapshot:()=>g,subscribe:(R)=>{return C.add(R),()=>{C.delete(R)}}}};var r=(g,A={})=>{let C=m(g,A),V=u(),I=A.browserMedia&&typeof window<"u"?n({...A.browserMedia,getScenarioId:()=>A.browserMedia?A.browserMedia.getScenarioId?.()??C.getScenarioId():C.getScenarioId(),getSessionId:()=>A.browserMedia?A.browserMedia.getSessionId?.()??C.getSessionId():C.getSessionId()}):null,R=new Set,w=(G)=>Promise.resolve().then(()=>{if(!G?.sessionId&&!G?.scenarioId)return;C.start(G),I?.start()}),L=()=>{R.forEach((G)=>G())},$=()=>{if(!A.reconnectReportPath||typeof fetch>"u")return;let G=V.getSnapshot(),h=JSON.stringify({at:Date.now(),reconnect:G.reconnect,scenarioId:G.scenarioId,sessionId:C.getSessionId(),turnIds:G.turns.map((_)=>_.id)});fetch(A.reconnectReportPath,{body:h,headers:{"Content-Type":"application/json"},keepalive:!0,method:"POST"}).catch(()=>{})},X=C.subscribe((G)=>{let h=l(G);if(h){if(V.dispatch(h),G.type==="connection")$();L()}});return{start:w,get assistantAudio(){return V.getSnapshot().assistantAudio},get assistantTexts(){return V.getSnapshot().assistantTexts},get assistantStreamingText(){return V.getSnapshot().assistantStreamingText},get call(){return V.getSnapshot().call},callControl(G){C.callControl(G)},close(){X(),I?.close(),C.close(),V.dispatch({type:"disconnected"}),L()},endTurn(){C.endTurn()},get error(){return V.getSnapshot().error},getServerSnapshot(){return V.getServerSnapshot()},getSnapshot(){return V.getSnapshot()},get isConnected(){return V.getSnapshot().isConnected},get partial(){return V.getSnapshot().partial},get reconnect(){return V.getSnapshot().reconnect},get scenarioId(){return V.getSnapshot().scenarioId},sendAudio(G){C.sendAudio(G)},get sessionId(){return C.getSessionId()},get sessionMetadata(){return V.getSnapshot().sessionMetadata},simulateDisconnect(){C.simulateDisconnect()},get status(){return V.getSnapshot().status},subscribe(G){return R.add(G),()=>{R.delete(G)}},get turns(){return V.getSnapshot().turns}}};var s=(g)=>{if(!g||g.enabled===!1)return;return{enabled:!0,maxGain:g.maxGain??3,noiseGateAttenuation:g.noiseGateAttenuation??0.15,noiseGateThreshold:g.noiseGateThreshold??0.006,targetLevel:g.targetLevel??0.08}};var b=1200;var x0={balanced:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:b,silenceMs:1400,speechThreshold:0.012,transcriptStabilityMs:1000},fast:{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:b,silenceMs:700,speechThreshold:0.015,transcriptStabilityMs:450},"long-form":{qualityProfile:"general",semanticVetoMaxMs:0,semanticVetoRecheckMs:b,silenceMs:2200,speechThreshold:0.01,transcriptStabilityMs:1500}},F0={"accent-heavy":{silenceMs:1200,speechThreshold:0.01,transcriptStabilityMs:1200},general:{},"noisy-room":{silenceMs:2000,speechThreshold:0.02,transcriptStabilityMs:1600},"short-command":{silenceMs:500,speechThreshold:0.016,transcriptStabilityMs:420}},f0="fast",l0="general",a=(g)=>{let A=g?.profile??f0,C=g?.qualityProfile??l0,V=x0[A],I=F0[C];return{profile:A,qualityProfile:C,semanticVetoMaxMs:g?.semanticVetoMaxMs??V.semanticVetoMaxMs,semanticVetoRecheckMs:g?.semanticVetoRecheckMs??V.semanticVetoRecheckMs,silenceMs:g?.silenceMs??I.silenceMs??V.silenceMs,speechThreshold:g?.speechThreshold??I.speechThreshold??V.speechThreshold,transcriptStabilityMs:g?.transcriptStabilityMs??I.transcriptStabilityMs??V.transcriptStabilityMs}};var d0={chat:{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"balanced",qualityProfile:"short-command"}},default:{capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:10,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"fast",qualityProfile:"general"}},dictation:{audioConditioning:{enabled:!0,maxGain:2.25,noiseGateAttenuation:0.05,noiseGateThreshold:0.003,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"guided-intake":{audioConditioning:{enabled:!0,maxGain:2.5,noiseGateAttenuation:0,noiseGateThreshold:0.004,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:12,pingInterval:30000,reconnect:!0},sttLifecycle:"turn-scoped",turnDetection:{profile:"long-form",qualityProfile:"accent-heavy"}},"noisy-room":{audioConditioning:{enabled:!0,maxGain:3,noiseGateAttenuation:0.12,noiseGateThreshold:0.006,targetLevel:0.085},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:2100,speechThreshold:0.02,transcriptStabilityMs:1650}},"pstn-balanced":{audioConditioning:{enabled:!0,maxGain:2.8,noiseGateAttenuation:0.07,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:660,speechThreshold:0.012,transcriptStabilityMs:300}},"pstn-fast":{audioConditioning:{enabled:!0,maxGain:2.75,noiseGateAttenuation:0.06,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room",silenceMs:620,speechThreshold:0.012,transcriptStabilityMs:280}},reliability:{audioConditioning:{enabled:!0,maxGain:2.9,noiseGateAttenuation:0.08,noiseGateThreshold:0.005,targetLevel:0.08},capture:{channelCount:1,sampleRateHz:16000},connection:{maxReconnectAttempts:14,pingInterval:45000,reconnect:!0},sttLifecycle:"continuous",turnDetection:{profile:"long-form",qualityProfile:"noisy-room"}}},p=(g="default")=>{let A=d0[g];return{audioConditioning:s(A.audioConditioning),capture:{channelCount:A.capture?.channelCount??1,sampleRateHz:A.capture?.sampleRateHz??16000},connection:{...A.connection},name:g,sttLifecycle:A.sttLifecycle??"continuous",turnDetection:a(A.turnDetection)}};var v0=(g)=>({assistantAudio:[...g.assistantAudio],assistantStreamingText:g.assistantStreamingText,assistantTexts:[...g.assistantTexts],call:g.call,error:g.error,isConnected:g.isConnected,isRecording:!1,partial:g.partial,reconnect:g.reconnect,recordingError:null,sessionId:g.sessionId,sessionMetadata:g.sessionMetadata,scenarioId:g.scenarioId,status:g.status,turns:[...g.turns]}),t=(g,A={})=>{let C=p(A.preset),V=r(g,{...C.connection,...A.connection}),I=null,R=v0(V),w=new Set,L=()=>{for(let y of w)y()},$=()=>{if(R={...R,assistantAudio:[...V.assistantAudio],assistantStreamingText:V.assistantStreamingText,assistantTexts:[...V.assistantTexts],call:V.call,error:V.error,isConnected:V.isConnected,partial:V.partial,reconnect:V.reconnect,sessionId:V.sessionId,sessionMetadata:V.sessionMetadata,scenarioId:V.scenarioId,status:V.status,turns:[...V.turns]},A.autoStopOnComplete!==!1&&R.status==="completed"&&R.isRecording)I?.stop(),I=null,R={...R,isRecording:!1};L()},X=V.subscribe($);$();let G=()=>{if(I)return I;return I=f({channelCount:A.capture?.channelCount??C.capture.channelCount,onLevel:A.capture?.onLevel,onAudio:(y)=>{if(A.capture?.onAudio){A.capture.onAudio(y,V.sendAudio);return}V.sendAudio(y)},sampleRateHz:A.capture?.sampleRateHz??C.capture.sampleRateHz,...A.capture?.stream?{stream:A.capture.stream}:{}}),I},h=()=>{I?.stop(),I=null,R={...R,isRecording:!1},L()},_=async()=>{if(R.isRecording)return;try{R={...R,recordingError:null},L(),await G().start(),R={...R,isRecording:!0},L()}catch(y){throw I=null,R={...R,isRecording:!1,recordingError:y instanceof Error?y.message:String(y)},L(),y}};return{close:()=>{X(),h(),V.close()},startRecording:_,stopRecording:h,get assistantAudio(){return R.assistantAudio},get assistantTexts(){return R.assistantTexts},get assistantStreamingText(){return R.assistantStreamingText},bindHTMX(y){return F(V,y)},get call(){return R.call},callControl:(y)=>V.callControl(y),endTurn:()=>V.endTurn(),get error(){return R.error},getServerSnapshot:()=>R,getSnapshot:()=>R,get isConnected(){return R.isConnected},get isRecording(){return R.isRecording},get partial(){return R.partial},get reconnect(){return R.reconnect},get recordingError(){return R.recordingError},get scenarioId(){return R.scenarioId},sendAudio:(y)=>V.sendAudio(y),get sessionId(){return R.sessionId},get sessionMetadata(){return R.sessionMetadata},simulateDisconnect:()=>V.simulateDisconnect(),get status(){return R.status},subscribe:(y)=>{return w.add(y),()=>{w.delete(y)}},toggleRecording:async()=>{if(R.isRecording){h();return}await _()},get turns(){return R.turns}}};var c=(g)=>String(g).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#39;");var e=(g)=>{if(!g.isConnected)return"idle";if(g.isPlaying)return"speaking";if(g.isRecording&&g.hasActivePartial)return"listening";if(g.isRecording)return"listening";if(g.lastTranscriptAt&&!g.lastAssistantAt)return"thinking";if(g.lastTranscriptAt&&g.lastAssistantAt&&g.lastTranscriptAt>g.lastAssistantAt)return"thinking";return"idle"};var i0={accent:"#3b82f6",background:"#0f172a",errorAccent:"#ef4444",fontFamily:'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',foreground:"#f8fafc",radius:16},o0={callEnded:"Call ended",connecting:"Connecting…",endCall:"End call",idle:"Idle",listening:"Listening",mute:"Mute",speaking:"Speaking",startCall:"Start call",thinking:"Thinking",unmute:"Unmute"},n0=(g,A)=>{switch(g){case"listening":return A.listening;case"speaking":return A.speaking;case"thinking":return A.thinking;case"idle":return A.idle}},g0=(g)=>{let A={...i0,...g.theme},C={...o0,...g.labels},V=g.state.assistantAudio.at(-1)?.receivedAt,I=g.state.turns.at(-1)?.committedAt,R=e({hasActivePartial:g.state.partial.length>0,isConnected:g.state.isConnected,isPlaying:!1,isRecording:g.state.isRecording,lastAssistantAt:V,lastTranscriptAt:I}),w=!g.state.isConnected&&g.state.status!=="idle"&&!g.state.error,L=g.state.error?"Error":w?C.connecting:g.state.status==="completed"?C.callEnded:n0(R,C);return{agentState:R,classes:{container:`absolute-voice-widget absolute-voice-widget--${R}`,dot:`absolute-voice-widget__dot${g.state.error?" absolute-voice-widget__dot--error":""}`},controls:{canEnd:g.state.isConnected,canMute:g.state.isRecording,canStart:!g.state.isRecording&&g.state.status!=="completed"},errorMessage:g.state.error??void 0,labels:C,partial:g.state.partial||void 0,statusLabel:L,theme:A,title:g.title??"Voice"}},m0=(g)=>typeof g==="number"?`${g}px`:g,A0=(g)=>{let A=g.theme,C=`background:${A.background};border-radius:${m0(A.radius)};color:${A.foreground};font-family:${A.fontFamily};min-width:240px;padding:20px 22px;`,V=`background:${g.errorMessage?A.errorAccent:g.agentState==="idle"?"rgba(148,163,184,0.6)":A.accent};border-radius:50%;height:10px;width:10px;`,I=[];if(g.controls.canStart)I.push(`<button type="button" data-action="start" style="background:${A.accent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${c(g.labels.startCall)}</button>`);if(g.controls.canMute)I.push(`<button type="button" data-action="mute" style="background:transparent;border:1px solid rgba(255,255,255,0.18);border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${c(g.labels.mute)}</button>`);if(g.controls.canEnd)I.push(`<button type="button" data-action="end" style="background:${A.errorAccent};border:none;border-radius:12px;color:${A.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${c(g.labels.endCall)}</button>`);return`<div role="region" aria-live="polite" data-agent-state="${g.agentState}" class="${c(g.classes.container)}" style="${C}">
2
2
  <div style="align-items:center;display:flex;gap:10px;margin-bottom:12px;">
3
- <span aria-hidden="true" class="${X(c.classes.dot)}" style="${A}"></span>
4
- <strong style="font-size:15px;">${X(c.title)}</strong>
5
- <span style="font-size:13px;margin-left:auto;opacity:0.7;">${X(c.statusLabel)}</span>
3
+ <span aria-hidden="true" class="${c(g.classes.dot)}" style="${V}"></span>
4
+ <strong style="font-size:15px;">${c(g.title)}</strong>
5
+ <span style="font-size:13px;margin-left:auto;opacity:0.7;">${c(g.statusLabel)}</span>
6
6
  </div>
7
- ${c.partial?`<p style="font-size:13px;margin:8px 0 12px;opacity:0.85;word-break:break-word;">“${X(c.partial)}”</p>`:""}
8
- <div style="display:flex;gap:10px;">${_.join("")}</div>
9
- ${c.errorMessage?`<p style="color:${g.errorAccent};font-size:12px;margin-top:12px;">${X(c.errorMessage)}</p>`:""}
10
- </div>`};var ic=(c)=>{if(typeof c!=="string")return c;let g=document.querySelector(c);if(!g)throw Error(`AbsoluteVoice.mount: no element matches "${c}"`);return g},cc=(c,g={})=>{let C=ic(c),A=u(g.path??"/voice",g.controllerOptions),_=null,V=null,R=()=>{let L=p({...g.labels!==void 0?{labels:g.labels}:{},state:{assistantAudio:A.assistantAudio,error:A.error,isConnected:A.isConnected,isRecording:A.isRecording,partial:A.partial,status:A.status,turns:A.turns},...g.theme!==void 0?{theme:g.theme}:{},...g.title!==void 0?{title:g.title}:{}});C.innerHTML=t(L);for(let $ of C.querySelectorAll("button[data-action]")){let{action:S}=$.dataset;$.addEventListener("click",()=>{if(S==="start")A.startRecording();else if(S==="mute")A.stopRecording();else if(S==="end")A.close()})}if(A.error&&A.error!==_)_=A.error,g.onError?.(A.error);if(A.status!==V)V=A.status,g.onStatusChange?.(A.status)},w=A.subscribe(R);if(R(),g.autoStart)A.startRecording();return{controller:A,async end(){await A.close()},mute(){A.stopRecording()},async start(){await A.startRecording()},unmount(){w(),A.close(),C.innerHTML=""}}},gc="0.0.22-beta.516",Ac={mount:cc,version:gc};if(typeof globalThis<"u")globalThis.AbsoluteVoice=Ac;var sc=Ac;})();
7
+ ${g.partial?`<p style="font-size:13px;margin:8px 0 12px;opacity:0.85;word-break:break-word;">“${c(g.partial)}”</p>`:""}
8
+ <div style="display:flex;gap:10px;">${I.join("")}</div>
9
+ ${g.errorMessage?`<p style="color:${A.errorAccent};font-size:12px;margin-top:12px;">${c(g.errorMessage)}</p>`:""}
10
+ </div>`};var u0=(g)=>{if(typeof g!=="string")return g;let A=document.querySelector(g);if(!A)throw Error(`AbsoluteVoice.mount: no element matches "${g}"`);return A},V0=(g,A={})=>{let C=u0(g),V=t(A.path??"/voice",A.controllerOptions),I=null,R=null,w=()=>{let $=g0({...A.labels!==void 0?{labels:A.labels}:{},state:{assistantAudio:V.assistantAudio,error:V.error,isConnected:V.isConnected,isRecording:V.isRecording,partial:V.partial,status:V.status,turns:V.turns},...A.theme!==void 0?{theme:A.theme}:{},...A.title!==void 0?{title:A.title}:{}});C.innerHTML=A0($);for(let X of C.querySelectorAll("button[data-action]")){let{action:G}=X.dataset;X.addEventListener("click",()=>{if(G==="start")V.startRecording();else if(G==="mute")V.stopRecording();else if(G==="end")V.close()})}if(V.error&&V.error!==I)I=V.error,A.onError?.(V.error);if(V.status!==R)R=V.status,A.onStatusChange?.(V.status)},L=V.subscribe(w);if(w(),A.autoStart)V.startRecording();return{controller:V,async end(){await V.close()},mute(){V.stopRecording()},async start(){await V.startRecording()},unmount(){L(),V.close(),C.innerHTML=""}}},C0="0.0.22-beta.516",R0={mount:V0,version:C0};if(typeof globalThis<"u")globalThis.AbsoluteVoice=R0;var s0=R0;})();
package/dist/index.d.ts CHANGED
@@ -192,6 +192,7 @@ export { createVoiceSQLiteAuditEventStore, createVoiceSQLiteAuditSinkDeliverySto
192
192
  export { createVoicePostgresAuditEventStore, createVoicePostgresAuditSinkDeliveryStore, createVoicePostgresCampaignStore, createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore, } from "./core/postgresStore";
193
193
  export { createVoiceS3RecordingStore, createVoiceS3ReviewStore, } from "./core/s3Store";
194
194
  export { createVoiceMemoryStore } from "./core/memoryStore";
195
+ export { createVoiceWriteBehindStore, type VoiceWriteBehindStore, type VoiceWriteBehindStoreOptions, } from "./core/writeBehindStore";
195
196
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks, } from "./core/opsSinks";
196
197
  export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature, } from "./core/opsWebhook";
197
198
  export { applyVoiceHandoffDeliveryResult, createVoiceHandoffDeliveryRecord, createVoiceMemoryHandoffDeliveryStore, createVoiceTwilioRedirectHandoffAdapter, createVoiceWebhookHandoffAdapter, deliverVoiceHandoff, deliverVoiceHandoffDelivery, } from "./core/handoff";
package/dist/index.js CHANGED
@@ -47076,6 +47076,113 @@ var createVoiceMemoryStore = () => {
47076
47076
  };
47077
47077
  return { get, getOrCreate, list, remove, set };
47078
47078
  };
47079
+ // src/core/writeBehindStore.ts
47080
+ var DEFAULT_FLUSH_DEBOUNCE_MS = 750;
47081
+ var MAX_FLUSH_DRAIN_PASSES = 5;
47082
+ var createVoiceWriteBehindStore = (options) => {
47083
+ const memory = options.memory ?? createVoiceMemoryStore();
47084
+ const { persistent } = options;
47085
+ const flushDebounceMs = options.flushDebounceMs ?? DEFAULT_FLUSH_DEBOUNCE_MS;
47086
+ const logger = options.logger;
47087
+ const pending = new Map;
47088
+ const persistedTurnCount = new Map;
47089
+ let timer = null;
47090
+ let flushChain = Promise.resolve();
47091
+ const doFlush = async () => {
47092
+ if (pending.size === 0) {
47093
+ return;
47094
+ }
47095
+ const batch = new Map(pending);
47096
+ pending.clear();
47097
+ for (const [id, value] of batch) {
47098
+ try {
47099
+ await persistent.set(id, value);
47100
+ persistedTurnCount.set(id, value.turns.length);
47101
+ } catch (error) {
47102
+ if (!pending.has(id)) {
47103
+ pending.set(id, value);
47104
+ }
47105
+ logger?.warn?.("voice write-behind flush failed; will retry", {
47106
+ error: error instanceof Error ? error.message : String(error),
47107
+ sessionId: id
47108
+ });
47109
+ scheduleFlush();
47110
+ }
47111
+ }
47112
+ };
47113
+ const runFlush = () => {
47114
+ flushChain = flushChain.then(doFlush, doFlush);
47115
+ return flushChain;
47116
+ };
47117
+ const scheduleFlush = () => {
47118
+ if (timer) {
47119
+ return;
47120
+ }
47121
+ timer = setTimeout(() => {
47122
+ timer = null;
47123
+ runFlush();
47124
+ }, flushDebounceMs);
47125
+ };
47126
+ const markDirty = (id, value) => {
47127
+ pending.set(id, value);
47128
+ if (value.turns.length > (persistedTurnCount.get(id) ?? 0)) {
47129
+ runFlush();
47130
+ } else {
47131
+ scheduleFlush();
47132
+ }
47133
+ };
47134
+ const hydrate = async (id) => {
47135
+ const persisted = await persistent.get(id);
47136
+ if (!persisted) {
47137
+ return;
47138
+ }
47139
+ await memory.set(id, persisted);
47140
+ persistedTurnCount.set(id, persisted.turns.length);
47141
+ return persisted;
47142
+ };
47143
+ const get = async (id) => {
47144
+ const live = await memory.get(id);
47145
+ if (live) {
47146
+ return live;
47147
+ }
47148
+ return hydrate(id);
47149
+ };
47150
+ const getOrCreate = async (id) => {
47151
+ const existing = await get(id);
47152
+ if (existing) {
47153
+ return existing;
47154
+ }
47155
+ return memory.getOrCreate(id);
47156
+ };
47157
+ const set = async (id, value) => {
47158
+ await memory.set(id, value);
47159
+ markDirty(id, value);
47160
+ };
47161
+ const remove = async (id) => {
47162
+ pending.delete(id);
47163
+ persistedTurnCount.delete(id);
47164
+ await memory.remove(id);
47165
+ await persistent.remove(id);
47166
+ };
47167
+ const list = () => memory.list();
47168
+ const flush = async () => {
47169
+ if (timer) {
47170
+ clearTimeout(timer);
47171
+ timer = null;
47172
+ }
47173
+ for (let pass = 0;pass < MAX_FLUSH_DRAIN_PASSES && pending.size > 0; pass += 1) {
47174
+ await runFlush();
47175
+ }
47176
+ await flushChain;
47177
+ };
47178
+ const dispose = () => {
47179
+ if (timer) {
47180
+ clearTimeout(timer);
47181
+ timer = null;
47182
+ }
47183
+ };
47184
+ return { dispose, flush, get, getOrCreate, list, remove, set };
47185
+ };
47079
47186
  // src/core/scribe.ts
47080
47187
  var createVoiceScribe = async (options) => {
47081
47188
  const session = await options.stt.open({
@@ -52916,6 +53023,7 @@ export {
52916
53023
  createVoiceZendeskTicketUpdateSink,
52917
53024
  createVoiceZendeskTicketSyncSinks,
52918
53025
  createVoiceZendeskTicketSink,
53026
+ createVoiceWriteBehindStore,
52919
53027
  createVoiceWorkflowScenario,
52920
53028
  createVoiceWorkflowContractPreset,
52921
53029
  createVoiceWorkflowContractHandler,
@@ -11433,9 +11433,11 @@ var createVoiceBrowserMediaReporter = (options) => {
11433
11433
  var WS_OPEN = 1;
11434
11434
  var WS_CLOSED = 3;
11435
11435
  var WS_NORMAL_CLOSURE = 1000;
11436
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
11436
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
11437
11437
  var DEFAULT_PING_INTERVAL = 30000;
11438
- var RECONNECT_DELAY_MS = 500;
11438
+ var RECONNECT_BASE_DELAY_MS = 500;
11439
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
11440
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
11439
11441
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
11440
11442
  var noop = () => {};
11441
11443
  var noopUnsubscribe = () => noop;
@@ -11504,7 +11506,9 @@ var createVoiceConnection = (path, options = {}) => {
11504
11506
  const listeners = new Set;
11505
11507
  const shouldReconnect = options.reconnect !== false;
11506
11508
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
11509
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
11507
11510
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
11511
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
11508
11512
  const state = {
11509
11513
  isConnected: false,
11510
11514
  pendingMessages: [],
@@ -11540,8 +11544,9 @@ var createVoiceConnection = (path, options = {}) => {
11540
11544
  }
11541
11545
  };
11542
11546
  const scheduleReconnect = () => {
11543
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
11544
11547
  state.reconnectAttempts += 1;
11548
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
11549
+ const nextAttemptAt = Date.now() + delayMs;
11545
11550
  emitConnection({
11546
11551
  reconnect: {
11547
11552
  attempts: state.reconnectAttempts,
@@ -11565,7 +11570,7 @@ var createVoiceConnection = (path, options = {}) => {
11565
11570
  return;
11566
11571
  }
11567
11572
  connect();
11568
- }, RECONNECT_DELAY_MS);
11573
+ }, delayMs);
11569
11574
  };
11570
11575
  const connect = () => {
11571
11576
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -756,9 +756,11 @@ var createVoiceBrowserMediaReporter = (options) => {
756
756
  var WS_OPEN = 1;
757
757
  var WS_CLOSED = 3;
758
758
  var WS_NORMAL_CLOSURE = 1000;
759
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
759
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
760
760
  var DEFAULT_PING_INTERVAL = 30000;
761
- var RECONNECT_DELAY_MS = 500;
761
+ var RECONNECT_BASE_DELAY_MS = 500;
762
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
763
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
762
764
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
763
765
  var noop = () => {};
764
766
  var noopUnsubscribe = () => noop;
@@ -827,7 +829,9 @@ var createVoiceConnection = (path, options = {}) => {
827
829
  const listeners = new Set;
828
830
  const shouldReconnect = options.reconnect !== false;
829
831
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
832
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
830
833
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
834
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
831
835
  const state = {
832
836
  isConnected: false,
833
837
  pendingMessages: [],
@@ -863,8 +867,9 @@ var createVoiceConnection = (path, options = {}) => {
863
867
  }
864
868
  };
865
869
  const scheduleReconnect = () => {
866
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
867
870
  state.reconnectAttempts += 1;
871
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
872
+ const nextAttemptAt = Date.now() + delayMs;
868
873
  emitConnection({
869
874
  reconnect: {
870
875
  attempts: state.reconnectAttempts,
@@ -888,7 +893,7 @@ var createVoiceConnection = (path, options = {}) => {
888
893
  return;
889
894
  }
890
895
  connect();
891
- }, RECONNECT_DELAY_MS);
896
+ }, delayMs);
892
897
  };
893
898
  const connect = () => {
894
899
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
@@ -2506,9 +2506,11 @@ var createVoiceBrowserMediaReporter = (options) => {
2506
2506
  var WS_OPEN = 1;
2507
2507
  var WS_CLOSED = 3;
2508
2508
  var WS_NORMAL_CLOSURE = 1000;
2509
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
2509
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
2510
2510
  var DEFAULT_PING_INTERVAL = 30000;
2511
- var RECONNECT_DELAY_MS = 500;
2511
+ var RECONNECT_BASE_DELAY_MS = 500;
2512
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
2513
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
2512
2514
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
2513
2515
  var noop = () => {};
2514
2516
  var noopUnsubscribe = () => noop;
@@ -2577,7 +2579,9 @@ var createVoiceConnection = (path, options = {}) => {
2577
2579
  const listeners = new Set;
2578
2580
  const shouldReconnect = options.reconnect !== false;
2579
2581
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
2582
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
2580
2583
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
2584
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
2581
2585
  const state = {
2582
2586
  isConnected: false,
2583
2587
  pendingMessages: [],
@@ -2613,8 +2617,9 @@ var createVoiceConnection = (path, options = {}) => {
2613
2617
  }
2614
2618
  };
2615
2619
  const scheduleReconnect = () => {
2616
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
2617
2620
  state.reconnectAttempts += 1;
2621
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
2622
+ const nextAttemptAt = Date.now() + delayMs;
2618
2623
  emitConnection({
2619
2624
  reconnect: {
2620
2625
  attempts: state.reconnectAttempts,
@@ -2638,7 +2643,7 @@ var createVoiceConnection = (path, options = {}) => {
2638
2643
  return;
2639
2644
  }
2640
2645
  connect();
2641
- }, RECONNECT_DELAY_MS);
2646
+ }, delayMs);
2642
2647
  };
2643
2648
  const connect = () => {
2644
2649
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
package/dist/vue/index.js CHANGED
@@ -10829,9 +10829,11 @@ var createVoiceBrowserMediaReporter = (options) => {
10829
10829
  var WS_OPEN = 1;
10830
10830
  var WS_CLOSED = 3;
10831
10831
  var WS_NORMAL_CLOSURE = 1000;
10832
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
10832
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
10833
10833
  var DEFAULT_PING_INTERVAL = 30000;
10834
- var RECONNECT_DELAY_MS = 500;
10834
+ var RECONNECT_BASE_DELAY_MS = 500;
10835
+ var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
10836
+ var computeVoiceReconnectDelayMs = (attempt, baseMs, maxDelayMs) => Math.min(maxDelayMs, baseMs * 2 ** (Math.max(1, attempt) - 1));
10835
10837
  var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
10836
10838
  var noop = () => {};
10837
10839
  var noopUnsubscribe = () => noop;
@@ -10900,7 +10902,9 @@ var createVoiceConnection = (path, options = {}) => {
10900
10902
  const listeners = new Set;
10901
10903
  const shouldReconnect = options.reconnect !== false;
10902
10904
  const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
10905
+ const reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
10903
10906
  const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
10907
+ const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
10904
10908
  const state = {
10905
10909
  isConnected: false,
10906
10910
  pendingMessages: [],
@@ -10936,8 +10940,9 @@ var createVoiceConnection = (path, options = {}) => {
10936
10940
  }
10937
10941
  };
10938
10942
  const scheduleReconnect = () => {
10939
- const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
10940
10943
  state.reconnectAttempts += 1;
10944
+ const delayMs = computeReconnectDelayMs(state.reconnectAttempts);
10945
+ const nextAttemptAt = Date.now() + delayMs;
10941
10946
  emitConnection({
10942
10947
  reconnect: {
10943
10948
  attempts: state.reconnectAttempts,
@@ -10961,7 +10966,7 @@ var createVoiceConnection = (path, options = {}) => {
10961
10966
  return;
10962
10967
  }
10963
10968
  connect();
10964
- }, RECONNECT_DELAY_MS);
10969
+ }, delayMs);
10965
10970
  };
10966
10971
  const connect = () => {
10967
10972
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.587",
3
+ "version": "0.0.22-beta.588",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",