@sendbird/ai-agent-messenger-react 1.31.1 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/dist/index.cjs +187 -187
  2. package/dist/index.d.ts +1507 -121
  3. package/dist/index.js +7487 -5593
  4. package/package.json +4 -4
package/dist/index.d.ts CHANGED
@@ -331,7 +331,7 @@ declare abstract class AIAgentBaseStats {
331
331
  const existing = this.timers.get(key);
332
332
  if (existing?.startTime !== null && existing?.startTime !== undefined) return this;
333
333
 
334
- this.timers.set(key, { startTime: Date.now(), endTime: null });
334
+ this.timers.set(key, { startTime: defaultStatsClock.now(), endTime: null });
335
335
  return this;
336
336
  }
337
337
 
@@ -341,7 +341,7 @@ declare abstract class AIAgentBaseStats {
341
341
  const existing = this.timers.get(key);
342
342
  if (!existing || existing.startTime === null || existing.endTime !== null) return this;
343
343
 
344
- this.timers.set(key, { ...existing, endTime: Date.now() });
344
+ this.timers.set(key, { ...existing, endTime: defaultStatsClock.now() });
345
345
  return this;
346
346
  }
347
347
 
@@ -719,10 +719,11 @@ declare interface AIAgentInterface {
719
719
  /** @internal Agent version for conversation initialization */
720
720
  agentVersion?: number;
721
721
 
722
- /** @internal Stats trackers for performance monitoring */
723
- readonly statsTrackers: {
724
- initialRender: ConversationInitialRenderStatsTracker;
725
- };
722
+ /** @internal Schedules metrics work that should only start while the app is foregrounded. */
723
+ readonly scheduleForegroundTask?: ForegroundTaskScheduler;
724
+
725
+ /** @internal Stats trackers owned by this SDK instance. */
726
+ readonly statsTrackers: AIAgentStatsTrackers;
726
727
 
727
728
  /** Authenticates with the AI Agent server and establishes a session */
728
729
  authenticate: (sessionInfo: ManualSessionInfo | AnonymousSessionInfo) => Promise<MessengerSettingsResponse>;
@@ -797,12 +798,6 @@ declare interface AIAgentMessengerSessionContextValue {
797
798
  deauthenticate: () => Promise<void>;
798
799
 
799
800
  createAttachmentRules: (params: { channel?: ChatSDKGroupChannel; uploadSizeLimit?: number }) => AttachmentRules;
800
- /**
801
- * @internal
802
- */
803
- statsTrackers: {
804
- initialRender: ConversationInitialRenderStatsTracker;
805
- };
806
801
  }
807
802
 
808
803
  declare interface AIAgentMessengerSessionRef {
@@ -1070,6 +1065,23 @@ declare interface AIAgentStatPayload {
1070
1065
  extra?: Record<string, unknown>;
1071
1066
  }
1072
1067
 
1068
+ declare type AIAgentStatsTrackerCollection = {
1069
+ conversationInitialRender: ConversationInitialRenderStatsTracker;
1070
+ conversationNotInteractiveTimeout: ConversationNotInteractiveTimeoutStatsTracker;
1071
+ recoveryNotInteractiveTimeout: RecoveryNotInteractiveTimeoutStatsTracker;
1072
+ conversationListInitialRender: ConversationListInitialRenderStatsTracker;
1073
+ aievAuthExpiredUnhandled: AIEVAuthExpiredUnhandledStatsTracker;
1074
+ messagePendingStuck: MessagePendingStuckStatsTracker;
1075
+ userPerceivedResponseTime: UserPerceivedResponseTimeStatsTracker;
1076
+ streamStalled: StreamStalledStatsTracker;
1077
+ typingIndicatorAbsentTimeout: TypingIndicatorAbsentTimeoutStatsTracker;
1078
+ renderedContentUnusable: RenderedContentUnusableStatsTracker;
1079
+ };
1080
+
1081
+ declare type AIAgentStatsTrackers = AIAgentStatsTrackerCollection & {
1082
+ clear: () => void;
1083
+ };
1084
+
1073
1085
  /**
1074
1086
  * Common string set interface shared between react and react-native packages
1075
1087
  * These are the base strings that both packages use
@@ -1254,6 +1266,33 @@ declare interface AIAgentStringSet {
1254
1266
  };
1255
1267
  }
1256
1268
 
1269
+ declare class AIEVAuthExpiredUnhandledStatsTracker {
1270
+ private readonly callback: StatsAppendCallback;
1271
+
1272
+ constructor(callback: StatsAppendCallback) {
1273
+ this.callback = callback;
1274
+ }
1275
+
1276
+ onPayloadParseFailed(params: { aiAgentId: string }): void {
1277
+ append(this.callback, METRIC_KEY_AIEV_AUTH_EXPIRED_UNHANDLED, 'payload_parse_failed', {
1278
+ extra: { agent_id: params.aiAgentId },
1279
+ });
1280
+ }
1281
+
1282
+ onCallbackNotFound(params: { aiAgentId: string }): void {
1283
+ append(this.callback, METRIC_KEY_AIEV_AUTH_EXPIRED_UNHANDLED, 'callback_not_found', {
1284
+ extra: { agent_id: params.aiAgentId },
1285
+ });
1286
+ }
1287
+
1288
+ clear(): void {}
1289
+ }
1290
+
1291
+ declare type AIResponseMessageMetadata = {
1292
+ isStreaming: boolean;
1293
+ userMessageId?: number;
1294
+ };
1295
+
1257
1296
  declare type AnnouncementQueueMode = 'latestOnly' | 'ordered';
1258
1297
 
1259
1298
  declare interface AnnounceOptions {
@@ -1532,7 +1571,7 @@ declare interface ContextObject {
1532
1571
  context: Record<string, string>;
1533
1572
  }
1534
1573
 
1535
- export declare const Conversation: ({ children, onNavigateToConversationList, channelUrl, onClearChannelUrl, shouldMarkAsRead, announcementsEnabled, initialFocusTarget, style, closedChannelUrl, onClearClosedChannelUrl, }: Props_3) => JSX.Element;
1574
+ export declare const Conversation: ({ children, onNavigateToConversationList, channelUrl, onClearChannelUrl, shouldMarkAsRead, announcementsEnabled, conversationStatsEnabled, initialFocusTarget, style, closedChannelUrl, onClearClosedChannelUrl, }: ConversationProps) => JSX.Element;
1536
1575
 
1537
1576
  export declare const ConversationContext: Context<ConversationContextValue | null>;
1538
1577
 
@@ -1541,11 +1580,13 @@ declare interface ConversationContextProps {
1541
1580
  channelUrl?: string;
1542
1581
  onClearChannelUrl?: () => void;
1543
1582
  shouldMarkAsRead?: boolean;
1583
+ conversationStatsEnabled?: boolean;
1544
1584
  }
1545
1585
 
1546
1586
  export declare const ConversationContextProvider: ({ children, ...props }: PropsWithChildren<ConversationContextProps>) => JSX.Element;
1547
1587
 
1548
1588
  declare interface ConversationContextValue extends AIAgentConversationContextValue {
1589
+ conversationStatsEnabled: boolean;
1549
1590
  scrollSource: ConversationScrollContextValue;
1550
1591
  goToActiveConversation: () => Promise<void>;
1551
1592
  onNavigateToConversationList?: () => void;
@@ -1664,24 +1705,17 @@ declare class ConversationInitialRenderStats extends AIAgentBaseStats {
1664
1705
  const extra: Record<string, unknown> = {
1665
1706
  ...this.extraData,
1666
1707
  present_method: this.presentMethod,
1667
- ...(authDuration !== null && { auth_duration: authDuration }),
1668
- ...(getChannelDuration !== null && { get_channel_duration: getChannelDuration }),
1669
- ...(getMessagesDuration !== null && { get_messages_duration: getMessagesDuration }),
1670
- ...(totalDurationFromCache !== null && { total_duration_from_cache: totalDurationFromCache }),
1671
- };
1672
-
1673
- const payload: AIAgentStatPayload = {
1674
- key: this.getMetricKey(),
1675
- // Use "0" when totalDuration is null (error case)
1676
- value: totalDuration !== null ? String(totalDuration) : '0',
1708
+ ...(authDuration !== null && { [DurationKey.AUTH]: authDuration }),
1709
+ ...(getChannelDuration !== null && { [DurationKey.GET_CHANNEL]: getChannelDuration }),
1710
+ ...(getMessagesDuration !== null && { [DurationKey.GET_MESSAGES]: getMessagesDuration }),
1711
+ ...(totalDurationFromCache !== null && { [DurationKey.TOTAL_DURATION_FROM_CACHE]: totalDurationFromCache }),
1677
1712
  };
1678
1713
 
1679
- if (this.conversationId !== null) payload.conversation_id = this.conversationId;
1680
- if (this.errorCode !== null) payload.error_code = this.errorCode;
1681
- if (this.errorDescription !== null) payload.error_description = this.errorDescription;
1682
- if (Object.keys(extra).length > 0) payload.extra = extra;
1683
-
1684
- return payload;
1714
+ return createStatsPayload(this.getMetricKey(), totalDuration ?? 0, {
1715
+ conversationId: this.conversationId ?? undefined,
1716
+ error: this.errorCode !== null ? { code: this.errorCode, message: this.errorDescription ?? '' } : undefined,
1717
+ extra,
1718
+ });
1685
1719
  }
1686
1720
  }
1687
1721
 
@@ -1690,100 +1724,193 @@ declare class ConversationInitialRenderStats extends AIAgentBaseStats {
1690
1724
  */
1691
1725
  declare class ConversationInitialRenderStatsTracker {
1692
1726
  private readonly commitCallback: StatsAppendCallback;
1727
+ private apiResultReceived = false;
1728
+ private channelUrl?: string;
1729
+ private consumed = false;
1730
+ private consumedByAuthError = false;
1693
1731
  private presentMethod: PresentMethod = 'direct_present';
1732
+ private renderedAfterApiResult = false;
1694
1733
  private stats: ConversationInitialRenderStats | null = null;
1695
1734
 
1696
1735
  constructor(commitCallback: StatsAppendCallback) {
1697
1736
  this.commitCallback = commitCallback;
1698
1737
  }
1699
1738
 
1700
- private getOrCreateStats(): ConversationInitialRenderStats {
1701
- if (!this.stats || this.stats.isCommitted()) {
1739
+ private getOrCreateStats(): ConversationInitialRenderStats | null {
1740
+ if (this.consumed) return null;
1741
+ if (this.stats?.isCommitted()) {
1742
+ this.consumed = true;
1743
+ return null;
1744
+ }
1745
+ if (!this.stats) {
1702
1746
  this.stats = new ConversationInitialRenderStats()
1703
1747
  .setCommitCallback(this.commitCallback)
1704
1748
  .setPresentMethod(this.presentMethod);
1749
+ this.apiResultReceived = false;
1750
+ this.renderedAfterApiResult = false;
1705
1751
  }
1706
1752
  return this.stats;
1707
1753
  }
1708
1754
 
1755
+ private getActiveStats(): ConversationInitialRenderStats | null {
1756
+ if (this.consumed || !this.stats || this.stats.isCommitted()) return null;
1757
+ return this.stats;
1758
+ }
1759
+
1760
+ private matchesChannelUrl(channelUrl?: string): boolean {
1761
+ return channelUrl === undefined || this.channelUrl === undefined || channelUrl === this.channelUrl;
1762
+ }
1763
+
1764
+ private pinChannelUrl(channelUrl?: string): boolean {
1765
+ if (channelUrl === undefined) return true;
1766
+ if (!this.matchesChannelUrl(channelUrl)) return false;
1767
+
1768
+ this.channelUrl = channelUrl;
1769
+ this.stats?.setChannelUrl(channelUrl);
1770
+ return true;
1771
+ }
1772
+
1773
+ private consume(commitResult: boolean): boolean {
1774
+ if (commitResult) {
1775
+ this.consumed = true;
1776
+ }
1777
+ return commitResult;
1778
+ }
1779
+
1780
+ private commitIfReady(channelUrl?: string): boolean {
1781
+ if (this.consumed || !this.stats || this.stats.isCommitted()) return true;
1782
+ if (!this.matchesChannelUrl(channelUrl)) return false;
1783
+ if (!this.apiResultReceived || !this.renderedAfterApiResult) return false;
1784
+ return this.consume(this.stats.stopTimer(DurationKey.TOTAL_DURATION).commit());
1785
+ }
1786
+
1787
+ start(channelUrl?: string): void {
1788
+ const stats = this.getOrCreateStats();
1789
+ if (!stats || !this.pinChannelUrl(channelUrl)) return;
1790
+
1791
+ stats.startTimer(DurationKey.TOTAL_DURATION).startTimer(DurationKey.TOTAL_DURATION_FROM_CACHE);
1792
+ }
1793
+
1709
1794
  onAuthStart(): void {
1710
- this.getOrCreateStats()
1711
- .startTimer(DurationKey.TOTAL_DURATION)
1712
- .startTimer(DurationKey.TOTAL_DURATION_FROM_CACHE)
1713
- .startTimer(DurationKey.AUTH);
1795
+ if (this.consumedByAuthError) {
1796
+ this.clear();
1797
+ }
1798
+ this.getOrCreateStats()?.startTimer(DurationKey.AUTH);
1714
1799
  }
1715
1800
 
1716
1801
  onAuthComplete(): void {
1717
- this.stats?.stopTimer(DurationKey.AUTH);
1802
+ this.getActiveStats()?.stopTimer(DurationKey.AUTH);
1718
1803
  }
1719
1804
 
1720
1805
  onAuthError(error: Error): void {
1721
- if (this.stats && !this.stats.isCommitted()) {
1722
- this.stats
1806
+ const stats = this.getActiveStats();
1807
+ if (stats) {
1808
+ const committed = this.consume(
1809
+ stats
1723
1810
  .stopTimer(DurationKey.AUTH)
1724
1811
  .setError(error instanceof SendbirdError ? error.code : undefined, error.message)
1725
1812
  .stopTimer(DurationKey.TOTAL_DURATION)
1726
- .commit();
1813
+ .commit(),
1814
+ );
1815
+ if (committed) {
1816
+ this.consumedByAuthError = true;
1817
+ }
1727
1818
  }
1728
1819
  }
1729
1820
 
1730
- onGetChannelStart(): void {
1731
- this.stats?.startTimer(DurationKey.GET_CHANNEL);
1821
+ onGetChannelStart(channelUrl?: string): void {
1822
+ const stats = this.getActiveStats();
1823
+ if (!stats || !this.pinChannelUrl(channelUrl)) return;
1824
+
1825
+ stats.startTimer(DurationKey.GET_CHANNEL);
1732
1826
  }
1733
1827
 
1734
- onGetChannelComplete(conversationId?: number): void {
1735
- if (this.stats) {
1736
- this.stats.stopTimer(DurationKey.GET_CHANNEL);
1737
- if (conversationId !== undefined) {
1738
- this.stats.setConversationId(conversationId);
1739
- }
1740
- this.stats.startTimer(DurationKey.GET_MESSAGES);
1828
+ onGetChannelComplete(params?: number | GetChannelCompleteParams): void {
1829
+ const channelUrl = typeof params === 'object' ? params.channelUrl : undefined;
1830
+ const conversationId = typeof params === 'object' ? params.conversationId : params;
1831
+ const stats = this.getActiveStats();
1832
+ if (!stats || !this.pinChannelUrl(channelUrl)) return;
1833
+
1834
+ stats.stopTimer(DurationKey.GET_CHANNEL);
1835
+ if (conversationId !== undefined) {
1836
+ stats.setConversationId(conversationId);
1741
1837
  }
1838
+ stats.startTimer(DurationKey.GET_MESSAGES);
1742
1839
  }
1743
1840
 
1744
- onGetChannelError(error: Error): void {
1745
- if (this.stats && !this.stats.isCommitted()) {
1746
- this.stats
1841
+ onGetChannelError(params: Error | GetChannelErrorParams): void {
1842
+ const channelUrl = params instanceof Error ? undefined : params.channelUrl;
1843
+ const error = params instanceof Error ? params : params.error;
1844
+ const stats = this.getActiveStats();
1845
+ if (stats && this.pinChannelUrl(channelUrl)) {
1846
+ this.consume(
1847
+ stats
1747
1848
  .stopTimer(DurationKey.GET_CHANNEL)
1748
1849
  .setError(error instanceof SendbirdError ? error.code : undefined, error.message)
1749
1850
  .stopTimer(DurationKey.TOTAL_DURATION)
1750
- .commit();
1851
+ .commit(),
1852
+ );
1751
1853
  }
1752
1854
  }
1753
1855
 
1754
1856
  onCacheResult(error: Error | null): void {
1755
- if (this.stats && !this.stats.isCommitted()) {
1857
+ const stats = this.getActiveStats();
1858
+ if (stats) {
1756
1859
  if (error) {
1757
- this.stats.setError(error instanceof SendbirdError ? error.code : undefined, error.message);
1860
+ stats.setError(error instanceof SendbirdError ? error.code : undefined, error.message);
1758
1861
  }
1759
- this.stats.stopTimer(DurationKey.TOTAL_DURATION_FROM_CACHE);
1862
+ stats.stopTimer(DurationKey.TOTAL_DURATION_FROM_CACHE);
1760
1863
  }
1761
1864
  }
1762
1865
 
1763
1866
  onApiResult(error: Error | null): void {
1764
- if (this.stats && !this.stats.isCommitted()) {
1867
+ const stats = this.getActiveStats();
1868
+ if (stats) {
1765
1869
  if (error) {
1766
- this.stats.setError(error instanceof SendbirdError ? error.code : undefined, error.message);
1870
+ this.consume(
1871
+ stats
1872
+ .setError(error instanceof SendbirdError ? error.code : undefined, error.message)
1873
+ .stopTimer(DurationKey.TOTAL_DURATION)
1874
+ .stopTimer(DurationKey.GET_MESSAGES)
1875
+ .commit(),
1876
+ );
1877
+ return;
1767
1878
  }
1768
- this.stats.stopTimer(DurationKey.TOTAL_DURATION).stopTimer(DurationKey.GET_MESSAGES).commit();
1879
+ this.apiResultReceived = true;
1880
+ this.renderedAfterApiResult = false;
1881
+ stats.stopTimer(DurationKey.GET_MESSAGES);
1882
+ this.commitIfReady();
1769
1883
  }
1770
1884
  }
1771
1885
 
1772
1886
  setChannelUrl(url: string): void {
1773
- this.stats?.setChannelUrl(url);
1887
+ this.pinChannelUrl(url);
1888
+ }
1889
+
1890
+ commitPending(): boolean {
1891
+ const stats = this.getActiveStats();
1892
+ return stats ? this.consume(stats.stopTimer(DurationKey.TOTAL_DURATION).commit()) : true;
1774
1893
  }
1775
1894
 
1776
- cleanup(): void {
1777
- this.stats?.commit();
1895
+ markRendered(channelUrl?: string): boolean {
1896
+ if (!this.matchesChannelUrl(channelUrl)) return false;
1897
+
1898
+ this.renderedAfterApiResult = true;
1899
+ return this.commitIfReady(channelUrl);
1778
1900
  }
1779
1901
 
1780
1902
  setPresentMethod(method: PresentMethod): void {
1781
1903
  this.presentMethod = method;
1782
- this.stats?.setPresentMethod(method);
1904
+ this.getActiveStats()?.setPresentMethod(method);
1783
1905
  }
1784
1906
 
1785
1907
  clear(): void {
1786
1908
  this.stats = null;
1909
+ this.apiResultReceived = false;
1910
+ this.channelUrl = undefined;
1911
+ this.consumed = false;
1912
+ this.consumedByAuthError = false;
1913
+ this.renderedAfterApiResult = false;
1787
1914
  }
1788
1915
  }
1789
1916
 
@@ -1826,7 +1953,7 @@ export declare const ConversationLayout: {
1826
1953
  declare interface ConversationLayoutTemplateProps {
1827
1954
  }
1828
1955
 
1829
- export declare const ConversationList: ({ conversationListLimit, conversationListFilter, children, onOpenConversationView, announcementsEnabled, style, }: Props_4) => JSX.Element;
1956
+ export declare const ConversationList: ({ conversationListLimit, conversationListFilter, children, onOpenConversationView, announcementsEnabled, conversationListStatsEnabled, style, }: ConversationListProps) => JSX.Element;
1830
1957
 
1831
1958
  /**
1832
1959
  * Public interface for ConversationListCollection.
@@ -1855,10 +1982,11 @@ export declare const ConversationListContext: Context<ConversationListContextVal
1855
1982
  declare interface ConversationListContextProps {
1856
1983
  conversationListLimit?: number;
1857
1984
  conversationListFilter?: Partial<AIAgentGroupChannelFilter>;
1985
+ conversationListStatsEnabled?: boolean;
1858
1986
  onOpenConversationView?: (channelUrl: string, status: 'open' | 'closed', options?: ConversationListOpenOptions) => void;
1859
1987
  }
1860
1988
 
1861
- export declare function ConversationListContextProvider({ conversationListLimit, conversationListFilter, onOpenConversationView, children, }: PropsWithChildren<ConversationListContextProps>): JSX.Element;
1989
+ export declare function ConversationListContextProvider({ conversationListLimit, conversationListFilter, conversationListStatsEnabled, onOpenConversationView, children, }: PropsWithChildren<ConversationListContextProps>): JSX.Element;
1862
1990
 
1863
1991
  declare interface ConversationListContextValue extends AIAgentConversationListContextValue {
1864
1992
  onOpenConversationView: (channelUrl: string, status: 'open' | 'closed', options?: ConversationListOpenOptions) => void;
@@ -1916,6 +2044,156 @@ declare interface ConversationListHeaderTemplateProps {
1916
2044
  titleAlign?: 'start' | 'center' | 'end';
1917
2045
  }
1918
2046
 
2047
+ declare class ConversationListInitialRenderStatsTracker {
2048
+ private readonly callback: StatsAppendCallback;
2049
+ private readonly clock: StatsClock;
2050
+ private measurement = createEmptyProgress();
2051
+ private renderedCommitted = false;
2052
+ private startedAt: number | null = null;
2053
+ private listStartedAt: number | null = null;
2054
+ private authStartedAt: number | null = null;
2055
+ private authDuration: number | null = null;
2056
+ private listDuration: number | null = null;
2057
+ private presentMethod: PresentMethod = 'direct_present';
2058
+ private committed = false;
2059
+
2060
+ constructor(callback: StatsAppendCallback, options?: TrackerOptions) {
2061
+ this.callback = callback;
2062
+ this.clock = getClock(options);
2063
+ }
2064
+
2065
+ onInitializeStart(): void {
2066
+ if (this.measurement.rendered) return;
2067
+ if (this.measurement.initialized) {
2068
+ this.clearMeasurementState();
2069
+ }
2070
+ this.renderedCommitted = false;
2071
+ this.startMeasurement();
2072
+ }
2073
+
2074
+ onListRequestStart(): void {
2075
+ if (this.measurement.listStarted && !this.measurement.listCompleted && !this.measurement.rendered) {
2076
+ this.clearMeasurementState();
2077
+ }
2078
+
2079
+ this.markListRequestStartOnce();
2080
+ }
2081
+
2082
+ onListRequestComplete(): void {
2083
+ this.markListRequestCompleteOnce();
2084
+ }
2085
+
2086
+ onRendered(): void {
2087
+ this.markRenderedOnce();
2088
+ }
2089
+
2090
+ setPresentMethod(method: PresentMethod): void {
2091
+ this.presentMethod = method;
2092
+ }
2093
+
2094
+ start(presentMethod?: PresentMethod): void {
2095
+ if (this.committed) return;
2096
+ this.startedAt = this.clock.now();
2097
+ if (presentMethod) this.presentMethod = presentMethod;
2098
+ }
2099
+
2100
+ onAuthStart(): void {
2101
+ if (this.committed) {
2102
+ this.clearMeasurementState();
2103
+ this.renderedCommitted = false;
2104
+ }
2105
+ this.authDuration = null;
2106
+ this.authStartedAt = this.clock.now();
2107
+ }
2108
+
2109
+ onAuthComplete(): void {
2110
+ if (this.committed || this.authDuration !== null) return;
2111
+ if (this.authStartedAt === null) {
2112
+ this.authDuration = 0;
2113
+ return;
2114
+ }
2115
+ this.authDuration = this.clock.now() - this.authStartedAt;
2116
+ this.authStartedAt = null;
2117
+ }
2118
+
2119
+ markListRequestStart(): void {
2120
+ if (this.committed) return;
2121
+ if (this.startedAt === null) this.start();
2122
+ this.listStartedAt = this.clock.now();
2123
+ }
2124
+
2125
+ markListRequestComplete(): void {
2126
+ if (this.committed || this.listStartedAt === null) return;
2127
+ this.listDuration = this.clock.now() - this.listStartedAt;
2128
+ }
2129
+
2130
+ markRendered(): void {
2131
+ if (this.committed || this.startedAt === null) return;
2132
+ this.committed = true;
2133
+ append(this.callback, METRIC_KEY_CONVERSATION_LIST_INITIAL_RENDER, this.clock.now() - this.startedAt, {
2134
+ extra: {
2135
+ ...(this.authDuration !== null && { auth_duration_ms: this.authDuration }),
2136
+ ...(this.listDuration !== null && { get_list_duration_ms: this.listDuration }),
2137
+ present_method: this.presentMethod,
2138
+ },
2139
+ });
2140
+ }
2141
+
2142
+ clear(): void {
2143
+ this.clearMeasurementState();
2144
+ this.clearAuthState();
2145
+ this.renderedCommitted = false;
2146
+ }
2147
+
2148
+ private clearMeasurementState(): void {
2149
+ this.startedAt = null;
2150
+ this.listStartedAt = null;
2151
+ this.listDuration = null;
2152
+ this.committed = false;
2153
+ this.measurement = createEmptyProgress();
2154
+ }
2155
+
2156
+ private clearAuthState(): void {
2157
+ this.authStartedAt = null;
2158
+ this.authDuration = null;
2159
+ }
2160
+
2161
+ private startMeasurement(): void {
2162
+ if (this.measurement.initialized) return;
2163
+
2164
+ this.clearMeasurementState();
2165
+ this.start();
2166
+ this.measurement.initialized = true;
2167
+ }
2168
+
2169
+ private markListRequestStartOnce(): void {
2170
+ this.startMeasurement();
2171
+ if (this.measurement.listStarted) return;
2172
+
2173
+ this.markListRequestStart();
2174
+ this.measurement.listStarted = true;
2175
+ }
2176
+
2177
+ private markListRequestCompleteOnce(): void {
2178
+ this.markListRequestStartOnce();
2179
+ if (this.measurement.listCompleted) return;
2180
+
2181
+ this.markListRequestComplete();
2182
+ this.measurement.listCompleted = true;
2183
+ }
2184
+
2185
+ private markRenderedOnce(): void {
2186
+ if (this.renderedCommitted) return;
2187
+
2188
+ this.startMeasurement();
2189
+ if (this.measurement.rendered) return;
2190
+
2191
+ this.markRendered();
2192
+ this.measurement.rendered = true;
2193
+ this.renderedCommitted = true;
2194
+ }
2195
+ }
2196
+
1919
2197
  export declare const ConversationListItemLayout: {
1920
2198
  (props: PropsWithChildren): ReactNode;
1921
2199
  defaults: {
@@ -1980,6 +2258,16 @@ declare interface ConversationListOpenOptions {
1980
2258
  initialFocusTarget?: 'messageInput';
1981
2259
  }
1982
2260
 
2261
+ export declare type ConversationListProps = PropsWithChildren<{
2262
+ conversationListLimit?: number;
2263
+ conversationListFilter?: Partial<AIAgentGroupChannelFilter>;
2264
+ onOpenConversationView?: (channelUrl: string, status: 'open' | 'closed', options?: ConversationListOpenOptions) => void;
2265
+ announcementsEnabled?: boolean;
2266
+ conversationListStatsEnabled?: boolean;
2267
+ /** Custom styles for the conversation list container. */
2268
+ style?: CSSProperties;
2269
+ }>;
2270
+
1983
2271
  declare type ConversationMessageEvent = ConversationMessageEventPayload & { id: number };
1984
2272
 
1985
2273
  declare type ConversationMessageEventPayload =
@@ -1999,6 +2287,147 @@ declare type ConversationMessageEventPayload =
1999
2287
  messages: ChatSDKBaseMessage[];
2000
2288
  };
2001
2289
 
2290
+ declare type ConversationNotInteractiveMeasurement = {
2291
+ timer: TimeoutId | null;
2292
+ startedAt: number;
2293
+ committed: boolean;
2294
+ inputEnabled: boolean;
2295
+ phaseReached: ConversationNotInteractivePhase;
2296
+ historyLoadResult: HistoryLoadResult;
2297
+ conversationId?: number;
2298
+ channelUrl?: string;
2299
+ };
2300
+
2301
+ declare type ConversationNotInteractivePhase = 'auth' | 'channel_join' | 'history_load' | 'input_enable' | 'render';
2302
+
2303
+ declare class ConversationNotInteractiveTimeoutStatsTracker {
2304
+ private readonly callback: StatsAppendCallback;
2305
+ private readonly clock: StatsClock;
2306
+ private readonly setTimeoutFn: typeof setTimeout;
2307
+ private readonly clearTimeoutFn: typeof clearTimeout;
2308
+ private readonly measurements = new Map<string, ConversationNotInteractiveMeasurement>();
2309
+
2310
+ constructor(callback: StatsAppendCallback, options?: TimedTrackerOptions) {
2311
+ this.callback = callback;
2312
+ this.clock = getClock(options);
2313
+ this.setTimeoutFn = getSetTimeout(options);
2314
+ this.clearTimeoutFn = getClearTimeout(options);
2315
+ }
2316
+
2317
+ start(params?: { channelUrl?: string; conversationId?: number }): void {
2318
+ const key = this.getKey(params?.channelUrl);
2319
+ this.clearKey(key);
2320
+
2321
+ const measurement = {
2322
+ startedAt: this.clock.now(),
2323
+ channelUrl: params?.channelUrl,
2324
+ conversationId: params?.conversationId,
2325
+ committed: false,
2326
+ inputEnabled: false,
2327
+ phaseReached: 'auth' as ConversationNotInteractivePhase,
2328
+ historyLoadResult: 'pending' as HistoryLoadResult,
2329
+ timer: null as TimeoutId | null,
2330
+ };
2331
+ measurement.timer = this.setTimeoutFn(() => this.commitTimeout(key), 10_000);
2332
+ this.measurements.set(key, measurement);
2333
+ }
2334
+
2335
+ markChannelJoined(conversationId?: number, channelUrl?: string): void {
2336
+ const measurement = this.getMeasurement(channelUrl);
2337
+ if (!measurement || measurement.committed) return;
2338
+ measurement.phaseReached = 'channel_join';
2339
+ if (conversationId !== undefined) measurement.conversationId = conversationId;
2340
+ }
2341
+
2342
+ markHistoryLoaded(result: HistoryLoadResult, channelUrl?: string): void {
2343
+ const measurement = this.getMeasurement(channelUrl);
2344
+ if (!measurement || measurement.committed) return;
2345
+ measurement.phaseReached = 'history_load';
2346
+ measurement.historyLoadResult = result;
2347
+ this.cancelIfReady(channelUrl);
2348
+ }
2349
+
2350
+ markInputEnabled(channelUrl?: string): void {
2351
+ const measurement = this.getMeasurement(channelUrl);
2352
+ if (!measurement || measurement.committed) return;
2353
+ measurement.phaseReached = 'input_enable';
2354
+ measurement.inputEnabled = true;
2355
+ this.cancelIfReady(channelUrl);
2356
+ }
2357
+
2358
+ markRendered(channelUrl?: string): void {
2359
+ const measurement = this.getMeasurement(channelUrl);
2360
+ if (!measurement || measurement.committed) return;
2361
+ measurement.phaseReached = 'render';
2362
+ }
2363
+
2364
+ clear(): void {
2365
+ this.measurements.forEach((measurement) => clearTimer(measurement.timer, this.clearTimeoutFn));
2366
+ this.measurements.clear();
2367
+ }
2368
+
2369
+ clearChannel(channelUrl?: string): void {
2370
+ if (channelUrl === undefined) return;
2371
+ this.clearKey(this.getKey(channelUrl));
2372
+ }
2373
+
2374
+ private clearKey(key: string): void {
2375
+ const measurement = this.measurements.get(key);
2376
+ if (!measurement) return;
2377
+ clearTimer(measurement.timer, this.clearTimeoutFn);
2378
+ this.measurements.delete(key);
2379
+ }
2380
+
2381
+ private cancelIfReady(channelUrl?: string): void {
2382
+ const measurement = this.getMeasurement(channelUrl);
2383
+ if (!measurement || !measurement.inputEnabled || measurement.historyLoadResult === 'pending') return;
2384
+ this.clearKey(this.getKey(measurement.channelUrl));
2385
+ }
2386
+
2387
+ private commitTimeout(key: string): void {
2388
+ const measurement = this.measurements.get(key);
2389
+ if (!measurement || measurement.committed) return;
2390
+ measurement.committed = true;
2391
+ measurement.timer = null;
2392
+ append(this.callback, METRIC_KEY_CONVERSATION_NOT_INTERACTIVE_TIMEOUT, this.clock.now() - measurement.startedAt, {
2393
+ conversationId: measurement.conversationId,
2394
+ error: { message: METRIC_KEY_CONVERSATION_NOT_INTERACTIVE_TIMEOUT },
2395
+ extra: {
2396
+ outcome: 'timeout',
2397
+ phase_reached: measurement.phaseReached,
2398
+ history_load_result: measurement.historyLoadResult,
2399
+ },
2400
+ });
2401
+ this.measurements.delete(key);
2402
+ }
2403
+
2404
+ private getMeasurement(channelUrl?: string): ConversationNotInteractiveMeasurement | undefined {
2405
+ if (channelUrl !== undefined) return this.measurements.get(this.getKey(channelUrl));
2406
+ if (this.measurements.size === 1) return Array.from(this.measurements.values())[0];
2407
+ return this.measurements.get(this.getKey());
2408
+ }
2409
+
2410
+ private getKey(channelUrl?: string): string {
2411
+ return channelUrl ?? '__default__';
2412
+ }
2413
+ }
2414
+
2415
+ export declare type ConversationProps = PropsWithChildren<{
2416
+ channelUrl?: string;
2417
+ onClearChannelUrl?: () => void;
2418
+ onNavigateToConversationList?: () => void;
2419
+ shouldMarkAsRead?: boolean;
2420
+ announcementsEnabled?: boolean;
2421
+ conversationStatsEnabled?: boolean;
2422
+ initialFocusTarget?: ConversationInitialFocusTarget;
2423
+ /** Custom styles for the conversation container. */
2424
+ style?: CSSProperties;
2425
+ /** @deprecated Please use `channelUrl` instead. **/
2426
+ closedChannelUrl?: string;
2427
+ /** @deprecated Please use `onClearChannelUrl` instead. **/
2428
+ onClearClosedChannelUrl?: () => void;
2429
+ }>;
2430
+
2002
2431
  declare interface ConversationScrollContextValue {
2003
2432
  ref: RefObject<HTMLDivElement | null> | ((element: HTMLDivElement | null) => void);
2004
2433
  state: {
@@ -2185,12 +2614,22 @@ declare interface Dispatcher {
2185
2614
  ): void;
2186
2615
  }
2187
2616
 
2617
+ declare enum DurationKey {
2618
+ AUTH = 'auth_duration_ms',
2619
+ GET_CHANNEL = 'get_channel_duration_ms',
2620
+ GET_MESSAGES = 'get_messages_duration_ms',
2621
+ TOTAL_DURATION = 'total_duration_ms',
2622
+ TOTAL_DURATION_FROM_CACHE = 'total_duration_from_cache_ms',
2623
+ }
2624
+
2188
2625
  declare type ErrorAnnouncementParams =
2189
2626
  | { type: 'fileSizeExceeded'; maxSizeMB?: number }
2190
2627
  | { type: 'sendFailed' }
2191
2628
  | { type: 'csatFormError' }
2192
2629
  | { type: 'generic'; message: string };
2193
2630
 
2631
+ declare type ErrorLike = StatsPayloadError;
2632
+
2194
2633
  /**
2195
2634
  * For better understanding: https://sendbird.atlassian.net/wiki/spaces/AA/pages/3075014695/Extended+Message+Payload+Spec
2196
2635
  */
@@ -2281,10 +2720,20 @@ export declare interface FeedbackInfo {
2281
2720
 
2282
2721
  export declare const FixedMessenger: ForwardRefExoticComponent<MessengerProps & RefAttributes<MessengerSessionRef>> & {
2283
2722
  Style: (props: FixedMessengerStyleProps) => null;
2723
+ Conversation: ({ component }: FixedMessengerConversationProps) => null;
2284
2724
  ConversationChildren: ({ children }: PropsWithChildren) => null;
2725
+ ConversationList: ({ component }: FixedMessengerConversationListProps) => null;
2285
2726
  ConversationListChildren: ({ children }: PropsWithChildren) => null;
2286
2727
  };
2287
2728
 
2729
+ declare interface FixedMessengerConversationListProps {
2730
+ component: ComponentType<ConversationListProps>;
2731
+ }
2732
+
2733
+ declare interface FixedMessengerConversationProps {
2734
+ component: ComponentType<ConversationProps>;
2735
+ }
2736
+
2288
2737
  export declare type FixedMessengerMargin = {
2289
2738
  top: number;
2290
2739
  bottom: number;
@@ -2314,6 +2763,8 @@ declare interface FixedMessengerStyleProps {
2314
2763
  launcherSize?: number;
2315
2764
  }
2316
2765
 
2766
+ declare type ForegroundTaskScheduler = (start: () => void) => void | (() => void);
2767
+
2317
2768
  declare interface Form {
2318
2769
  key: string;
2319
2770
  version: number;
@@ -2343,6 +2794,16 @@ export declare interface FunctionCallsInfo {
2343
2794
  };
2344
2795
  }
2345
2796
 
2797
+ declare type GetChannelCompleteParams = {
2798
+ channelUrl?: string;
2799
+ conversationId?: number;
2800
+ };
2801
+
2802
+ declare type GetChannelErrorParams = {
2803
+ channelUrl?: string;
2804
+ error: Error;
2805
+ };
2806
+
2346
2807
  export declare interface GroundednessInfo {
2347
2808
  id: number;
2348
2809
  source_type:
@@ -2365,6 +2826,8 @@ export declare interface GroundednessInfo {
2365
2826
  */
2366
2827
  declare type HexColor = `#${string}`;
2367
2828
 
2829
+ declare type HistoryLoadResult = 'success' | 'verified_empty' | 'failed' | 'pending';
2830
+
2368
2831
  export declare type IconComponent = AIAgentIconComponent;
2369
2832
 
2370
2833
  export declare type IconComponentProps = AIAgentIconComponentProps;
@@ -2446,13 +2909,8 @@ export declare const IncomingMessageLayout: {
2446
2909
  };
2447
2910
  }) => ReactNode;
2448
2911
  SuggestedReplies: ({ extendedMessagePayload, onClickSuggestedReply, suggestedRepliesDirection, }: IncomingMessageProps) => ReactNode;
2449
- MessageTemplate: (props: {
2450
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
2451
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
2452
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
2453
- messageTemplateErrorFallback?: ReactNode;
2454
- messageTemplateLoadingFallback?: ReactNode;
2455
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
2912
+ MessageTemplate: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
2913
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
2456
2914
  }) => ReactNode;
2457
2915
  CustomMessageTemplate: (_: ({
2458
2916
  messageType: "user";
@@ -2916,7 +3374,9 @@ export declare const IncomingMessageLayout: {
2916
3374
  })) => ReactNode;
2917
3375
  CTAButton: ({ extendedMessagePayload, onClickCTA, }: IncomingMessageProps) => ReactNode;
2918
3376
  Citation: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
2919
- Form: (props: IncomingMessageProps) => ReactNode;
3377
+ Form: (props: IncomingMessageProps & {
3378
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
3379
+ }) => ReactNode;
2920
3380
  Feedback: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
2921
3381
  MessageLogs: (_: IncomingMessageProps) => ReactNode;
2922
3382
  };
@@ -2941,13 +3401,8 @@ export declare const IncomingMessageLayout: {
2941
3401
  };
2942
3402
  }) => ReactNode;
2943
3403
  SuggestedReplies: ({ extendedMessagePayload, onClickSuggestedReply, suggestedRepliesDirection, }: IncomingMessageProps) => ReactNode;
2944
- MessageTemplate: (props: {
2945
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
2946
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
2947
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
2948
- messageTemplateErrorFallback?: ReactNode;
2949
- messageTemplateLoadingFallback?: ReactNode;
2950
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
3404
+ MessageTemplate: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
3405
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
2951
3406
  }) => ReactNode;
2952
3407
  CustomMessageTemplate: (_: ({
2953
3408
  messageType: "user";
@@ -3411,7 +3866,9 @@ export declare const IncomingMessageLayout: {
3411
3866
  })) => ReactNode;
3412
3867
  CTAButton: ({ extendedMessagePayload, onClickCTA, }: IncomingMessageProps) => ReactNode;
3413
3868
  Citation: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
3414
- Form: (props: IncomingMessageProps) => ReactNode;
3869
+ Form: (props: IncomingMessageProps & {
3870
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
3871
+ }) => ReactNode;
3415
3872
  Feedback: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
3416
3873
  MessageLogs: (_: IncomingMessageProps) => ReactNode;
3417
3874
  }>>;
@@ -3431,13 +3888,8 @@ export declare const IncomingMessageLayout: {
3431
3888
  };
3432
3889
  }) => ReactNode;
3433
3890
  SuggestedReplies: ({ extendedMessagePayload, onClickSuggestedReply, suggestedRepliesDirection, }: IncomingMessageProps) => ReactNode;
3434
- MessageTemplate: (props: {
3435
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
3436
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
3437
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
3438
- messageTemplateErrorFallback?: ReactNode;
3439
- messageTemplateLoadingFallback?: ReactNode;
3440
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
3891
+ MessageTemplate: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
3892
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
3441
3893
  }) => ReactNode;
3442
3894
  CustomMessageTemplate: (_: ({
3443
3895
  messageType: "user";
@@ -3901,19 +4353,16 @@ export declare const IncomingMessageLayout: {
3901
4353
  })) => ReactNode;
3902
4354
  CTAButton: ({ extendedMessagePayload, onClickCTA, }: IncomingMessageProps) => ReactNode;
3903
4355
  Citation: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
3904
- Form: (props: IncomingMessageProps) => ReactNode;
4356
+ Form: (props: IncomingMessageProps & {
4357
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
4358
+ }) => ReactNode;
3905
4359
  Feedback: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
3906
4360
  MessageLogs: (_: IncomingMessageProps) => ReactNode;
3907
4361
  }>;
3908
4362
  } & {
3909
4363
  MessageTemplate: (props: {
3910
- component: (props: {
3911
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
3912
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
3913
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
3914
- messageTemplateErrorFallback?: ReactNode;
3915
- messageTemplateLoadingFallback?: ReactNode;
3916
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
4364
+ component: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
4365
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
3917
4366
  }) => ReactNode;
3918
4367
  }) => null;
3919
4368
  SenderName: (props: {
@@ -4422,7 +4871,9 @@ export declare const IncomingMessageLayout: {
4422
4871
  component: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
4423
4872
  }) => null;
4424
4873
  Form: (props: {
4425
- component: (props: IncomingMessageProps) => ReactNode;
4874
+ component: (props: IncomingMessageProps & {
4875
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
4876
+ }) => ReactNode;
4426
4877
  }) => null;
4427
4878
  Feedback: (props: {
4428
4879
  component: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
@@ -4435,6 +4886,13 @@ export declare const IncomingMessageLayout: {
4435
4886
  export declare type IncomingMessageProps<T extends IncomingMessageUnion['messageType'] = IncomingMessageUnion['messageType']> =
4436
4887
  PickMessageProps<IncomingMessageUnion, T>;
4437
4888
 
4889
+ declare type IncomingMessageStatsCallbacks = {
4890
+ onVisibleTextProgress?: (displayedLength: number) => void;
4891
+ onRenderedTextProgress?: (displayedLength: number) => void;
4892
+ onMessageTemplateError?: (error: Error) => void;
4893
+ onFormRenderError?: (error: Error) => void;
4894
+ };
4895
+
4438
4896
  declare type IncomingMessageTemplateProps = IncomingMessageProps & Partial<InternalExtraProps>;
4439
4897
 
4440
4898
  declare type IncomingMessageUnion =
@@ -4474,7 +4932,7 @@ declare type InputState =
4474
4932
 
4475
4933
  declare type InternalExtraProps = {
4476
4934
  maxBodyWidth?: number;
4477
- testerMode?: boolean;
4935
+ statsCallbacks?: IncomingMessageStatsCallbacks;
4478
4936
  };
4479
4937
 
4480
4938
  declare type InternalExtraProps_2 = {
@@ -4617,6 +5075,95 @@ export declare const MessageListUILayout: {
4617
5075
 
4618
5076
  export declare const MessageLogs: ({ actionbook, functionCalls, groundedness, agentMessageTemplates, flaggedTypes, onClickActionbook, onClickFunctionCall, onClickFunctionCallDetail, onClickGroundedness, onClickAgentMessageTemplate, topContent, bottomContent, renderCustomGroundednessIcon, style, }: Props_2) => JSX.Element;
4619
5077
 
5078
+ declare class MessagePendingStuckStatsTracker {
5079
+ private readonly callback: StatsAppendCallback;
5080
+ private readonly setTimeoutFn: typeof setTimeout;
5081
+ private readonly clearTimeoutFn: typeof clearTimeout;
5082
+ private readonly measurements = new Map<string, PendingMeasurement>();
5083
+
5084
+ constructor(callback: StatsAppendCallback, options?: PendingStuckTrackerOptions) {
5085
+ this.callback = callback;
5086
+ this.setTimeoutFn = getSetTimeout(options);
5087
+ this.clearTimeoutFn = getClearTimeout(options);
5088
+ }
5089
+
5090
+ start(params: {
5091
+ requestId: string;
5092
+ channelUrl?: string;
5093
+ messageType: PendingMessageType;
5094
+ tracksUploadProgress?: boolean;
5095
+ }): void {
5096
+ if (!params.requestId || this.measurements.has(params.requestId)) return;
5097
+
5098
+ const usesUploadProgress = params.messageType !== 'text' && params.tracksUploadProgress === true;
5099
+ const measurement: PendingMeasurement = {
5100
+ channelUrl: params.channelUrl,
5101
+ messageType: params.messageType,
5102
+ stuckReason: usesUploadProgress ? 'upload_no_progress' : 'pending_too_long',
5103
+ timer: null,
5104
+ };
5105
+ const timeoutMs = usesUploadProgress ? FILE_UPLOAD_NO_PROGRESS_TIMEOUT_MS : TEXT_PENDING_TIMEOUT_MS;
5106
+ measurement.timer = this.setTimeoutFn(() => this.detect(params.requestId), timeoutMs);
5107
+ this.measurements.set(params.requestId, measurement);
5108
+ }
5109
+
5110
+ resolve(params: { requestId: string }): void {
5111
+ const measurement = this.measurements.get(params.requestId);
5112
+ if (!measurement) return;
5113
+
5114
+ clearTimer(measurement.timer, this.clearTimeoutFn);
5115
+ this.measurements.delete(params.requestId);
5116
+ }
5117
+
5118
+ disappear(params: { requestId: string }): void {
5119
+ const measurement = this.measurements.get(params.requestId);
5120
+ if (!measurement) return;
5121
+
5122
+ clearTimer(measurement.timer, this.clearTimeoutFn);
5123
+ this.measurements.delete(params.requestId);
5124
+ }
5125
+
5126
+ markUploadProgress(requestId: string): void {
5127
+ const measurement = this.measurements.get(requestId);
5128
+ if (!measurement) return;
5129
+ if (measurement.stuckReason !== 'upload_no_progress') return;
5130
+
5131
+ clearTimer(measurement.timer, this.clearTimeoutFn);
5132
+ measurement.timer = this.setTimeoutFn(() => this.detect(requestId), FILE_UPLOAD_NO_PROGRESS_TIMEOUT_MS);
5133
+ }
5134
+
5135
+ clear(): void {
5136
+ this.measurements.forEach((measurement) => clearTimer(measurement.timer, this.clearTimeoutFn));
5137
+ this.measurements.clear();
5138
+ }
5139
+
5140
+ clearChannel(channelUrl?: string): void {
5141
+ if (!channelUrl) return;
5142
+ this.measurements.forEach((measurement, requestId) => {
5143
+ if (measurement.channelUrl !== channelUrl) return;
5144
+ clearTimer(measurement.timer, this.clearTimeoutFn);
5145
+ this.measurements.delete(requestId);
5146
+ });
5147
+ }
5148
+
5149
+ private detect(requestId: string): void {
5150
+ const measurement = this.measurements.get(requestId);
5151
+ if (!measurement) return;
5152
+
5153
+ this.measurements.delete(requestId);
5154
+ this.commit(requestId, measurement);
5155
+ }
5156
+
5157
+ private commit(requestId: string, measurement: PendingMeasurement): void {
5158
+ append(this.callback, METRIC_KEY_MESSAGE_PENDING_STUCK, measurement.stuckReason, {
5159
+ extra: {
5160
+ message_type: measurement.messageType,
5161
+ request_id: requestId,
5162
+ },
5163
+ });
5164
+ }
5165
+ }
5166
+
4620
5167
  declare interface MessageTemplateCache {
4621
5168
  set(key: string, value: string): void;
4622
5169
  get(key: string): string | null;
@@ -4881,6 +5428,7 @@ export declare const OutgoingMessageLayout: {
4881
5428
  MediaMessageBody: typeof OutgoingImageBody;
4882
5429
  FileMessageBody: typeof OutgoingFileBody;
4883
5430
  MultipleFilesMessageBody: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
5431
+ MessageLogs: (_: OutgoingMessageProps) => ReactNode;
4884
5432
  };
4885
5433
  };
4886
5434
  Template: ({ template, children }: {
@@ -4895,6 +5443,7 @@ export declare const OutgoingMessageLayout: {
4895
5443
  MediaMessageBody: typeof OutgoingImageBody;
4896
5444
  FileMessageBody: typeof OutgoingFileBody;
4897
5445
  MultipleFilesMessageBody: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
5446
+ MessageLogs: (_: OutgoingMessageProps) => ReactNode;
4898
5447
  }>>;
4899
5448
  useContext: () => LayoutContextValue<OutgoingMessageProps, {
4900
5449
  SendingStatus: ({ sendingStatus }: OutgoingMessageProps) => ReactNode;
@@ -4904,6 +5453,7 @@ export declare const OutgoingMessageLayout: {
4904
5453
  MediaMessageBody: typeof OutgoingImageBody;
4905
5454
  FileMessageBody: typeof OutgoingFileBody;
4906
5455
  MultipleFilesMessageBody: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
5456
+ MessageLogs: (_: OutgoingMessageProps) => ReactNode;
4907
5457
  }>;
4908
5458
  } & {
4909
5459
  SentTime: (props: {
@@ -4924,6 +5474,9 @@ export declare const OutgoingMessageLayout: {
4924
5474
  MultipleFilesMessageBody: (props: {
4925
5475
  component: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
4926
5476
  }) => null;
5477
+ MessageLogs: (props: {
5478
+ component: (_: OutgoingMessageProps) => ReactNode;
5479
+ }) => null;
4927
5480
  SendingStatus: (props: {
4928
5481
  component: ({ sendingStatus }: OutgoingMessageProps) => ReactNode;
4929
5482
  }) => null;
@@ -5001,6 +5554,31 @@ declare interface PaletteTextEmphasis {
5001
5554
  textDisabled: string;
5002
5555
  }
5003
5556
 
5557
+ declare type PendingAIResponseRaw = {
5558
+ messageId: number;
5559
+ channelUrl?: string;
5560
+ aiResponse: AIResponseMessageMetadata;
5561
+ firstChunkAt: number;
5562
+ };
5563
+
5564
+ declare type PendingAIResponseStart = {
5565
+ channelUrl?: string;
5566
+ startedAt: number;
5567
+ };
5568
+
5569
+ declare type PendingMeasurement = {
5570
+ channelUrl?: string;
5571
+ messageType: PendingMessageType;
5572
+ stuckReason: PendingStuckReason;
5573
+ timer: TimeoutId | null;
5574
+ };
5575
+
5576
+ declare type PendingMessageType = 'text' | 'file_image' | 'file_video' | 'file_document' | 'file_audio';
5577
+
5578
+ declare type PendingStuckReason = 'pending_too_long' | 'upload_no_progress';
5579
+
5580
+ declare type PendingStuckTrackerOptions = Pick<TimedTrackerOptions, 'setTimeout' | 'clearTimeout'>;
5581
+
5004
5582
  declare type PickMessageProps<
5005
5583
  Union,
5006
5584
  T extends Union extends { messageType: infer MT } ? MT : never = Union extends { messageType: infer MT } ? MT : never,
@@ -5081,7 +5659,7 @@ export declare type PositionHorizontal = 'start' | 'end';
5081
5659
 
5082
5660
  export declare type PositionVertical = 'top' | 'bottom';
5083
5661
 
5084
- declare type PresentMethod = 'launcher_toggle' | 'direct_present';
5662
+ declare type PresentMethod = 'launcher_toggle' | 'direct_present' | 'embedded';
5085
5663
 
5086
5664
  declare type Props = {
5087
5665
  children: ReactNode;
@@ -5136,31 +5714,121 @@ declare type Props_2 = {
5136
5714
  }) => ReactNode;
5137
5715
  };
5138
5716
 
5139
- declare type Props_3 = PropsWithChildren<{
5140
- channelUrl?: string;
5141
- onClearChannelUrl?: () => void;
5142
- onNavigateToConversationList?: () => void;
5143
- shouldMarkAsRead?: boolean;
5144
- announcementsEnabled?: boolean;
5145
- initialFocusTarget?: ConversationInitialFocusTarget;
5146
- /** Custom styles for the conversation container. */
5147
- style?: CSSProperties;
5148
- /** @deprecated Please use `channelUrl` instead. **/
5149
- closedChannelUrl?: string;
5150
- /** @deprecated Please use `onClearChannelUrl` instead. **/
5151
- onClearClosedChannelUrl?: () => void;
5152
- }>;
5717
+ declare type ReactOnlyIconName = 'expand' | 'collapse' | 'chevron-right' | 'attach' | 'close-filled' | 'actionbook' | 'function' | 'confluence' | 'zendesk' | 'salesforce' | 'sprinklr' | 'website' | 'snippet' | 'template' | 'show' | 'mute' | 'activity';
5153
5718
 
5154
- declare type Props_4 = PropsWithChildren<{
5155
- conversationListLimit?: number;
5156
- conversationListFilter?: Partial<AIAgentGroupChannelFilter>;
5157
- onOpenConversationView?: (channelUrl: string, status: 'open' | 'closed', options?: ConversationListOpenOptions) => void;
5158
- announcementsEnabled?: boolean;
5159
- /** Custom styles for the conversation list container. */
5160
- style?: CSSProperties;
5161
- }>;
5719
+ declare type RecoveryMeasurement = {
5720
+ timer: TimeoutId | null;
5721
+ startedAt: number | null;
5722
+ triggerType: RecoveryTriggerType | null;
5723
+ inputEnabled: boolean;
5724
+ };
5162
5725
 
5163
- declare type ReactOnlyIconName = 'expand' | 'collapse' | 'chevron-right' | 'attach' | 'close-filled' | 'actionbook' | 'function' | 'confluence' | 'zendesk' | 'salesforce' | 'sprinklr' | 'website' | 'snippet' | 'template' | 'show' | 'mute' | 'activity';
5726
+ declare class RecoveryNotInteractiveTimeoutStatsTracker {
5727
+ private readonly callback: StatsAppendCallback;
5728
+ private readonly clock: StatsClock;
5729
+ private readonly setTimeoutFn: typeof setTimeout;
5730
+ private readonly clearTimeoutFn: typeof clearTimeout;
5731
+ private readonly measurements = new Map<string, RecoveryMeasurement>();
5732
+
5733
+ constructor(callback: StatsAppendCallback, options?: TimedTrackerOptions) {
5734
+ this.callback = callback;
5735
+ this.clock = getClock(options);
5736
+ this.setTimeoutFn = getSetTimeout(options);
5737
+ this.clearTimeoutFn = getClearTimeout(options);
5738
+ }
5739
+
5740
+ start(triggerType: RecoveryTriggerType, channelUrl?: string): void {
5741
+ const key = this.getKey(channelUrl);
5742
+ let measurement = this.measurements.get(key);
5743
+ if (measurement && measurement.startedAt !== null) return;
5744
+ if (measurement?.inputEnabled) return;
5745
+
5746
+ measurement = {
5747
+ startedAt: this.clock.now(),
5748
+ triggerType,
5749
+ inputEnabled: measurement?.inputEnabled ?? false,
5750
+ timer: this.setTimeoutFn(() => this.commitTimeout(key), 10_000),
5751
+ };
5752
+ this.measurements.set(key, measurement);
5753
+ }
5754
+
5755
+ setInputEnabled(enabled: boolean, channelUrl?: string): void {
5756
+ const key = this.getKey(channelUrl);
5757
+ let measurement = this.measurements.get(key);
5758
+
5759
+ if (!measurement) {
5760
+ if (!enabled) return;
5761
+ measurement = {
5762
+ startedAt: null,
5763
+ triggerType: null,
5764
+ inputEnabled: enabled,
5765
+ timer: null,
5766
+ };
5767
+ this.measurements.set(key, measurement);
5768
+ }
5769
+
5770
+ measurement.inputEnabled = enabled;
5771
+ if (enabled) {
5772
+ this.cancelMeasurement(measurement);
5773
+ }
5774
+ }
5775
+
5776
+ cancelPending(channelUrl: string): void {
5777
+ const key = this.getKey(channelUrl);
5778
+ const measurement = this.measurements.get(key);
5779
+ if (!measurement) return;
5780
+ this.cancelMeasurement(measurement);
5781
+ if (!measurement.inputEnabled) this.measurements.delete(key);
5782
+ }
5783
+
5784
+ clear(): void {
5785
+ this.measurements.forEach((measurement) => clearTimer(measurement.timer, this.clearTimeoutFn));
5786
+ this.measurements.clear();
5787
+ }
5788
+
5789
+ clearChannel(channelUrl?: string): void {
5790
+ if (channelUrl === undefined) return;
5791
+ this.clearKey(this.getKey(channelUrl));
5792
+ }
5793
+
5794
+ private clearKey(key: string): void {
5795
+ const measurement = this.measurements.get(key);
5796
+ if (!measurement) return;
5797
+ clearTimer(measurement.timer, this.clearTimeoutFn);
5798
+ this.measurements.delete(key);
5799
+ }
5800
+
5801
+ private commitTimeout(key: string): void {
5802
+ const measurement = this.measurements.get(key);
5803
+ if (!measurement || measurement.startedAt === null || measurement.triggerType === null) return;
5804
+ if (measurement.inputEnabled) {
5805
+ this.cancelMeasurement(measurement);
5806
+ return;
5807
+ }
5808
+ const duration = this.clock.now() - measurement.startedAt;
5809
+ const triggerType = measurement.triggerType;
5810
+ // Drop the entry so a subsequent trigger can record a fresh cycle.
5811
+ this.measurements.delete(key);
5812
+ append(this.callback, METRIC_KEY_RECOVERY_NOT_INTERACTIVE_TIMEOUT, duration, {
5813
+ extra: {
5814
+ trigger_type: triggerType,
5815
+ },
5816
+ });
5817
+ }
5818
+
5819
+ private cancelMeasurement(measurement: RecoveryMeasurement): void {
5820
+ clearTimer(measurement.timer, this.clearTimeoutFn);
5821
+ measurement.timer = null;
5822
+ measurement.startedAt = null;
5823
+ measurement.triggerType = null;
5824
+ }
5825
+
5826
+ private getKey(channelUrl?: string): string {
5827
+ return channelUrl ?? '__default__';
5828
+ }
5829
+ }
5830
+
5831
+ declare type RecoveryTriggerType = 'foreground_resume' | 'ws_reconnected';
5164
5832
 
5165
5833
  declare type ReleaseDisabledByValue =
5166
5834
  | 'unavailable'
@@ -5172,6 +5840,39 @@ declare type ReleaseDisabledByValue =
5172
5840
  | 'reconnecting'
5173
5841
  | 'handoff_pending';
5174
5842
 
5843
+ declare class RenderedContentUnusableStatsTracker {
5844
+ private readonly callback: StatsAppendCallback;
5845
+ private readonly committed = new Set<string>();
5846
+
5847
+ constructor(callback: StatsAppendCallback) {
5848
+ this.callback = callback;
5849
+ }
5850
+
5851
+ commit(params: {
5852
+ messageId: string | number;
5853
+ contentKind: 'text' | 'image' | 'video' | 'audio' | 'template' | 'form';
5854
+ errorDomain: 'empty_render' | 'template_error' | 'form_error';
5855
+ error?: ErrorLike;
5856
+ }): void {
5857
+ const key = `${params.messageId}:${params.contentKind}:${params.errorDomain}`;
5858
+ if (this.committed.has(key)) return;
5859
+ evictOldestIfAtLimit(this.committed, RENDERED_CONTENT_UNUSABLE_COMMITTED_LIMIT);
5860
+ this.committed.add(key);
5861
+
5862
+ append(this.callback, METRIC_KEY_RENDERED_CONTENT_UNUSABLE, params.errorDomain, {
5863
+ error: params.error ? { code: params.error.code, message: params.errorDomain } : undefined,
5864
+ extra: {
5865
+ content_kind: params.contentKind,
5866
+ message_id: String(params.messageId),
5867
+ },
5868
+ });
5869
+ }
5870
+
5871
+ clear(): void {
5872
+ this.committed.clear();
5873
+ }
5874
+ }
5875
+
5175
5876
  declare interface ResolvedHandlers extends AgentClientHandlers {
5176
5877
  onClickLink: (params: {
5177
5878
  url: string;
@@ -5213,6 +5914,15 @@ declare type SingleSelectField = {
5213
5914
 
5214
5915
  declare type StatsAppendCallback = (type: string, data: AIAgentStatPayload) => boolean;
5215
5916
 
5917
+ declare interface StatsClock {
5918
+ now: () => number;
5919
+ }
5920
+
5921
+ declare type StatsPayloadError = {
5922
+ code?: number;
5923
+ message?: string;
5924
+ };
5925
+
5216
5926
  /** Per-event override for the platform `announceStatus` wrappers — same shape as `AnnounceOptions`. */
5217
5927
  declare type StatusAnnouncementOptions = AnnounceOptions;
5218
5928
 
@@ -5266,6 +5976,251 @@ declare enum StewardTaskStatus {
5266
5976
  CANCELED = 'CANCELED',
5267
5977
  }
5268
5978
 
5979
+ declare class StreamStalledStatsTracker {
5980
+ private readonly callback: StatsAppendCallback;
5981
+ private readonly clock: StatsClock;
5982
+ private readonly setTimeoutFn: typeof setTimeout;
5983
+ private readonly clearTimeoutFn: typeof clearTimeout;
5984
+ private readonly streams = new Map<number, StreamState>();
5985
+ private readonly emittedMessageChannels = new Map<number, string | undefined>();
5986
+
5987
+ constructor(callback: StatsAppendCallback, options?: TimedTrackerOptions) {
5988
+ this.callback = callback;
5989
+ this.clock = getClock(options);
5990
+ this.setTimeoutFn = getSetTimeout(options);
5991
+ this.clearTimeoutFn = getClearTimeout(options);
5992
+ }
5993
+
5994
+ markRawProgress(params: {
5995
+ messageId: number;
5996
+ channelUrl?: string;
5997
+ textLength: number;
5998
+ aiResponse: AIResponseMessageMetadata;
5999
+ }): void {
6000
+ if (!params.aiResponse.isStreaming) {
6001
+ this.completeDelivery(params.messageId, params.textLength);
6002
+ return;
6003
+ }
6004
+
6005
+ if (this.emittedMessageChannels.has(params.messageId)) return;
6006
+
6007
+ const stream = this.getOrCreate(params.messageId, params.channelUrl);
6008
+
6009
+ if (params.textLength > stream.lastRawLength) {
6010
+ stream.chunksReceived += 1;
6011
+ stream.lastRawLength = params.textLength;
6012
+ stream.lastRawAt = this.clock.now();
6013
+
6014
+ clearTimer(stream.deliveryTimer, this.clearTimeoutFn);
6015
+ stream.deliveryTimer = null;
6016
+ this.scheduleDeliveryStallCheck(params.messageId, stream);
6017
+ } else {
6018
+ this.scheduleDeliveryStallCheck(params.messageId, stream);
6019
+ }
6020
+
6021
+ if (stream.lastRenderedLength < stream.lastRawLength && stream.renderTimer === null) {
6022
+ stream.lastRenderAt = this.clock.now();
6023
+ this.scheduleRenderStallCheck(params.messageId, stream, STREAM_RENDER_STALL_THRESHOLD_MS);
6024
+ }
6025
+ }
6026
+
6027
+ markRenderedProgress(params: { messageId: number; displayedLength: number }): void {
6028
+ const stream = this.streams.get(params.messageId);
6029
+ if (!stream || params.displayedLength <= stream.lastRenderedLength) return;
6030
+
6031
+ stream.lastRenderedLength = params.displayedLength;
6032
+ stream.lastRenderAt = this.clock.now();
6033
+
6034
+ if (stream.lastRenderedLength >= stream.lastRawLength) {
6035
+ if (stream.deliveryTimer === null) {
6036
+ this.clearStream(params.messageId, stream);
6037
+ return;
6038
+ }
6039
+
6040
+ clearTimer(stream.renderTimer, this.clearTimeoutFn);
6041
+ stream.renderTimer = null;
6042
+ return;
6043
+ }
6044
+
6045
+ this.scheduleRenderStallCheck(params.messageId, stream, STREAM_RENDER_STALL_THRESHOLD_MS);
6046
+ }
6047
+
6048
+ hasPendingStream(messageId: number): boolean {
6049
+ return this.streams.has(messageId);
6050
+ }
6051
+
6052
+ complete(messageId: number): void {
6053
+ const stream = this.streams.get(messageId);
6054
+ if (stream) this.clearStream(messageId, stream);
6055
+ }
6056
+
6057
+ // Final stream update: no more raw chunks are coming, so stop delivery-stall
6058
+ // tracking. The animation can still be catching up after streaming ends
6059
+ // (useStreamingText keeps animating while displayedLength < text.length), so
6060
+ // keep the render-stall watch alive until rendered progress reaches the last
6061
+ // raw length — otherwise a renderer that freezes mid-catch-up never emits a
6062
+ // 'render' stall.
6063
+ private completeDelivery(messageId: number, finalRawLength: number): void {
6064
+ const stream = this.streams.get(messageId);
6065
+ if (!stream) return;
6066
+
6067
+ // The final stream:false payload can carry text beyond the last streamed
6068
+ // chunk; fold it into the raw length first so the render watch targets the
6069
+ // full text the animation still has to reveal (otherwise the stream could be
6070
+ // cleared while a freeze on that trailing text would go unreported).
6071
+ if (finalRawLength > stream.lastRawLength) {
6072
+ stream.lastRawLength = finalRawLength;
6073
+ stream.lastRawAt = this.clock.now();
6074
+ }
6075
+
6076
+ clearTimer(stream.deliveryTimer, this.clearTimeoutFn);
6077
+ stream.deliveryTimer = null;
6078
+
6079
+ if (stream.lastRenderedLength >= stream.lastRawLength) {
6080
+ this.clearStream(messageId, stream);
6081
+ return;
6082
+ }
6083
+
6084
+ if (stream.renderTimer === null) {
6085
+ stream.lastRenderAt = this.clock.now();
6086
+ this.scheduleRenderStallCheck(messageId, stream, STREAM_RENDER_STALL_THRESHOLD_MS);
6087
+ }
6088
+ }
6089
+
6090
+ clearMessage(messageId: number): void {
6091
+ const stream = this.streams.get(messageId);
6092
+ if (stream) this.clearStream(messageId, stream);
6093
+ this.emittedMessageChannels.delete(messageId);
6094
+ }
6095
+
6096
+ clear(): void {
6097
+ this.streams.forEach((stream) => {
6098
+ clearTimer(stream.deliveryTimer, this.clearTimeoutFn);
6099
+ clearTimer(stream.renderTimer, this.clearTimeoutFn);
6100
+ });
6101
+ this.streams.clear();
6102
+ this.emittedMessageChannels.clear();
6103
+ }
6104
+
6105
+ clearChannel(channelUrl?: string): void {
6106
+ if (!channelUrl) return;
6107
+ this.streams.forEach((stream, messageId) => {
6108
+ if (stream.channelUrl !== channelUrl && stream.channelUrl !== undefined) return;
6109
+ this.clearStream(messageId, stream);
6110
+ });
6111
+ this.emittedMessageChannels.forEach((emittedChannelUrl, messageId) => {
6112
+ if (emittedChannelUrl === channelUrl || emittedChannelUrl === undefined)
6113
+ this.emittedMessageChannels.delete(messageId);
6114
+ });
6115
+ }
6116
+
6117
+ private detectDelivery(messageId: number): void {
6118
+ const stream = this.streams.get(messageId);
6119
+ if (!stream) return;
6120
+ stream.deliveryTimer = null;
6121
+ // Mutex: cancel any in-flight render stall check so delivery wins this turn.
6122
+ clearTimer(stream.renderTimer, this.clearTimeoutFn);
6123
+ stream.renderTimer = null;
6124
+ this.emit(messageId, stream, 'delivery');
6125
+ }
6126
+
6127
+ private detectRender(messageId: number): void {
6128
+ const stream = this.streams.get(messageId);
6129
+ if (!stream) return;
6130
+ stream.renderTimer = null;
6131
+ if (stream.lastRenderedLength >= stream.lastRawLength) return;
6132
+
6133
+ const elapsedSinceRender = this.clock.now() - stream.lastRenderAt;
6134
+ if (elapsedSinceRender < STREAM_RENDER_STALL_THRESHOLD_MS) {
6135
+ this.scheduleRenderStallCheck(messageId, stream, STREAM_RENDER_STALL_THRESHOLD_MS - elapsedSinceRender);
6136
+ return;
6137
+ }
6138
+
6139
+ // Mutex rule: when raw progress has been silent for at least the render threshold,
6140
+ // delivery is the root cause and will win. Defer the render emit until delivery has
6141
+ // had a chance to fire so we don't surface render as the symptom.
6142
+ const elapsedSinceRaw = this.clock.now() - stream.lastRawAt;
6143
+ if (elapsedSinceRaw >= STREAM_RENDER_STALL_THRESHOLD_MS && stream.deliveryTimer !== null) {
6144
+ const untilDelivery = STREAM_DELIVERY_STALL_THRESHOLD_MS - elapsedSinceRaw;
6145
+ if (untilDelivery > 0) {
6146
+ this.scheduleRenderStallCheck(messageId, stream, untilDelivery);
6147
+ return;
6148
+ }
6149
+ }
6150
+
6151
+ this.emit(messageId, stream, 'render');
6152
+ }
6153
+
6154
+ private scheduleRenderStallCheck(messageId: number, stream: StreamState, delayMs: number): void {
6155
+ if (stream.renderTimer !== null) return;
6156
+ stream.renderTimer = this.setTimeoutFn(() => this.detectRender(messageId), delayMs);
6157
+ }
6158
+
6159
+ private scheduleDeliveryStallCheck(messageId: number, stream: StreamState): void {
6160
+ if (stream.deliveryTimer !== null) return;
6161
+ stream.deliveryTimer = this.setTimeoutFn(() => this.detectDelivery(messageId), STREAM_DELIVERY_STALL_THRESHOLD_MS);
6162
+ }
6163
+
6164
+ private emit(messageId: number, stream: StreamState, stallKind: StreamStallKind): void {
6165
+ if (this.emittedMessageChannels.has(messageId)) return;
6166
+
6167
+ const stallStartedAt = stallKind === 'delivery' ? stream.lastRawAt : stream.lastRenderAt;
6168
+ append(this.callback, METRIC_KEY_STREAM_STALLED, this.clock.now() - stallStartedAt, {
6169
+ extra: {
6170
+ message_id: String(messageId),
6171
+ stall_kind: stallKind,
6172
+ },
6173
+ });
6174
+ this.rememberEmitted(messageId, stream.channelUrl);
6175
+ this.clearStream(messageId, stream);
6176
+ }
6177
+
6178
+ private getOrCreate(messageId: number, channelUrl: string | undefined): StreamState {
6179
+ const existing = this.streams.get(messageId);
6180
+ if (existing) return existing;
6181
+
6182
+ const now = this.clock.now();
6183
+ const stream: StreamState = {
6184
+ channelUrl,
6185
+ chunksReceived: 0,
6186
+ lastRawAt: now,
6187
+ lastRenderAt: now,
6188
+ lastRawLength: 0,
6189
+ lastRenderedLength: 0,
6190
+ deliveryTimer: null,
6191
+ renderTimer: null,
6192
+ };
6193
+ this.streams.set(messageId, stream);
6194
+ return stream;
6195
+ }
6196
+
6197
+ private rememberEmitted(messageId: number, channelUrl: string | undefined): void {
6198
+ if (!this.emittedMessageChannels.has(messageId)) {
6199
+ evictOldestIfAtLimit(this.emittedMessageChannels, STREAM_STALLED_EMITTED_LIMIT);
6200
+ }
6201
+ this.emittedMessageChannels.set(messageId, channelUrl);
6202
+ }
6203
+
6204
+ private clearStream(messageId: number, stream: StreamState): void {
6205
+ clearTimer(stream.deliveryTimer, this.clearTimeoutFn);
6206
+ clearTimer(stream.renderTimer, this.clearTimeoutFn);
6207
+ this.streams.delete(messageId);
6208
+ }
6209
+ }
6210
+
6211
+ declare type StreamStallKind = 'delivery' | 'render';
6212
+
6213
+ declare type StreamState = {
6214
+ channelUrl?: string;
6215
+ chunksReceived: number;
6216
+ lastRawAt: number;
6217
+ lastRenderAt: number;
6218
+ lastRawLength: number;
6219
+ lastRenderedLength: number;
6220
+ deliveryTimer: TimeoutId | null;
6221
+ renderTimer: TimeoutId | null;
6222
+ };
6223
+
5269
6224
  /**
5270
6225
  * StringSet type for React components
5271
6226
  * Uses SNAKE_CASE keys for backward compatibility
@@ -5540,11 +6495,248 @@ declare type TextField = {
5540
6495
  };
5541
6496
  };
5542
6497
 
6498
+ declare type TimedTrackerOptions = TrackerOptions & {
6499
+ setTimeout?: typeof setTimeout;
6500
+ clearTimeout?: typeof clearTimeout;
6501
+ };
6502
+
6503
+ declare type TimeoutId = ReturnType<typeof setTimeout>;
6504
+
5543
6505
  declare interface TimerData {
5544
6506
  startTime: number | null;
5545
6507
  endTime: number | null;
5546
6508
  }
5547
6509
 
6510
+ declare type TrackerOptions = {
6511
+ clock?: StatsClock;
6512
+ };
6513
+
6514
+ declare class TypingIndicatorAbsentTimeoutStatsTracker {
6515
+ private readonly callback: StatsAppendCallback;
6516
+ private readonly clock: StatsClock;
6517
+ private readonly setTimeoutFn: typeof setTimeout;
6518
+ private readonly clearTimeoutFn: typeof clearTimeout;
6519
+ private readonly turns = new Map<string, TypingTurn>();
6520
+ private readonly pendingAIResponseStartsByUserMessageId = new Map<number, PendingAIResponseStart>();
6521
+
6522
+ constructor(callback: StatsAppendCallback, options?: TimedTrackerOptions) {
6523
+ this.callback = callback;
6524
+ this.clock = getClock(options);
6525
+ this.setTimeoutFn = getSetTimeout(options);
6526
+ this.clearTimeoutFn = getClearTimeout(options);
6527
+ }
6528
+
6529
+ startTurn(params: { requestId: string; channelUrl?: string }): void {
6530
+ this.cancelTurn(params.requestId);
6531
+ const turn: TypingTurn = {
6532
+ requestId: params.requestId,
6533
+ channelUrl: params.channelUrl,
6534
+ beforeTpstStartedAt: this.clock.now(),
6535
+ afterTpenStartedAt: null,
6536
+ timer: null,
6537
+ };
6538
+ turn.timer = this.setTimeoutFn(() => this.commitTimeout(turn.requestId), TYPING_COLLECTION_TIMEOUT_MS);
6539
+ this.turns.set(params.requestId, turn);
6540
+ }
6541
+
6542
+ markUserMessageAck(params: { requestId: string; messageId: number }): void {
6543
+ const turn = this.turns.get(params.requestId);
6544
+ if (!turn) return;
6545
+ turn.userMessageId = params.messageId;
6546
+
6547
+ const pending = this.pendingAIResponseStartsByUserMessageId.get(params.messageId);
6548
+ if (!pending) return;
6549
+ this.pendingAIResponseStartsByUserMessageId.delete(params.messageId);
6550
+ this.applyAIResponseStarted(
6551
+ { channelUrl: pending.channelUrl, matchesUserMessage: true, userMessageId: params.messageId },
6552
+ pending.startedAt,
6553
+ );
6554
+ }
6555
+
6556
+ replaceTurnRequestId(currentRequestId: string, nextRequestId: string): void {
6557
+ if (currentRequestId === nextRequestId) return;
6558
+
6559
+ const turn = this.turns.get(currentRequestId);
6560
+ if (!turn) return;
6561
+
6562
+ this.turns.delete(currentRequestId);
6563
+ turn.requestId = nextRequestId;
6564
+ this.turns.set(nextRequestId, turn);
6565
+ }
6566
+
6567
+ cancelTurn(requestId: string): void {
6568
+ this.cleanupTurn(requestId);
6569
+ }
6570
+
6571
+ markTypingStarted(channelUrl?: string): void {
6572
+ const now = this.clock.now();
6573
+ this.turns.forEach((turn) => {
6574
+ if (!matchesChannel(turn, channelUrl)) return;
6575
+ this.recordAfterTpen(turn, now);
6576
+ this.recordBeforeTpst(turn, now);
6577
+ });
6578
+ }
6579
+
6580
+ markTypingEnded(channelUrl?: string): void {
6581
+ const now = this.clock.now();
6582
+ this.turns.forEach((turn) => {
6583
+ if (!matchesChannel(turn, channelUrl)) return;
6584
+ this.recordBeforeTpst(turn, now);
6585
+ turn.afterTpenStartedAt = now;
6586
+ });
6587
+ }
6588
+
6589
+ markAIResponseStarted(params: { channelUrl?: string; userMessageId?: number } = {}): void {
6590
+ const channelUrl = params.channelUrl;
6591
+ const userMessageId = params.userMessageId;
6592
+ const matchesUserMessage = userMessageId !== undefined;
6593
+ const now = this.clock.now();
6594
+ const matched = this.applyAIResponseStarted({ channelUrl, matchesUserMessage, userMessageId }, now);
6595
+ if (matched || !matchesUserMessage || userMessageId === undefined || !this.hasUnackedTurn(channelUrl)) return;
6596
+
6597
+ if (!this.pendingAIResponseStartsByUserMessageId.has(userMessageId)) {
6598
+ evictOldestIfAtLimit(this.pendingAIResponseStartsByUserMessageId, 100);
6599
+ this.pendingAIResponseStartsByUserMessageId.set(userMessageId, { channelUrl, startedAt: now });
6600
+ }
6601
+ }
6602
+
6603
+ private applyAIResponseStarted(
6604
+ params: { channelUrl?: string; matchesUserMessage: boolean; userMessageId?: number },
6605
+ startedAt: number,
6606
+ ): boolean {
6607
+ if (!params.matchesUserMessage) {
6608
+ const match = this.findSingleTurn(params.channelUrl);
6609
+ if (!match) return false;
6610
+ this.completeTurn(match.requestId, match.turn, startedAt);
6611
+ return true;
6612
+ }
6613
+
6614
+ let matched = false;
6615
+ this.turns.forEach((turn, requestId) => {
6616
+ if (!matchesChannel(turn, params.channelUrl)) return;
6617
+ if (params.matchesUserMessage && turn.userMessageId !== params.userMessageId) return;
6618
+ matched = true;
6619
+ this.completeTurn(requestId, turn, startedAt);
6620
+ });
6621
+ return matched;
6622
+ }
6623
+
6624
+ private findSingleTurn(channelUrl?: string): TypingTurnEntry | undefined {
6625
+ let match: TypingTurnEntry | undefined;
6626
+ let matchedCount = 0;
6627
+ this.turns.forEach((turn, requestId) => {
6628
+ if (!matchesChannel(turn, channelUrl)) return;
6629
+ matchedCount += 1;
6630
+ if (matchedCount > 1) return;
6631
+ match = { requestId, turn };
6632
+ });
6633
+ return matchedCount === 1 ? match : undefined;
6634
+ }
6635
+
6636
+ private completeTurn(requestId: string, turn: TypingTurn, startedAt: number): void {
6637
+ this.recordBeforeTpst(turn, startedAt);
6638
+ this.recordAfterTpen(turn, startedAt);
6639
+ this.commitIfAbsent(requestId, turn);
6640
+ }
6641
+
6642
+ private hasUnackedTurn(channelUrl?: string): boolean {
6643
+ return hasUnackedChannelTurn(this.turns.values(), channelUrl);
6644
+ }
6645
+
6646
+ clear(): void {
6647
+ this.turns.forEach((turn) => {
6648
+ clearTimer(turn.timer, this.clearTimeoutFn);
6649
+ });
6650
+ this.turns.clear();
6651
+ this.pendingAIResponseStartsByUserMessageId.clear();
6652
+ }
6653
+
6654
+ clearChannel(channelUrl?: string): void {
6655
+ if (!channelUrl) return;
6656
+ this.turns.forEach((turn, requestId) => {
6657
+ if (turn.channelUrl !== channelUrl) return;
6658
+ this.cleanupTurn(requestId);
6659
+ });
6660
+ this.pendingAIResponseStartsByUserMessageId.forEach((pending, userMessageId) => {
6661
+ if (pending.channelUrl === channelUrl) this.pendingAIResponseStartsByUserMessageId.delete(userMessageId);
6662
+ });
6663
+ }
6664
+
6665
+ private commitTimeout(requestId: string): void {
6666
+ const turn = this.turns.get(requestId);
6667
+ if (!turn) return;
6668
+ const now = this.clock.now();
6669
+ this.recordBeforeTpst(turn, now);
6670
+ this.recordAfterTpen(turn, now);
6671
+ this.commitIfAbsent(requestId, turn);
6672
+ }
6673
+
6674
+ private recordBeforeTpst(
6675
+ turn: { beforeTpstStartedAt: number | null; beforeTpstAbsentMs?: number },
6676
+ endedAt: number,
6677
+ ): void {
6678
+ if (turn.beforeTpstStartedAt === null) return;
6679
+ const duration = endedAt - turn.beforeTpstStartedAt;
6680
+ if (duration >= TYPING_ABSENT_THRESHOLD_MS) turn.beforeTpstAbsentMs = duration;
6681
+ turn.beforeTpstStartedAt = null;
6682
+ }
6683
+
6684
+ private recordAfterTpen(
6685
+ turn: { afterTpenStartedAt: number | null; afterTpenAbsentMs?: number },
6686
+ endedAt: number,
6687
+ ): void {
6688
+ if (turn.afterTpenStartedAt === null) return;
6689
+ const duration = endedAt - turn.afterTpenStartedAt;
6690
+ if (duration >= TYPING_ABSENT_THRESHOLD_MS) turn.afterTpenAbsentMs = duration;
6691
+ turn.afterTpenStartedAt = null;
6692
+ }
6693
+
6694
+ private commitIfAbsent(requestId: string, turn: { beforeTpstAbsentMs?: number; afterTpenAbsentMs?: number }): void {
6695
+ const beforeTpstAbsentMs = turn.beforeTpstAbsentMs;
6696
+ const afterTpenAbsentMs = turn.afterTpenAbsentMs;
6697
+ if (beforeTpstAbsentMs === undefined && afterTpenAbsentMs === undefined) {
6698
+ this.cleanupTurn(requestId);
6699
+ return;
6700
+ }
6701
+
6702
+ append(
6703
+ this.callback,
6704
+ METRIC_KEY_AI_TYPING_INDICATOR_ABSENT_TIMEOUT,
6705
+ (beforeTpstAbsentMs ?? 0) + (afterTpenAbsentMs ?? 0),
6706
+ {
6707
+ extra: {
6708
+ ...(beforeTpstAbsentMs !== undefined && { before_tpst_absent_ms: beforeTpstAbsentMs }),
6709
+ ...(afterTpenAbsentMs !== undefined && { after_tpen_absent_ms: afterTpenAbsentMs }),
6710
+ },
6711
+ },
6712
+ );
6713
+ this.cleanupTurn(requestId);
6714
+ }
6715
+
6716
+ private cleanupTurn(requestId: string): void {
6717
+ const turn = this.turns.get(requestId);
6718
+ if (!turn) return;
6719
+ clearTimer(turn.timer, this.clearTimeoutFn);
6720
+ this.turns.delete(requestId);
6721
+ }
6722
+ }
6723
+
6724
+ declare type TypingTurn = {
6725
+ requestId: string;
6726
+ channelUrl?: string;
6727
+ userMessageId?: number;
6728
+ beforeTpstStartedAt: number | null;
6729
+ afterTpenStartedAt: number | null;
6730
+ beforeTpstAbsentMs?: number;
6731
+ afterTpenAbsentMs?: number;
6732
+ timer: TimeoutId | null;
6733
+ };
6734
+
6735
+ declare type TypingTurnEntry = {
6736
+ requestId: string;
6737
+ turn: TypingTurn;
6738
+ };
6739
+
5548
6740
  declare interface TypographyShape {
5549
6741
  h1: TypographyVariant;
5550
6742
  h2: TypographyVariant;
@@ -5620,6 +6812,189 @@ export declare type UserActionStatus =
5620
6812
 
5621
6813
  export declare const useRefreshActiveChannel: (updater: () => Promise<void>) => () => Promise<void>;
5622
6814
 
6815
+ declare class UserPerceivedResponseTimeStatsTracker {
6816
+ private readonly callback: StatsAppendCallback;
6817
+ private readonly clock: StatsClock;
6818
+ private readonly random: () => number;
6819
+ private readonly sampledByRequestId = new Map<string, UserTurn>();
6820
+ private readonly sampledByAIMessageId = new Map<number, UserTurn>();
6821
+ private readonly pendingAIResponseByUserMessageId = new Map<number, PendingAIResponseRaw>();
6822
+ private collectedCount = 0;
6823
+
6824
+ constructor(callback: StatsAppendCallback, options?: TrackerOptions & { random?: () => number }) {
6825
+ this.callback = callback;
6826
+ this.clock = getClock(options);
6827
+ this.random = options?.random ?? Math.random;
6828
+ }
6829
+
6830
+ startTurn(params: { requestId: string; channelUrl?: string }): void {
6831
+ if (this.collectedCount >= 10) return;
6832
+ if (this.random() >= 0.15) return;
6833
+ this.collectedCount += 1;
6834
+ this.sampledByRequestId.set(params.requestId, {
6835
+ requestId: params.requestId,
6836
+ channelUrl: params.channelUrl,
6837
+ startedAt: this.clock.now(),
6838
+ });
6839
+ }
6840
+
6841
+ markUserMessageAck(params: { requestId: string; messageId: number }): void {
6842
+ const turn = this.sampledByRequestId.get(params.requestId);
6843
+ if (!turn) return;
6844
+
6845
+ turn.ackAt = this.clock.now();
6846
+ turn.userMessageId = params.messageId;
6847
+
6848
+ const pending = this.pendingAIResponseByUserMessageId.get(params.messageId);
6849
+ if (!pending) return;
6850
+ this.pendingAIResponseByUserMessageId.delete(params.messageId);
6851
+ this.applyAIResponseRaw(turn, pending);
6852
+ }
6853
+
6854
+ replaceTurnRequestId(currentRequestId: string, nextRequestId: string): void {
6855
+ if (currentRequestId === nextRequestId) return;
6856
+
6857
+ const turn = this.sampledByRequestId.get(currentRequestId);
6858
+ if (!turn) return;
6859
+
6860
+ this.sampledByRequestId.delete(currentRequestId);
6861
+ turn.requestId = nextRequestId;
6862
+ this.sampledByRequestId.set(nextRequestId, turn);
6863
+ }
6864
+
6865
+ cancelTurn(requestId: string): void {
6866
+ const turn = this.sampledByRequestId.get(requestId);
6867
+ if (!turn) return;
6868
+ this.deleteTurn(turn);
6869
+ // Return the unused sample slot so cancelled turns don't burn the per-session cap.
6870
+ if (this.collectedCount > 0) this.collectedCount -= 1;
6871
+ }
6872
+
6873
+ markAIResponseRaw(params: { messageId: number; channelUrl?: string; aiResponse: AIResponseMessageMetadata }): void {
6874
+ const firstChunkAt = this.clock.now();
6875
+ const userMessageId = params.aiResponse.userMessageId;
6876
+ if (userMessageId === undefined && this.hasUnackedTurn(params.channelUrl)) return;
6877
+
6878
+ const turn = this.findPendingTurn(params.channelUrl, userMessageId);
6879
+ if (turn) {
6880
+ this.applyAIResponseRaw(turn, { ...params, firstChunkAt });
6881
+ return;
6882
+ }
6883
+
6884
+ if (userMessageId === undefined || !this.hasUnackedTurn(params.channelUrl)) return;
6885
+ this.bufferAIResponseRaw(
6886
+ {
6887
+ messageId: params.messageId,
6888
+ ...(params.channelUrl !== undefined && { channelUrl: params.channelUrl }),
6889
+ aiResponse: params.aiResponse,
6890
+ firstChunkAt,
6891
+ },
6892
+ userMessageId,
6893
+ );
6894
+ }
6895
+
6896
+ markAIResponseVisible(aiMessageId: number): boolean {
6897
+ const turn = this.sampledByAIMessageId.get(aiMessageId);
6898
+ if (!turn || turn.ackAt === undefined || turn.firstChunkAt === undefined || turn.userMessageId === undefined)
6899
+ return false;
6900
+
6901
+ const renderedAt = this.clock.now();
6902
+ append(this.callback, METRIC_KEY_USER_PERCEIVED_RESPONSE_TIME, renderedAt - turn.startedAt, {
6903
+ extra: {
6904
+ ack_to_first_chunk_ms: turn.firstChunkAt - turn.ackAt,
6905
+ first_chunk_to_render_ms: renderedAt - turn.firstChunkAt,
6906
+ is_streaming: Boolean(turn.isStreaming),
6907
+ send_to_ack_ms: turn.ackAt - turn.startedAt,
6908
+ },
6909
+ });
6910
+
6911
+ this.deleteTurn(turn);
6912
+ return true;
6913
+ }
6914
+
6915
+ hasPendingAIResponse(aiMessageId: number): boolean {
6916
+ return this.sampledByAIMessageId.has(aiMessageId);
6917
+ }
6918
+
6919
+ clearMessage(aiMessageId: number): void {
6920
+ this.pendingAIResponseByUserMessageId.forEach((pending, userMessageId) => {
6921
+ if (pending.messageId === aiMessageId) this.pendingAIResponseByUserMessageId.delete(userMessageId);
6922
+ });
6923
+
6924
+ const turn = this.sampledByAIMessageId.get(aiMessageId);
6925
+ if (!turn) return;
6926
+
6927
+ this.deleteTurn(turn);
6928
+ if (this.collectedCount > 0) this.collectedCount -= 1;
6929
+ }
6930
+
6931
+ clearChannel(channelUrl?: string): void {
6932
+ if (!channelUrl) return;
6933
+ this.sampledByRequestId.forEach((turn) => {
6934
+ if (turn.channelUrl !== channelUrl) return;
6935
+ this.deleteTurn(turn);
6936
+ if (this.collectedCount > 0) this.collectedCount -= 1;
6937
+ });
6938
+ this.pendingAIResponseByUserMessageId.forEach((pending, userMessageId) => {
6939
+ if (pending.channelUrl === channelUrl) this.pendingAIResponseByUserMessageId.delete(userMessageId);
6940
+ });
6941
+ }
6942
+
6943
+ clear(): void {
6944
+ this.sampledByRequestId.clear();
6945
+ this.sampledByAIMessageId.clear();
6946
+ this.pendingAIResponseByUserMessageId.clear();
6947
+ this.collectedCount = 0;
6948
+ }
6949
+
6950
+ private findPendingTurn(channelUrl?: string, userMessageId?: number): UserTurn | undefined {
6951
+ if (userMessageId !== undefined) {
6952
+ for (const turn of this.sampledByRequestId.values()) {
6953
+ if (turn.ackAt === undefined || turn.firstChunkAt !== undefined) continue;
6954
+ if (!matchesChannel(turn, channelUrl)) continue;
6955
+ if (turn.userMessageId === userMessageId) return turn;
6956
+ }
6957
+ return undefined;
6958
+ }
6959
+
6960
+ let match: UserTurn | undefined;
6961
+ for (const turn of this.sampledByRequestId.values()) {
6962
+ if (turn.ackAt === undefined || turn.firstChunkAt !== undefined) continue;
6963
+ if (!matchesChannel(turn, channelUrl)) continue;
6964
+ if (match) return undefined;
6965
+ match = turn;
6966
+ }
6967
+ return match;
6968
+ }
6969
+
6970
+ private applyAIResponseRaw(turn: UserTurn, params: PendingAIResponseRaw): void {
6971
+ if (turn.firstChunkAt !== undefined) return;
6972
+
6973
+ turn.firstChunkAt = turn.ackAt === undefined ? params.firstChunkAt : Math.max(params.firstChunkAt, turn.ackAt);
6974
+ turn.aiMessageId = params.messageId;
6975
+ turn.channelUrl = turn.channelUrl ?? params.channelUrl;
6976
+ turn.isStreaming = params.aiResponse.isStreaming;
6977
+ this.sampledByAIMessageId.set(params.messageId, turn);
6978
+ }
6979
+
6980
+ private bufferAIResponseRaw(params: PendingAIResponseRaw, userMessageId: number): void {
6981
+ if (!this.pendingAIResponseByUserMessageId.has(userMessageId)) {
6982
+ evictOldestIfAtLimit(this.pendingAIResponseByUserMessageId, 100);
6983
+ this.pendingAIResponseByUserMessageId.set(userMessageId, params);
6984
+ }
6985
+ }
6986
+
6987
+ private hasUnackedTurn(channelUrl?: string): boolean {
6988
+ return hasUnackedChannelTurn(this.sampledByRequestId.values(), channelUrl);
6989
+ }
6990
+
6991
+ private deleteTurn(turn: UserTurn): void {
6992
+ this.sampledByRequestId.delete(turn.requestId);
6993
+ if (turn.aiMessageId !== undefined) this.sampledByAIMessageId.delete(turn.aiMessageId);
6994
+ if (turn.userMessageId !== undefined) this.pendingAIResponseByUserMessageId.delete(turn.userMessageId);
6995
+ }
6996
+ }
6997
+
5623
6998
  /**
5624
6999
  * User session containing authentication credentials.
5625
7000
  */
@@ -5637,6 +7012,17 @@ declare interface UserSessionInfo {
5637
7012
  sessionHandler: AIAgentSessionHandler;
5638
7013
  }
5639
7014
 
7015
+ declare type UserTurn = {
7016
+ requestId: string;
7017
+ channelUrl?: string;
7018
+ startedAt: number;
7019
+ ackAt?: number;
7020
+ firstChunkAt?: number;
7021
+ userMessageId?: number;
7022
+ aiMessageId?: number;
7023
+ isStreaming?: boolean;
7024
+ };
7025
+
5640
7026
  export { }
5641
7027
 
5642
7028