@exotel-npm-dev/exotel-ai-assist 1.0.1 → 1.0.3

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.
@@ -4,11 +4,12 @@ export declare class ExotelAIAssistController extends EventEmitter<ControllerEve
4
4
  private params;
5
5
  private transport;
6
6
  private status;
7
+ private destroyed;
8
+ private connectionEstablished;
7
9
  private reconnectAttempt;
8
10
  private reconnectTimer;
9
- private destroyed;
10
- private readonly reconnectInterval;
11
- private readonly maxReconnectAttempts;
11
+ private static readonly MAX_RECONNECT_ATTEMPTS;
12
+ private static readonly RECONNECT_BASE_MS;
12
13
  constructor(params: ExotelAIAssistParams);
13
14
  connect(): void;
14
15
  disconnect(): void;
@@ -17,9 +18,8 @@ export declare class ExotelAIAssistController extends EventEmitter<ControllerEve
17
18
  getStatus(): ConnectionStatus;
18
19
  private _ensureTransport;
19
20
  private _handleTransportMessage;
20
- private _handleServerPayload;
21
21
  private _scheduleReconnect;
22
22
  private _clearReconnectTimer;
23
+ private _handleServerPayload;
23
24
  private _setStatus;
24
- private _log;
25
25
  }
@@ -402,13 +402,6 @@ class BroadcastChannelTransport {
402
402
  this.socket.onopen = () => {
403
403
  var _a;
404
404
  this.socketConnected = true;
405
- // First-message auth protocol: send the JWT before any other message.
406
- // The server does not send an auth_required prompt; the client is
407
- // responsible for sending this immediately after the socket opens.
408
- console.log('Sending token', {
409
- type: "auth",
410
- access_token: authToken
411
- });
412
405
  this.socket.send(JSON.stringify({
413
406
  type: "auth",
414
407
  access_token: authToken
@@ -1543,10 +1536,8 @@ class Utils {
1543
1536
  }
1544
1537
  static constructQueryParams(params) {
1545
1538
  // Keys consumed by the library — never forwarded to the server.
1546
- // authToken is intentionally excluded: authentication is done via the
1547
- // first WebSocket text message (first-message auth protocol), not the URL.
1548
1539
  const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
1549
- // Stream-key resolution params sent as query parameters.
1540
+ // Mandatory server params remapped to the names the API expects.
1550
1541
  // accountId is embedded in the URL path — never sent as a query param.
1551
1542
  const pairs = [];
1552
1543
  if (params.call_sid !== undefined && params.call_sid !== null) {
@@ -1581,20 +1572,25 @@ Utils.MAX_EXTRA_QUERY_PARAMS = 3;
1581
1572
 
1582
1573
  class ExotelAIAssistController extends EventEmitter {
1583
1574
  constructor(params) {
1584
- var _a, _b;
1585
1575
  super();
1586
1576
  this.transport = null;
1587
1577
  this.status = "idle";
1578
+ this.destroyed = false;
1579
+ // True once the WebSocket's onopen fires (CONNECTED received).
1580
+ // Reset to false before every new connection attempt.
1581
+ // This is the gate for the retry decision:
1582
+ // connectionEstablished === false → connection never opened → retry
1583
+ // connectionEstablished === true → server ended a live connection → destroy
1584
+ this.connectionEstablished = false;
1588
1585
  this.reconnectAttempt = 0;
1589
1586
  this.reconnectTimer = null;
1590
- this.destroyed = false;
1591
1587
  this.params = Object.assign({}, params);
1592
- this.reconnectInterval = (_a = params.reconnectInterval) !== null && _a !== void 0 ? _a : 3000;
1593
- this.maxReconnectAttempts = (_b = params.maxReconnectAttempts) !== null && _b !== void 0 ? _b : 5;
1594
1588
  }
1595
1589
  connect() {
1596
1590
  if (this.destroyed) return;
1597
1591
  this._clearReconnectTimer();
1592
+ this.connectionEstablished = false;
1593
+ this.reconnectAttempt = 0;
1598
1594
  this._setStatus("connecting");
1599
1595
  this._ensureTransport();
1600
1596
  this.transport.connect(Utils.buildWsUrl(this.params), this.params.authToken);
@@ -1609,14 +1605,12 @@ class ExotelAIAssistController extends EventEmitter {
1609
1605
  var _a, _b;
1610
1606
  const prev = this.params;
1611
1607
  this.params = Object.assign(Object.assign({}, prev), patch);
1612
- const hash = Utils.hash(this.params);
1613
- const prevHash = Utils.hash(prev);
1614
- const hashChanged = hash !== prevHash;
1608
+ const hashChanged = Utils.hash(this.params) !== Utils.hash(prev);
1615
1609
  if (hashChanged) {
1616
- this.reconnectAttempt = 0;
1617
1610
  this._clearReconnectTimer();
1618
- // Destroy (not just disconnect) so the next _ensureTransport call creates
1619
- // a new instance scoped to the new session hash.
1611
+ this.connectionEstablished = false;
1612
+ this.reconnectAttempt = 0;
1613
+ // Tear down the current transport and start fresh with the new params.
1620
1614
  (_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
1621
1615
  this.transport = null;
1622
1616
  this._setStatus("connecting");
@@ -1631,6 +1625,7 @@ class ExotelAIAssistController extends EventEmitter {
1631
1625
  }
1632
1626
  destroy() {
1633
1627
  var _a;
1628
+ if (this.destroyed) return;
1634
1629
  this.destroyed = true;
1635
1630
  this._clearReconnectTimer();
1636
1631
  (_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
@@ -1649,21 +1644,27 @@ class ExotelAIAssistController extends EventEmitter {
1649
1644
  var _a;
1650
1645
  switch (msg.type) {
1651
1646
  case "CONNECTED":
1647
+ this.connectionEstablished = true;
1652
1648
  this.reconnectAttempt = 0;
1653
1649
  this._setStatus("connected");
1654
1650
  this.emit("onCallStart");
1655
1651
  break;
1656
1652
  case "DISCONNECTED":
1657
- this._setStatus("disconnected");
1658
- this.emit("onCallEnd");
1659
- // Close codes 4001 (auth_failed) and 4002 (auth_timeout) are permanent
1660
- // auth failures — reconnecting with the same token would fail again.
1661
- if (msg.code !== 4001 && msg.code !== 4002) {
1653
+ if (this.connectionEstablished) {
1654
+ // Server ended an already-established connection (call ended, stream
1655
+ // not found, auth failure, etc.) — do not retry.
1656
+ this._setStatus("disconnected");
1657
+ this.emit("onCallEnd");
1658
+ this.destroy();
1659
+ } else {
1660
+ // WebSocket never opened — network/DNS/refused error.
1661
+ // Retry up to MAX_RECONNECT_ATTEMPTS then give up.
1662
1662
  this._scheduleReconnect();
1663
1663
  }
1664
1664
  break;
1665
1665
  case "ERROR":
1666
- this._setStatus("error");
1666
+ // onerror fires before onclose; DISCONNECTED will arrive right after
1667
+ // and drive the retry / destroy decision. Just surface the error here.
1667
1668
  this.emit("error", new Error((_a = msg.message) !== null && _a !== void 0 ? _a : "Unknown transport error"));
1668
1669
  break;
1669
1670
  case "MESSAGE":
@@ -1671,6 +1672,32 @@ class ExotelAIAssistController extends EventEmitter {
1671
1672
  break;
1672
1673
  }
1673
1674
  }
1675
+ _scheduleReconnect() {
1676
+ if (this.destroyed) return;
1677
+ if (this.reconnectAttempt >= ExotelAIAssistController.MAX_RECONNECT_ATTEMPTS) {
1678
+ const err = new Error("Connection failed after maximum retries");
1679
+ err.code = "MAX_RECONNECT_EXCEEDED";
1680
+ this._setStatus("error");
1681
+ this.emit("error", err);
1682
+ this.destroy();
1683
+ return;
1684
+ }
1685
+ const delay = Math.min(ExotelAIAssistController.RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt), 30000);
1686
+ this.reconnectAttempt += 1;
1687
+ this._setStatus("connecting");
1688
+ this.reconnectTimer = setTimeout(() => {
1689
+ var _a;
1690
+ if (this.destroyed) return;
1691
+ this.connectionEstablished = false;
1692
+ (_a = this.transport) === null || _a === void 0 ? void 0 : _a.connect(Utils.buildWsUrl(this.params), this.params.authToken);
1693
+ }, delay);
1694
+ }
1695
+ _clearReconnectTimer() {
1696
+ if (this.reconnectTimer !== null) {
1697
+ clearTimeout(this.reconnectTimer);
1698
+ this.reconnectTimer = null;
1699
+ }
1700
+ }
1674
1701
  _handleServerPayload(raw) {
1675
1702
  var _a;
1676
1703
  let parsed;
@@ -1733,38 +1760,13 @@ class ExotelAIAssistController extends EventEmitter {
1733
1760
  }
1734
1761
  }
1735
1762
  }
1736
- _scheduleReconnect() {
1737
- if (this.destroyed) return;
1738
- if (this.status === "error") return;
1739
- if (this.reconnectAttempt >= this.maxReconnectAttempts) {
1740
- const err = new Error("MAX_RECONNECT_EXCEEDED");
1741
- err.code = "MAX_RECONNECT_EXCEEDED";
1742
- this.emit("error", err);
1743
- return;
1744
- }
1745
- const delay = Math.min(this.reconnectInterval * Math.pow(2, this.reconnectAttempt), 30000);
1746
- this.reconnectTimer = setTimeout(() => {
1747
- var _a;
1748
- if (this.destroyed) return;
1749
- this.reconnectAttempt += 1;
1750
- this._setStatus("connecting");
1751
- (_a = this.transport) === null || _a === void 0 ? void 0 : _a.connect(Utils.buildWsUrl(this.params), this.params.authToken);
1752
- }, delay);
1753
- }
1754
- _clearReconnectTimer() {
1755
- if (this.reconnectTimer !== null) {
1756
- clearTimeout(this.reconnectTimer);
1757
- this.reconnectTimer = null;
1758
- }
1759
- }
1760
1763
  _setStatus(status) {
1761
1764
  if (this.status === status) return;
1762
1765
  this.status = status;
1763
1766
  this.emit("statusChange", status);
1764
1767
  }
1765
- _log(...args) {
1766
- console.log("[ExotelAIAssist]", ...args);
1767
- }
1768
1768
  }
1769
+ ExotelAIAssistController.MAX_RECONNECT_ATTEMPTS = 5;
1770
+ ExotelAIAssistController.RECONNECT_BASE_MS = 3000;
1769
1771
 
1770
1772
  exports.ExotelAIAssistController = ExotelAIAssistController;
package/dist/index.js CHANGED
@@ -19820,13 +19820,6 @@ class BroadcastChannelTransport {
19820
19820
  this.socket.onopen = () => {
19821
19821
  var _a;
19822
19822
  this.socketConnected = true;
19823
- // First-message auth protocol: send the JWT before any other message.
19824
- // The server does not send an auth_required prompt; the client is
19825
- // responsible for sending this immediately after the socket opens.
19826
- console.log('Sending token', {
19827
- type: "auth",
19828
- access_token: authToken
19829
- });
19830
19823
  this.socket.send(JSON.stringify({
19831
19824
  type: "auth",
19832
19825
  access_token: authToken
@@ -20961,10 +20954,8 @@ class Utils {
20961
20954
  }
20962
20955
  static constructQueryParams(params) {
20963
20956
  // Keys consumed by the library — never forwarded to the server.
20964
- // authToken is intentionally excluded: authentication is done via the
20965
- // first WebSocket text message (first-message auth protocol), not the URL.
20966
20957
  const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
20967
- // Stream-key resolution params sent as query parameters.
20958
+ // Mandatory server params remapped to the names the API expects.
20968
20959
  // accountId is embedded in the URL path — never sent as a query param.
20969
20960
  const pairs = [];
20970
20961
  if (params.call_sid !== undefined && params.call_sid !== null) {
@@ -20999,20 +20990,25 @@ Utils.MAX_EXTRA_QUERY_PARAMS = 3;
20999
20990
 
21000
20991
  class ExotelAIAssistController extends EventEmitter {
21001
20992
  constructor(params) {
21002
- var _a, _b;
21003
20993
  super();
21004
20994
  this.transport = null;
21005
20995
  this.status = "idle";
20996
+ this.destroyed = false;
20997
+ // True once the WebSocket's onopen fires (CONNECTED received).
20998
+ // Reset to false before every new connection attempt.
20999
+ // This is the gate for the retry decision:
21000
+ // connectionEstablished === false → connection never opened → retry
21001
+ // connectionEstablished === true → server ended a live connection → destroy
21002
+ this.connectionEstablished = false;
21006
21003
  this.reconnectAttempt = 0;
21007
21004
  this.reconnectTimer = null;
21008
- this.destroyed = false;
21009
21005
  this.params = Object.assign({}, params);
21010
- this.reconnectInterval = (_a = params.reconnectInterval) !== null && _a !== void 0 ? _a : 3000;
21011
- this.maxReconnectAttempts = (_b = params.maxReconnectAttempts) !== null && _b !== void 0 ? _b : 5;
21012
21006
  }
21013
21007
  connect() {
21014
21008
  if (this.destroyed) return;
21015
21009
  this._clearReconnectTimer();
21010
+ this.connectionEstablished = false;
21011
+ this.reconnectAttempt = 0;
21016
21012
  this._setStatus("connecting");
21017
21013
  this._ensureTransport();
21018
21014
  this.transport.connect(Utils.buildWsUrl(this.params), this.params.authToken);
@@ -21027,14 +21023,12 @@ class ExotelAIAssistController extends EventEmitter {
21027
21023
  var _a, _b;
21028
21024
  const prev = this.params;
21029
21025
  this.params = Object.assign(Object.assign({}, prev), patch);
21030
- const hash = Utils.hash(this.params);
21031
- const prevHash = Utils.hash(prev);
21032
- const hashChanged = hash !== prevHash;
21026
+ const hashChanged = Utils.hash(this.params) !== Utils.hash(prev);
21033
21027
  if (hashChanged) {
21034
- this.reconnectAttempt = 0;
21035
21028
  this._clearReconnectTimer();
21036
- // Destroy (not just disconnect) so the next _ensureTransport call creates
21037
- // a new instance scoped to the new session hash.
21029
+ this.connectionEstablished = false;
21030
+ this.reconnectAttempt = 0;
21031
+ // Tear down the current transport and start fresh with the new params.
21038
21032
  (_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
21039
21033
  this.transport = null;
21040
21034
  this._setStatus("connecting");
@@ -21049,6 +21043,7 @@ class ExotelAIAssistController extends EventEmitter {
21049
21043
  }
21050
21044
  destroy() {
21051
21045
  var _a;
21046
+ if (this.destroyed) return;
21052
21047
  this.destroyed = true;
21053
21048
  this._clearReconnectTimer();
21054
21049
  (_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
@@ -21067,21 +21062,27 @@ class ExotelAIAssistController extends EventEmitter {
21067
21062
  var _a;
21068
21063
  switch (msg.type) {
21069
21064
  case "CONNECTED":
21065
+ this.connectionEstablished = true;
21070
21066
  this.reconnectAttempt = 0;
21071
21067
  this._setStatus("connected");
21072
21068
  this.emit("onCallStart");
21073
21069
  break;
21074
21070
  case "DISCONNECTED":
21075
- this._setStatus("disconnected");
21076
- this.emit("onCallEnd");
21077
- // Close codes 4001 (auth_failed) and 4002 (auth_timeout) are permanent
21078
- // auth failures — reconnecting with the same token would fail again.
21079
- if (msg.code !== 4001 && msg.code !== 4002) {
21071
+ if (this.connectionEstablished) {
21072
+ // Server ended an already-established connection (call ended, stream
21073
+ // not found, auth failure, etc.) — do not retry.
21074
+ this._setStatus("disconnected");
21075
+ this.emit("onCallEnd");
21076
+ this.destroy();
21077
+ } else {
21078
+ // WebSocket never opened — network/DNS/refused error.
21079
+ // Retry up to MAX_RECONNECT_ATTEMPTS then give up.
21080
21080
  this._scheduleReconnect();
21081
21081
  }
21082
21082
  break;
21083
21083
  case "ERROR":
21084
- this._setStatus("error");
21084
+ // onerror fires before onclose; DISCONNECTED will arrive right after
21085
+ // and drive the retry / destroy decision. Just surface the error here.
21085
21086
  this.emit("error", new Error((_a = msg.message) !== null && _a !== void 0 ? _a : "Unknown transport error"));
21086
21087
  break;
21087
21088
  case "MESSAGE":
@@ -21089,6 +21090,32 @@ class ExotelAIAssistController extends EventEmitter {
21089
21090
  break;
21090
21091
  }
21091
21092
  }
21093
+ _scheduleReconnect() {
21094
+ if (this.destroyed) return;
21095
+ if (this.reconnectAttempt >= ExotelAIAssistController.MAX_RECONNECT_ATTEMPTS) {
21096
+ const err = new Error("Connection failed after maximum retries");
21097
+ err.code = "MAX_RECONNECT_EXCEEDED";
21098
+ this._setStatus("error");
21099
+ this.emit("error", err);
21100
+ this.destroy();
21101
+ return;
21102
+ }
21103
+ const delay = Math.min(ExotelAIAssistController.RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt), 30000);
21104
+ this.reconnectAttempt += 1;
21105
+ this._setStatus("connecting");
21106
+ this.reconnectTimer = setTimeout(() => {
21107
+ var _a;
21108
+ if (this.destroyed) return;
21109
+ this.connectionEstablished = false;
21110
+ (_a = this.transport) === null || _a === void 0 ? void 0 : _a.connect(Utils.buildWsUrl(this.params), this.params.authToken);
21111
+ }, delay);
21112
+ }
21113
+ _clearReconnectTimer() {
21114
+ if (this.reconnectTimer !== null) {
21115
+ clearTimeout(this.reconnectTimer);
21116
+ this.reconnectTimer = null;
21117
+ }
21118
+ }
21092
21119
  _handleServerPayload(raw) {
21093
21120
  var _a;
21094
21121
  let parsed;
@@ -21151,39 +21178,14 @@ class ExotelAIAssistController extends EventEmitter {
21151
21178
  }
21152
21179
  }
21153
21180
  }
21154
- _scheduleReconnect() {
21155
- if (this.destroyed) return;
21156
- if (this.status === "error") return;
21157
- if (this.reconnectAttempt >= this.maxReconnectAttempts) {
21158
- const err = new Error("MAX_RECONNECT_EXCEEDED");
21159
- err.code = "MAX_RECONNECT_EXCEEDED";
21160
- this.emit("error", err);
21161
- return;
21162
- }
21163
- const delay = Math.min(this.reconnectInterval * Math.pow(2, this.reconnectAttempt), 30000);
21164
- this.reconnectTimer = setTimeout(() => {
21165
- var _a;
21166
- if (this.destroyed) return;
21167
- this.reconnectAttempt += 1;
21168
- this._setStatus("connecting");
21169
- (_a = this.transport) === null || _a === void 0 ? void 0 : _a.connect(Utils.buildWsUrl(this.params), this.params.authToken);
21170
- }, delay);
21171
- }
21172
- _clearReconnectTimer() {
21173
- if (this.reconnectTimer !== null) {
21174
- clearTimeout(this.reconnectTimer);
21175
- this.reconnectTimer = null;
21176
- }
21177
- }
21178
21181
  _setStatus(status) {
21179
21182
  if (this.status === status) return;
21180
21183
  this.status = status;
21181
21184
  this.emit("statusChange", status);
21182
21185
  }
21183
- _log(...args) {
21184
- console.log("[ExotelAIAssist]", ...args);
21185
- }
21186
21186
  }
21187
+ ExotelAIAssistController.MAX_RECONNECT_ATTEMPTS = 5;
21188
+ ExotelAIAssistController.RECONNECT_BASE_MS = 3000;
21187
21189
 
21188
21190
  function useExotelAIAssist(params) {
21189
21191
  const controllerRef = reactExports.useRef(null);
@@ -21284,7 +21286,7 @@ function styleInject(css, ref) {
21284
21286
  }
21285
21287
  }
21286
21288
 
21287
- var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover{background:#f9fafb;color:#374151}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-muted{color:#6b7280}";
21289
+ var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{flex:1;min-height:0;overflow:hidden}.oa-tabs-content[data-state=inactive]{display:none!important}.oa-tabs-content[data-state=active]{display:flex;flex-direction:column}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover{background:#f9fafb;color:#374151}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-muted{color:#6b7280}";
21288
21290
  styleInject(css_248z);
21289
21291
 
21290
21292
  function Sparkle() {
@@ -5106,13 +5106,6 @@ class BroadcastChannelTransport {
5106
5106
  this.socket.onopen = () => {
5107
5107
  var _a;
5108
5108
  this.socketConnected = true;
5109
- // First-message auth protocol: send the JWT before any other message.
5110
- // The server does not send an auth_required prompt; the client is
5111
- // responsible for sending this immediately after the socket opens.
5112
- console.log('Sending token', {
5113
- type: "auth",
5114
- access_token: authToken
5115
- });
5116
5109
  this.socket.send(JSON.stringify({
5117
5110
  type: "auth",
5118
5111
  access_token: authToken
@@ -6247,10 +6240,8 @@ class Utils {
6247
6240
  }
6248
6241
  static constructQueryParams(params) {
6249
6242
  // Keys consumed by the library — never forwarded to the server.
6250
- // authToken is intentionally excluded: authentication is done via the
6251
- // first WebSocket text message (first-message auth protocol), not the URL.
6252
6243
  const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
6253
- // Stream-key resolution params sent as query parameters.
6244
+ // Mandatory server params remapped to the names the API expects.
6254
6245
  // accountId is embedded in the URL path — never sent as a query param.
6255
6246
  const pairs = [];
6256
6247
  if (params.call_sid !== undefined && params.call_sid !== null) {
@@ -6285,20 +6276,25 @@ Utils.MAX_EXTRA_QUERY_PARAMS = 3;
6285
6276
 
6286
6277
  class ExotelAIAssistController extends EventEmitter {
6287
6278
  constructor(params) {
6288
- var _a, _b;
6289
6279
  super();
6290
6280
  this.transport = null;
6291
6281
  this.status = "idle";
6282
+ this.destroyed = false;
6283
+ // True once the WebSocket's onopen fires (CONNECTED received).
6284
+ // Reset to false before every new connection attempt.
6285
+ // This is the gate for the retry decision:
6286
+ // connectionEstablished === false → connection never opened → retry
6287
+ // connectionEstablished === true → server ended a live connection → destroy
6288
+ this.connectionEstablished = false;
6292
6289
  this.reconnectAttempt = 0;
6293
6290
  this.reconnectTimer = null;
6294
- this.destroyed = false;
6295
6291
  this.params = Object.assign({}, params);
6296
- this.reconnectInterval = (_a = params.reconnectInterval) !== null && _a !== void 0 ? _a : 3000;
6297
- this.maxReconnectAttempts = (_b = params.maxReconnectAttempts) !== null && _b !== void 0 ? _b : 5;
6298
6292
  }
6299
6293
  connect() {
6300
6294
  if (this.destroyed) return;
6301
6295
  this._clearReconnectTimer();
6296
+ this.connectionEstablished = false;
6297
+ this.reconnectAttempt = 0;
6302
6298
  this._setStatus("connecting");
6303
6299
  this._ensureTransport();
6304
6300
  this.transport.connect(Utils.buildWsUrl(this.params), this.params.authToken);
@@ -6313,14 +6309,12 @@ class ExotelAIAssistController extends EventEmitter {
6313
6309
  var _a, _b;
6314
6310
  const prev = this.params;
6315
6311
  this.params = Object.assign(Object.assign({}, prev), patch);
6316
- const hash = Utils.hash(this.params);
6317
- const prevHash = Utils.hash(prev);
6318
- const hashChanged = hash !== prevHash;
6312
+ const hashChanged = Utils.hash(this.params) !== Utils.hash(prev);
6319
6313
  if (hashChanged) {
6320
- this.reconnectAttempt = 0;
6321
6314
  this._clearReconnectTimer();
6322
- // Destroy (not just disconnect) so the next _ensureTransport call creates
6323
- // a new instance scoped to the new session hash.
6315
+ this.connectionEstablished = false;
6316
+ this.reconnectAttempt = 0;
6317
+ // Tear down the current transport and start fresh with the new params.
6324
6318
  (_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
6325
6319
  this.transport = null;
6326
6320
  this._setStatus("connecting");
@@ -6335,6 +6329,7 @@ class ExotelAIAssistController extends EventEmitter {
6335
6329
  }
6336
6330
  destroy() {
6337
6331
  var _a;
6332
+ if (this.destroyed) return;
6338
6333
  this.destroyed = true;
6339
6334
  this._clearReconnectTimer();
6340
6335
  (_a = this.transport) === null || _a === void 0 ? void 0 : _a.destroy();
@@ -6353,21 +6348,27 @@ class ExotelAIAssistController extends EventEmitter {
6353
6348
  var _a;
6354
6349
  switch (msg.type) {
6355
6350
  case "CONNECTED":
6351
+ this.connectionEstablished = true;
6356
6352
  this.reconnectAttempt = 0;
6357
6353
  this._setStatus("connected");
6358
6354
  this.emit("onCallStart");
6359
6355
  break;
6360
6356
  case "DISCONNECTED":
6361
- this._setStatus("disconnected");
6362
- this.emit("onCallEnd");
6363
- // Close codes 4001 (auth_failed) and 4002 (auth_timeout) are permanent
6364
- // auth failures — reconnecting with the same token would fail again.
6365
- if (msg.code !== 4001 && msg.code !== 4002) {
6357
+ if (this.connectionEstablished) {
6358
+ // Server ended an already-established connection (call ended, stream
6359
+ // not found, auth failure, etc.) — do not retry.
6360
+ this._setStatus("disconnected");
6361
+ this.emit("onCallEnd");
6362
+ this.destroy();
6363
+ } else {
6364
+ // WebSocket never opened — network/DNS/refused error.
6365
+ // Retry up to MAX_RECONNECT_ATTEMPTS then give up.
6366
6366
  this._scheduleReconnect();
6367
6367
  }
6368
6368
  break;
6369
6369
  case "ERROR":
6370
- this._setStatus("error");
6370
+ // onerror fires before onclose; DISCONNECTED will arrive right after
6371
+ // and drive the retry / destroy decision. Just surface the error here.
6371
6372
  this.emit("error", new Error((_a = msg.message) !== null && _a !== void 0 ? _a : "Unknown transport error"));
6372
6373
  break;
6373
6374
  case "MESSAGE":
@@ -6375,6 +6376,32 @@ class ExotelAIAssistController extends EventEmitter {
6375
6376
  break;
6376
6377
  }
6377
6378
  }
6379
+ _scheduleReconnect() {
6380
+ if (this.destroyed) return;
6381
+ if (this.reconnectAttempt >= ExotelAIAssistController.MAX_RECONNECT_ATTEMPTS) {
6382
+ const err = new Error("Connection failed after maximum retries");
6383
+ err.code = "MAX_RECONNECT_EXCEEDED";
6384
+ this._setStatus("error");
6385
+ this.emit("error", err);
6386
+ this.destroy();
6387
+ return;
6388
+ }
6389
+ const delay = Math.min(ExotelAIAssistController.RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt), 30000);
6390
+ this.reconnectAttempt += 1;
6391
+ this._setStatus("connecting");
6392
+ this.reconnectTimer = setTimeout(() => {
6393
+ var _a;
6394
+ if (this.destroyed) return;
6395
+ this.connectionEstablished = false;
6396
+ (_a = this.transport) === null || _a === void 0 ? void 0 : _a.connect(Utils.buildWsUrl(this.params), this.params.authToken);
6397
+ }, delay);
6398
+ }
6399
+ _clearReconnectTimer() {
6400
+ if (this.reconnectTimer !== null) {
6401
+ clearTimeout(this.reconnectTimer);
6402
+ this.reconnectTimer = null;
6403
+ }
6404
+ }
6378
6405
  _handleServerPayload(raw) {
6379
6406
  var _a;
6380
6407
  let parsed;
@@ -6437,39 +6464,14 @@ class ExotelAIAssistController extends EventEmitter {
6437
6464
  }
6438
6465
  }
6439
6466
  }
6440
- _scheduleReconnect() {
6441
- if (this.destroyed) return;
6442
- if (this.status === "error") return;
6443
- if (this.reconnectAttempt >= this.maxReconnectAttempts) {
6444
- const err = new Error("MAX_RECONNECT_EXCEEDED");
6445
- err.code = "MAX_RECONNECT_EXCEEDED";
6446
- this.emit("error", err);
6447
- return;
6448
- }
6449
- const delay = Math.min(this.reconnectInterval * Math.pow(2, this.reconnectAttempt), 30000);
6450
- this.reconnectTimer = setTimeout(() => {
6451
- var _a;
6452
- if (this.destroyed) return;
6453
- this.reconnectAttempt += 1;
6454
- this._setStatus("connecting");
6455
- (_a = this.transport) === null || _a === void 0 ? void 0 : _a.connect(Utils.buildWsUrl(this.params), this.params.authToken);
6456
- }, delay);
6457
- }
6458
- _clearReconnectTimer() {
6459
- if (this.reconnectTimer !== null) {
6460
- clearTimeout(this.reconnectTimer);
6461
- this.reconnectTimer = null;
6462
- }
6463
- }
6464
6467
  _setStatus(status) {
6465
6468
  if (this.status === status) return;
6466
6469
  this.status = status;
6467
6470
  this.emit("statusChange", status);
6468
6471
  }
6469
- _log(...args) {
6470
- console.log("[ExotelAIAssist]", ...args);
6471
- }
6472
6472
  }
6473
+ ExotelAIAssistController.MAX_RECONNECT_ATTEMPTS = 5;
6474
+ ExotelAIAssistController.RECONNECT_BASE_MS = 3000;
6473
6475
 
6474
6476
  function useExotelAIAssist(params) {
6475
6477
  const controllerRef = React.useRef(null);
@@ -6570,7 +6572,7 @@ function styleInject(css, ref) {
6570
6572
  }
6571
6573
  }
6572
6574
 
6573
- var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover{background:#f9fafb;color:#374151}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-muted{color:#6b7280}";
6575
+ var css_248z = ".oa-panel{background:#fff;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif}.oa-panel,.oa-theme-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%}.oa-theme-root{position:relative}.oa-header-title{color:#111827;font-size:16px;font-weight:600;line-height:20px;margin:0}.oa-tabs{width:100%}.oa-tabs-content{flex:1;min-height:0;overflow:hidden}.oa-tabs-content[data-state=inactive]{display:none!important}.oa-tabs-content[data-state=active]{display:flex;flex-direction:column}.oa-tabs-list{border-bottom:1px solid #e5e7eb;display:flex;gap:20px;padding:0}.oa-tabs-trigger{align-items:center;background:transparent;border:none;border-radius:0;color:#6b7280;cursor:pointer;display:inline-flex;font-size:15px;font-weight:500;gap:6px;outline:none;padding:10px 0;position:relative}.oa-tabs-trigger:hover{color:#374151}.oa-tabs-trigger[data-state=active]{color:#2563eb}.oa-tabs-trigger[data-state=active]:after{background:#2563eb;border-radius:2px;bottom:-1px;content:\"\";height:2px;left:0;position:absolute;right:0}.oa-suggestion-card{border:1px solid rgba(17,24,39,.04);border-radius:12px;padding:12px}.oa-suggestion-card--recent-wrapper{animation:oa-suggestion-enter .4s ease-out,oa-border-shimmer 3s ease-in-out .4s infinite;background:linear-gradient(90deg,#7c3aed,#e11d48,#7c3aed);background-size:200% 100%;border-radius:12px;padding:2px}.oa-suggestion-card--recent-wrapper .oa-suggestion-card--recent{border:none}.oa-suggestion-card--recent{animation:none;background:linear-gradient(90deg,#f5f3ff 0,#fef3f2 50%,#fff7ed);border-radius:9px}.oa-suggestion-card--older{background:#f5f5f5;border:1px solid #e5e7eb}.oa-suggestion-card--older .oa-suggestion-text{color:#252525}@keyframes oa-suggestion-enter{0%{opacity:.3;transform:scale(.97)}to{opacity:1;transform:scale(1)}}@keyframes oa-border-shimmer{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.oa-suggestion-text{color:#111827;font-size:13px;line-height:18px;white-space:pre-wrap}.oa-copy-icon{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;color:#6b7280;cursor:pointer;display:inline-flex;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px}.oa-copy-icon:hover{background:#f9fafb;color:#374151}@keyframes oa-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.oa-muted{color:#6b7280}";
6574
6576
  styleInject(css_248z);
6575
6577
 
6576
6578
  function Sparkle() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exotel-npm-dev/exotel-ai-assist",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "AI-powered real-time suggestions, transcript, and sentiment for agent calls",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",