@sendbird/ai-agent-messenger-react 1.32.0 → 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 +1474 -98
  3. package/dist/index.js +7511 -5655
  4. package/package.json +3 -3
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, }: ConversationProps) => 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 }),
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 }),
1671
1712
  };
1672
1713
 
1673
- const payload: AIAgentStatPayload = {
1674
- key: this.getMetricKey(),
1675
- // Use "0" when totalDuration is null (error case)
1676
- value: totalDuration !== null ? String(totalDuration) : '0',
1677
- };
1678
-
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, }: ConversationListProps) => 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: {
@@ -1985,6 +2263,7 @@ export declare type ConversationListProps = PropsWithChildren<{
1985
2263
  conversationListFilter?: Partial<AIAgentGroupChannelFilter>;
1986
2264
  onOpenConversationView?: (channelUrl: string, status: 'open' | 'closed', options?: ConversationListOpenOptions) => void;
1987
2265
  announcementsEnabled?: boolean;
2266
+ conversationListStatsEnabled?: boolean;
1988
2267
  /** Custom styles for the conversation list container. */
1989
2268
  style?: CSSProperties;
1990
2269
  }>;
@@ -2008,12 +2287,138 @@ declare type ConversationMessageEventPayload =
2008
2287
  messages: ChatSDKBaseMessage[];
2009
2288
  };
2010
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
+
2011
2415
  export declare type ConversationProps = PropsWithChildren<{
2012
2416
  channelUrl?: string;
2013
2417
  onClearChannelUrl?: () => void;
2014
2418
  onNavigateToConversationList?: () => void;
2015
2419
  shouldMarkAsRead?: boolean;
2016
2420
  announcementsEnabled?: boolean;
2421
+ conversationStatsEnabled?: boolean;
2017
2422
  initialFocusTarget?: ConversationInitialFocusTarget;
2018
2423
  /** Custom styles for the conversation container. */
2019
2424
  style?: CSSProperties;
@@ -2209,12 +2614,22 @@ declare interface Dispatcher {
2209
2614
  ): void;
2210
2615
  }
2211
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
+
2212
2625
  declare type ErrorAnnouncementParams =
2213
2626
  | { type: 'fileSizeExceeded'; maxSizeMB?: number }
2214
2627
  | { type: 'sendFailed' }
2215
2628
  | { type: 'csatFormError' }
2216
2629
  | { type: 'generic'; message: string };
2217
2630
 
2631
+ declare type ErrorLike = StatsPayloadError;
2632
+
2218
2633
  /**
2219
2634
  * For better understanding: https://sendbird.atlassian.net/wiki/spaces/AA/pages/3075014695/Extended+Message+Payload+Spec
2220
2635
  */
@@ -2348,6 +2763,8 @@ declare interface FixedMessengerStyleProps {
2348
2763
  launcherSize?: number;
2349
2764
  }
2350
2765
 
2766
+ declare type ForegroundTaskScheduler = (start: () => void) => void | (() => void);
2767
+
2351
2768
  declare interface Form {
2352
2769
  key: string;
2353
2770
  version: number;
@@ -2377,6 +2794,16 @@ export declare interface FunctionCallsInfo {
2377
2794
  };
2378
2795
  }
2379
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
+
2380
2807
  export declare interface GroundednessInfo {
2381
2808
  id: number;
2382
2809
  source_type:
@@ -2399,6 +2826,8 @@ export declare interface GroundednessInfo {
2399
2826
  */
2400
2827
  declare type HexColor = `#${string}`;
2401
2828
 
2829
+ declare type HistoryLoadResult = 'success' | 'verified_empty' | 'failed' | 'pending';
2830
+
2402
2831
  export declare type IconComponent = AIAgentIconComponent;
2403
2832
 
2404
2833
  export declare type IconComponentProps = AIAgentIconComponentProps;
@@ -2480,13 +2909,8 @@ export declare const IncomingMessageLayout: {
2480
2909
  };
2481
2910
  }) => ReactNode;
2482
2911
  SuggestedReplies: ({ extendedMessagePayload, onClickSuggestedReply, suggestedRepliesDirection, }: IncomingMessageProps) => ReactNode;
2483
- MessageTemplate: (props: {
2484
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
2485
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
2486
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
2487
- messageTemplateErrorFallback?: ReactNode;
2488
- messageTemplateLoadingFallback?: ReactNode;
2489
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
2912
+ MessageTemplate: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
2913
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
2490
2914
  }) => ReactNode;
2491
2915
  CustomMessageTemplate: (_: ({
2492
2916
  messageType: "user";
@@ -2950,7 +3374,9 @@ export declare const IncomingMessageLayout: {
2950
3374
  })) => ReactNode;
2951
3375
  CTAButton: ({ extendedMessagePayload, onClickCTA, }: IncomingMessageProps) => ReactNode;
2952
3376
  Citation: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
2953
- Form: (props: IncomingMessageProps) => ReactNode;
3377
+ Form: (props: IncomingMessageProps & {
3378
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
3379
+ }) => ReactNode;
2954
3380
  Feedback: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
2955
3381
  MessageLogs: (_: IncomingMessageProps) => ReactNode;
2956
3382
  };
@@ -2975,13 +3401,8 @@ export declare const IncomingMessageLayout: {
2975
3401
  };
2976
3402
  }) => ReactNode;
2977
3403
  SuggestedReplies: ({ extendedMessagePayload, onClickSuggestedReply, suggestedRepliesDirection, }: IncomingMessageProps) => ReactNode;
2978
- MessageTemplate: (props: {
2979
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
2980
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
2981
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
2982
- messageTemplateErrorFallback?: ReactNode;
2983
- messageTemplateLoadingFallback?: ReactNode;
2984
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
3404
+ MessageTemplate: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
3405
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
2985
3406
  }) => ReactNode;
2986
3407
  CustomMessageTemplate: (_: ({
2987
3408
  messageType: "user";
@@ -3445,7 +3866,9 @@ export declare const IncomingMessageLayout: {
3445
3866
  })) => ReactNode;
3446
3867
  CTAButton: ({ extendedMessagePayload, onClickCTA, }: IncomingMessageProps) => ReactNode;
3447
3868
  Citation: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
3448
- Form: (props: IncomingMessageProps) => ReactNode;
3869
+ Form: (props: IncomingMessageProps & {
3870
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
3871
+ }) => ReactNode;
3449
3872
  Feedback: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
3450
3873
  MessageLogs: (_: IncomingMessageProps) => ReactNode;
3451
3874
  }>>;
@@ -3465,13 +3888,8 @@ export declare const IncomingMessageLayout: {
3465
3888
  };
3466
3889
  }) => ReactNode;
3467
3890
  SuggestedReplies: ({ extendedMessagePayload, onClickSuggestedReply, suggestedRepliesDirection, }: IncomingMessageProps) => ReactNode;
3468
- MessageTemplate: (props: {
3469
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
3470
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
3471
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
3472
- messageTemplateErrorFallback?: ReactNode;
3473
- messageTemplateLoadingFallback?: ReactNode;
3474
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
3891
+ MessageTemplate: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
3892
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
3475
3893
  }) => ReactNode;
3476
3894
  CustomMessageTemplate: (_: ({
3477
3895
  messageType: "user";
@@ -3935,19 +4353,16 @@ export declare const IncomingMessageLayout: {
3935
4353
  })) => ReactNode;
3936
4354
  CTAButton: ({ extendedMessagePayload, onClickCTA, }: IncomingMessageProps) => ReactNode;
3937
4355
  Citation: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
3938
- Form: (props: IncomingMessageProps) => ReactNode;
4356
+ Form: (props: IncomingMessageProps & {
4357
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
4358
+ }) => ReactNode;
3939
4359
  Feedback: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
3940
4360
  MessageLogs: (_: IncomingMessageProps) => ReactNode;
3941
4361
  }>;
3942
4362
  } & {
3943
4363
  MessageTemplate: (props: {
3944
- component: (props: {
3945
- onGetCachedMessageTemplate?: ((templateKey: string) => string | null) | undefined;
3946
- onRequestMessageTemplate?: ((templateKey: string) => Promise<string>) | undefined;
3947
- onHandleTemplateInternalAction?: ((action: Action) => void) | undefined;
3948
- messageTemplateErrorFallback?: ReactNode;
3949
- messageTemplateLoadingFallback?: ReactNode;
3950
- extendedMessagePayload?: Partial<ExtendedMessagePayload> | undefined;
4364
+ component: (props: Pick<IncomingMessageProps, "onGetCachedMessageTemplate" | "onRequestMessageTemplate" | "onHandleTemplateInternalAction" | "messageTemplateErrorFallback" | "messageTemplateLoadingFallback" | "extendedMessagePayload"> & {
4365
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onMessageTemplateError">;
3951
4366
  }) => ReactNode;
3952
4367
  }) => null;
3953
4368
  SenderName: (props: {
@@ -4456,7 +4871,9 @@ export declare const IncomingMessageLayout: {
4456
4871
  component: ({ extendedMessagePayload, onClickCitation, }: IncomingMessageProps) => ReactNode;
4457
4872
  }) => null;
4458
4873
  Form: (props: {
4459
- component: (props: IncomingMessageProps) => ReactNode;
4874
+ component: (props: IncomingMessageProps & {
4875
+ statsCallbacks?: Pick<IncomingMessageStatsCallbacks, "onFormRenderError">;
4876
+ }) => ReactNode;
4460
4877
  }) => null;
4461
4878
  Feedback: (props: {
4462
4879
  component: ({ isBotMessage, isConversationClosed, isStreaming, isFeedbackEnabled, isFeedbackCommentEnabled, extendedMessagePayload, onFeedbackUpdate, }: IncomingMessageProps) => ReactNode;
@@ -4469,6 +4886,13 @@ export declare const IncomingMessageLayout: {
4469
4886
  export declare type IncomingMessageProps<T extends IncomingMessageUnion['messageType'] = IncomingMessageUnion['messageType']> =
4470
4887
  PickMessageProps<IncomingMessageUnion, T>;
4471
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
+
4472
4896
  declare type IncomingMessageTemplateProps = IncomingMessageProps & Partial<InternalExtraProps>;
4473
4897
 
4474
4898
  declare type IncomingMessageUnion =
@@ -4508,7 +4932,7 @@ declare type InputState =
4508
4932
 
4509
4933
  declare type InternalExtraProps = {
4510
4934
  maxBodyWidth?: number;
4511
- testerMode?: boolean;
4935
+ statsCallbacks?: IncomingMessageStatsCallbacks;
4512
4936
  };
4513
4937
 
4514
4938
  declare type InternalExtraProps_2 = {
@@ -4651,6 +5075,95 @@ export declare const MessageListUILayout: {
4651
5075
 
4652
5076
  export declare const MessageLogs: ({ actionbook, functionCalls, groundedness, agentMessageTemplates, flaggedTypes, onClickActionbook, onClickFunctionCall, onClickFunctionCallDetail, onClickGroundedness, onClickAgentMessageTemplate, topContent, bottomContent, renderCustomGroundednessIcon, style, }: Props_2) => JSX.Element;
4653
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
+
4654
5167
  declare interface MessageTemplateCache {
4655
5168
  set(key: string, value: string): void;
4656
5169
  get(key: string): string | null;
@@ -4915,6 +5428,7 @@ export declare const OutgoingMessageLayout: {
4915
5428
  MediaMessageBody: typeof OutgoingImageBody;
4916
5429
  FileMessageBody: typeof OutgoingFileBody;
4917
5430
  MultipleFilesMessageBody: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
5431
+ MessageLogs: (_: OutgoingMessageProps) => ReactNode;
4918
5432
  };
4919
5433
  };
4920
5434
  Template: ({ template, children }: {
@@ -4929,6 +5443,7 @@ export declare const OutgoingMessageLayout: {
4929
5443
  MediaMessageBody: typeof OutgoingImageBody;
4930
5444
  FileMessageBody: typeof OutgoingFileBody;
4931
5445
  MultipleFilesMessageBody: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
5446
+ MessageLogs: (_: OutgoingMessageProps) => ReactNode;
4932
5447
  }>>;
4933
5448
  useContext: () => LayoutContextValue<OutgoingMessageProps, {
4934
5449
  SendingStatus: ({ sendingStatus }: OutgoingMessageProps) => ReactNode;
@@ -4938,6 +5453,7 @@ export declare const OutgoingMessageLayout: {
4938
5453
  MediaMessageBody: typeof OutgoingImageBody;
4939
5454
  FileMessageBody: typeof OutgoingFileBody;
4940
5455
  MultipleFilesMessageBody: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
5456
+ MessageLogs: (_: OutgoingMessageProps) => ReactNode;
4941
5457
  }>;
4942
5458
  } & {
4943
5459
  SentTime: (props: {
@@ -4958,6 +5474,9 @@ export declare const OutgoingMessageLayout: {
4958
5474
  MultipleFilesMessageBody: (props: {
4959
5475
  component: ({ sendingStatus, files, metadata, onClickMedia, onClickMediaFiles, children, }: OutgoingMessageBodyProps<"multipleFiles">) => JSX.Element;
4960
5476
  }) => null;
5477
+ MessageLogs: (props: {
5478
+ component: (_: OutgoingMessageProps) => ReactNode;
5479
+ }) => null;
4961
5480
  SendingStatus: (props: {
4962
5481
  component: ({ sendingStatus }: OutgoingMessageProps) => ReactNode;
4963
5482
  }) => null;
@@ -5035,6 +5554,31 @@ declare interface PaletteTextEmphasis {
5035
5554
  textDisabled: string;
5036
5555
  }
5037
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
+
5038
5582
  declare type PickMessageProps<
5039
5583
  Union,
5040
5584
  T extends Union extends { messageType: infer MT } ? MT : never = Union extends { messageType: infer MT } ? MT : never,
@@ -5115,7 +5659,7 @@ export declare type PositionHorizontal = 'start' | 'end';
5115
5659
 
5116
5660
  export declare type PositionVertical = 'top' | 'bottom';
5117
5661
 
5118
- declare type PresentMethod = 'launcher_toggle' | 'direct_present';
5662
+ declare type PresentMethod = 'launcher_toggle' | 'direct_present' | 'embedded';
5119
5663
 
5120
5664
  declare type Props = {
5121
5665
  children: ReactNode;
@@ -5172,6 +5716,120 @@ declare type Props_2 = {
5172
5716
 
5173
5717
  declare type ReactOnlyIconName = 'expand' | 'collapse' | 'chevron-right' | 'attach' | 'close-filled' | 'actionbook' | 'function' | 'confluence' | 'zendesk' | 'salesforce' | 'sprinklr' | 'website' | 'snippet' | 'template' | 'show' | 'mute' | 'activity';
5174
5718
 
5719
+ declare type RecoveryMeasurement = {
5720
+ timer: TimeoutId | null;
5721
+ startedAt: number | null;
5722
+ triggerType: RecoveryTriggerType | null;
5723
+ inputEnabled: boolean;
5724
+ };
5725
+
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';
5832
+
5175
5833
  declare type ReleaseDisabledByValue =
5176
5834
  | 'unavailable'
5177
5835
  | 'form_active'
@@ -5182,6 +5840,39 @@ declare type ReleaseDisabledByValue =
5182
5840
  | 'reconnecting'
5183
5841
  | 'handoff_pending';
5184
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
+
5185
5876
  declare interface ResolvedHandlers extends AgentClientHandlers {
5186
5877
  onClickLink: (params: {
5187
5878
  url: string;
@@ -5223,6 +5914,15 @@ declare type SingleSelectField = {
5223
5914
 
5224
5915
  declare type StatsAppendCallback = (type: string, data: AIAgentStatPayload) => boolean;
5225
5916
 
5917
+ declare interface StatsClock {
5918
+ now: () => number;
5919
+ }
5920
+
5921
+ declare type StatsPayloadError = {
5922
+ code?: number;
5923
+ message?: string;
5924
+ };
5925
+
5226
5926
  /** Per-event override for the platform `announceStatus` wrappers — same shape as `AnnounceOptions`. */
5227
5927
  declare type StatusAnnouncementOptions = AnnounceOptions;
5228
5928
 
@@ -5276,6 +5976,251 @@ declare enum StewardTaskStatus {
5276
5976
  CANCELED = 'CANCELED',
5277
5977
  }
5278
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
+
5279
6224
  /**
5280
6225
  * StringSet type for React components
5281
6226
  * Uses SNAKE_CASE keys for backward compatibility
@@ -5550,11 +6495,248 @@ declare type TextField = {
5550
6495
  };
5551
6496
  };
5552
6497
 
6498
+ declare type TimedTrackerOptions = TrackerOptions & {
6499
+ setTimeout?: typeof setTimeout;
6500
+ clearTimeout?: typeof clearTimeout;
6501
+ };
6502
+
6503
+ declare type TimeoutId = ReturnType<typeof setTimeout>;
6504
+
5553
6505
  declare interface TimerData {
5554
6506
  startTime: number | null;
5555
6507
  endTime: number | null;
5556
6508
  }
5557
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
+
5558
6740
  declare interface TypographyShape {
5559
6741
  h1: TypographyVariant;
5560
6742
  h2: TypographyVariant;
@@ -5630,6 +6812,189 @@ export declare type UserActionStatus =
5630
6812
 
5631
6813
  export declare const useRefreshActiveChannel: (updater: () => Promise<void>) => () => Promise<void>;
5632
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
+
5633
6998
  /**
5634
6999
  * User session containing authentication credentials.
5635
7000
  */
@@ -5647,6 +7012,17 @@ declare interface UserSessionInfo {
5647
7012
  sessionHandler: AIAgentSessionHandler;
5648
7013
  }
5649
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
+
5650
7026
  export { }
5651
7027
 
5652
7028