@agent-native/core 0.98.4 → 0.98.6

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 (314) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +37 -0
  4. package/corpus/core/docs/content/external-agents.mdx +62 -7
  5. package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +38 -7
  6. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +2 -0
  7. package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +39 -7
  8. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +2 -0
  9. package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +39 -7
  10. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +2 -0
  11. package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +39 -7
  12. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +4 -2
  13. package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +38 -7
  14. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +2 -0
  15. package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +38 -7
  16. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +2 -0
  17. package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +38 -7
  18. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +2 -0
  19. package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +39 -7
  20. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +2 -0
  21. package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +37 -7
  22. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +2 -0
  23. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  24. package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +37 -7
  25. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +2 -0
  26. package/corpus/core/docs/content/tracking.mdx +2 -0
  27. package/corpus/core/package.json +1 -1
  28. package/corpus/core/src/a2a/handlers.ts +95 -21
  29. package/corpus/core/src/a2a/task-store.ts +65 -3
  30. package/corpus/core/src/agent/durable-background.ts +16 -16
  31. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +25 -8
  32. package/corpus/core/src/agent/engine/anthropic-engine.ts +17 -3
  33. package/corpus/core/src/agent/engine/output-tokens.ts +6 -6
  34. package/corpus/core/src/agent/production-agent.ts +200 -27
  35. package/corpus/core/src/agent/run-manager.ts +81 -1
  36. package/corpus/core/src/agent/run-store.ts +301 -41
  37. package/corpus/core/src/client/AgentPanel.tsx +6 -2
  38. package/corpus/core/src/client/AssistantChat.tsx +364 -69
  39. package/corpus/core/src/client/agent-chat-adapter.ts +169 -1
  40. package/corpus/core/src/client/chat/message-components.tsx +3 -0
  41. package/corpus/core/src/client/chat/tool-call-display.tsx +20 -36
  42. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +7 -0
  43. package/corpus/core/src/client/extensions/AgentNativeExtensionFrame.tsx +2 -0
  44. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +2 -0
  45. package/corpus/core/src/client/extensions/ExtensionEditor.tsx +9 -2
  46. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +2 -0
  47. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +2 -0
  48. package/corpus/core/src/client/extensions/portable-extension.ts +2 -0
  49. package/corpus/core/src/client/session-replay.ts +361 -29
  50. package/corpus/core/src/client/sse-event-processor.ts +43 -1
  51. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +1 -1
  52. package/corpus/core/src/demo/config.ts +6 -6
  53. package/corpus/core/src/demo/fetch-interceptor.ts +13 -4
  54. package/corpus/core/src/demo/redact.ts +13 -149
  55. package/corpus/core/src/extensions/html-shell.ts +3 -0
  56. package/corpus/core/src/extensions/session-replay-iframe.ts +131 -0
  57. package/corpus/core/src/integrations/adapters/slack.ts +123 -13
  58. package/corpus/core/src/integrations/identity-links-store.ts +210 -0
  59. package/corpus/core/src/integrations/identity.ts +132 -0
  60. package/corpus/core/src/integrations/index.ts +6 -0
  61. package/corpus/core/src/integrations/plugin.ts +58 -1
  62. package/corpus/core/src/integrations/types.ts +7 -0
  63. package/corpus/core/src/mcp/build-server.ts +127 -22
  64. package/corpus/core/src/mcp/external-agent-policy.ts +18 -0
  65. package/corpus/core/src/mcp/index.ts +1 -0
  66. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -20
  67. package/corpus/core/src/server/agent-chat/plugin-options.ts +12 -1
  68. package/corpus/core/src/server/agent-chat/script-entries.ts +20 -3
  69. package/corpus/core/src/server/agent-chat-plugin.ts +3 -0
  70. package/corpus/core/src/session-replay-iframe-protocol.ts +58 -0
  71. package/corpus/core/src/shared/reasoning-effort.ts +35 -0
  72. package/corpus/core/src/styles/agent-conversation.css +26 -0
  73. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  74. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  75. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +30 -21
  76. package/corpus/templates/analytics/AGENTS.md +27 -0
  77. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +0 -1
  78. package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
  79. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
  80. package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +40 -0
  81. package/corpus/templates/analytics/actions/list-error-issues.ts +13 -0
  82. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
  83. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -0
  84. package/corpus/templates/analytics/app/global.css +2 -2
  85. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -4
  86. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +52 -203
  87. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +69 -16
  88. package/corpus/templates/analytics/changelog/2026-07-12-analytics-chat-retries-before-showing-a-generic-no-data-mess.md +6 -0
  89. package/corpus/templates/analytics/changelog/2026-07-12-analytics-sessions-search-no-longer-loses-fast-keystrokes-an.md +6 -0
  90. package/corpus/templates/analytics/changelog/2026-07-12-chart-selection-respects-chat-state.md +6 -0
  91. package/corpus/templates/analytics/changelog/2026-07-12-connected-external-agents-can-now-look-up-sessions-error-iss.md +6 -0
  92. package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-anonymizes-error-reporting-emails.md +6 -0
  93. package/corpus/templates/analytics/changelog/2026-07-12-session-identities-stay-visible-when-demo-mode-is-off.md +6 -0
  94. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-show-iframe-content.md +6 -0
  95. package/corpus/templates/analytics/netlify.toml +0 -3
  96. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +14 -0
  97. package/corpus/templates/analytics/server/lib/error-capture.ts +101 -2
  98. package/corpus/templates/analytics/server/lib/real-data-actions.ts +24 -0
  99. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +49 -17
  100. package/corpus/templates/analytics/server/lib/session-replay.ts +18 -16
  101. package/corpus/templates/analytics/server/plugins/agent-chat.ts +43 -6
  102. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  103. package/corpus/templates/assets/app/global.css +1 -1
  104. package/corpus/templates/brain/app/global.css +2 -2
  105. package/corpus/templates/brain/changelog/2026-07-12-fix-long-response-progress.md +6 -0
  106. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +1 -1
  107. package/corpus/templates/forms/app/components/ThemeToggle.tsx +22 -6
  108. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +5 -5
  109. package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +3 -3
  110. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +16 -16
  111. package/corpus/templates/forms/app/global.css +164 -13
  112. package/corpus/templates/forms/app/pages/AskPage.tsx +35 -13
  113. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +143 -67
  114. package/corpus/templates/forms/app/pages/FormFillPage.tsx +44 -11
  115. package/corpus/templates/forms/app/pages/FormsListPage.tsx +42 -24
  116. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +28 -15
  117. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +46 -20
  118. package/corpus/templates/forms/app/routes/_app.settings.tsx +1 -1
  119. package/corpus/templates/forms/changelog/2026-07-12-ask-forms-suggestions-now-sit-beneath-the-composer-for-a-tig.md +6 -0
  120. package/corpus/templates/forms/changelog/2026-07-12-builder-response-tables-now-fill-the-view-without-a-redundan.md +6 -0
  121. package/corpus/templates/forms/changelog/2026-07-12-form-builder-surfaces-are-calmer-fields-stay-stable-on-hover.md +6 -0
  122. package/corpus/templates/forms/changelog/2026-07-12-response-tables-now-fill-the-available-pane-with-a-flexible-.md +6 -0
  123. package/corpus/templates/forms/changelog/2026-07-12-smoother-more-tactile-controls-throughout-animated-icon-and-.md +6 -0
  124. package/corpus/templates/mail/AGENTS.md +3 -1
  125. package/corpus/templates/mail/app/components/email/EmailThread.tsx +21 -13
  126. package/corpus/templates/mail/app/components/email/email-iframe-document.ts +18 -0
  127. package/corpus/templates/mail/server/lib/mail-integrations.ts +3 -0
  128. package/dist/a2a/handlers.d.ts.map +1 -1
  129. package/dist/a2a/handlers.js +84 -15
  130. package/dist/a2a/handlers.js.map +1 -1
  131. package/dist/a2a/task-store.d.ts +20 -1
  132. package/dist/a2a/task-store.d.ts.map +1 -1
  133. package/dist/a2a/task-store.js +57 -4
  134. package/dist/a2a/task-store.js.map +1 -1
  135. package/dist/agent/durable-background.d.ts +10 -10
  136. package/dist/agent/durable-background.d.ts.map +1 -1
  137. package/dist/agent/durable-background.js +16 -16
  138. package/dist/agent/durable-background.js.map +1 -1
  139. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  140. package/dist/agent/engine/ai-sdk-engine.js +19 -8
  141. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  142. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  143. package/dist/agent/engine/anthropic-engine.js +11 -3
  144. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  145. package/dist/agent/engine/output-tokens.js +6 -6
  146. package/dist/agent/engine/output-tokens.js.map +1 -1
  147. package/dist/agent/production-agent.d.ts +52 -0
  148. package/dist/agent/production-agent.d.ts.map +1 -1
  149. package/dist/agent/production-agent.js +154 -26
  150. package/dist/agent/production-agent.js.map +1 -1
  151. package/dist/agent/run-manager.d.ts +9 -0
  152. package/dist/agent/run-manager.d.ts.map +1 -1
  153. package/dist/agent/run-manager.js +71 -1
  154. package/dist/agent/run-manager.js.map +1 -1
  155. package/dist/agent/run-store.d.ts +9 -0
  156. package/dist/agent/run-store.d.ts.map +1 -1
  157. package/dist/agent/run-store.js +243 -48
  158. package/dist/agent/run-store.js.map +1 -1
  159. package/dist/client/AgentPanel.d.ts.map +1 -1
  160. package/dist/client/AgentPanel.js +6 -2
  161. package/dist/client/AgentPanel.js.map +1 -1
  162. package/dist/client/AssistantChat.d.ts +21 -0
  163. package/dist/client/AssistantChat.d.ts.map +1 -1
  164. package/dist/client/AssistantChat.js +298 -67
  165. package/dist/client/AssistantChat.js.map +1 -1
  166. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  167. package/dist/client/agent-chat-adapter.js +146 -1
  168. package/dist/client/agent-chat-adapter.js.map +1 -1
  169. package/dist/client/chat/message-components.d.ts.map +1 -1
  170. package/dist/client/chat/message-components.js +2 -1
  171. package/dist/client/chat/message-components.js.map +1 -1
  172. package/dist/client/chat/tool-call-display.d.ts +3 -1
  173. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  174. package/dist/client/chat/tool-call-display.js +13 -37
  175. package/dist/client/chat/tool-call-display.js.map +1 -1
  176. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +1 -0
  177. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  178. package/dist/client/conversation/use-near-bottom-autoscroll.js +6 -0
  179. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  180. package/dist/client/extensions/AgentNativeExtensionFrame.d.ts.map +1 -1
  181. package/dist/client/extensions/AgentNativeExtensionFrame.js +2 -1
  182. package/dist/client/extensions/AgentNativeExtensionFrame.js.map +1 -1
  183. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  184. package/dist/client/extensions/EmbeddedExtension.js +2 -1
  185. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  186. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  187. package/dist/client/extensions/ExtensionEditor.js +5 -2
  188. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  189. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  190. package/dist/client/extensions/ExtensionViewer.js +2 -1
  191. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  192. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -1
  193. package/dist/client/extensions/InlineExtensionFrame.js +2 -1
  194. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -1
  195. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  196. package/dist/client/extensions/portable-extension.js +2 -0
  197. package/dist/client/extensions/portable-extension.js.map +1 -1
  198. package/dist/client/session-replay.d.ts.map +1 -1
  199. package/dist/client/session-replay.js +308 -29
  200. package/dist/client/session-replay.js.map +1 -1
  201. package/dist/client/sse-event-processor.d.ts +5 -0
  202. package/dist/client/sse-event-processor.d.ts.map +1 -1
  203. package/dist/client/sse-event-processor.js +37 -1
  204. package/dist/client/sse-event-processor.js.map +1 -1
  205. package/dist/collab/routes.d.ts +1 -1
  206. package/dist/demo/actions/toggle-demo-mode.js +1 -1
  207. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  208. package/dist/demo/config.js +6 -6
  209. package/dist/demo/config.js.map +1 -1
  210. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  211. package/dist/demo/fetch-interceptor.js +13 -4
  212. package/dist/demo/fetch-interceptor.js.map +1 -1
  213. package/dist/demo/redact.d.ts +7 -7
  214. package/dist/demo/redact.d.ts.map +1 -1
  215. package/dist/demo/redact.js +8 -145
  216. package/dist/demo/redact.js.map +1 -1
  217. package/dist/extensions/html-shell.d.ts.map +1 -1
  218. package/dist/extensions/html-shell.js +2 -0
  219. package/dist/extensions/html-shell.js.map +1 -1
  220. package/dist/extensions/session-replay-iframe.d.ts +10 -0
  221. package/dist/extensions/session-replay-iframe.d.ts.map +1 -0
  222. package/dist/extensions/session-replay-iframe.js +121 -0
  223. package/dist/extensions/session-replay-iframe.js.map +1 -0
  224. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  225. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  226. package/dist/integrations/adapters/slack.js +98 -13
  227. package/dist/integrations/adapters/slack.js.map +1 -1
  228. package/dist/integrations/identity-links-store.d.ts +23 -0
  229. package/dist/integrations/identity-links-store.d.ts.map +1 -0
  230. package/dist/integrations/identity-links-store.js +158 -0
  231. package/dist/integrations/identity-links-store.js.map +1 -0
  232. package/dist/integrations/identity.d.ts +11 -0
  233. package/dist/integrations/identity.d.ts.map +1 -0
  234. package/dist/integrations/identity.js +100 -0
  235. package/dist/integrations/identity.js.map +1 -0
  236. package/dist/integrations/index.d.ts +2 -0
  237. package/dist/integrations/index.d.ts.map +1 -1
  238. package/dist/integrations/index.js +2 -0
  239. package/dist/integrations/index.js.map +1 -1
  240. package/dist/integrations/plugin.d.ts.map +1 -1
  241. package/dist/integrations/plugin.js +43 -1
  242. package/dist/integrations/plugin.js.map +1 -1
  243. package/dist/integrations/types.d.ts +6 -0
  244. package/dist/integrations/types.d.ts.map +1 -1
  245. package/dist/integrations/types.js.map +1 -1
  246. package/dist/mcp/build-server.d.ts +9 -2
  247. package/dist/mcp/build-server.d.ts.map +1 -1
  248. package/dist/mcp/build-server.js +97 -20
  249. package/dist/mcp/build-server.js.map +1 -1
  250. package/dist/mcp/external-agent-policy.d.ts +19 -0
  251. package/dist/mcp/external-agent-policy.d.ts.map +1 -0
  252. package/dist/mcp/external-agent-policy.js +2 -0
  253. package/dist/mcp/external-agent-policy.js.map +1 -0
  254. package/dist/mcp/index.d.ts +1 -0
  255. package/dist/mcp/index.d.ts.map +1 -1
  256. package/dist/mcp/index.js.map +1 -1
  257. package/dist/notifications/routes.d.ts +1 -1
  258. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  259. package/dist/scripts/agent-engines/list-agent-engines.js +1 -19
  260. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  261. package/dist/server/agent-chat/plugin-options.d.ts +11 -1
  262. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  263. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  264. package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
  265. package/dist/server/agent-chat/script-entries.js +16 -2
  266. package/dist/server/agent-chat/script-entries.js.map +1 -1
  267. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  268. package/dist/server/agent-chat-plugin.js +3 -0
  269. package/dist/server/agent-chat-plugin.js.map +1 -1
  270. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  271. package/dist/server/transcribe-voice.d.ts +1 -1
  272. package/dist/session-replay-iframe-protocol.d.ts +36 -0
  273. package/dist/session-replay-iframe-protocol.d.ts.map +1 -0
  274. package/dist/session-replay-iframe-protocol.js +24 -0
  275. package/dist/session-replay-iframe-protocol.js.map +1 -0
  276. package/dist/shared/reasoning-effort.d.ts +11 -0
  277. package/dist/shared/reasoning-effort.d.ts.map +1 -1
  278. package/dist/shared/reasoning-effort.js +36 -0
  279. package/dist/shared/reasoning-effort.js.map +1 -1
  280. package/dist/styles/agent-conversation.css +26 -0
  281. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  282. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  283. package/docs/content/durable-background-runs.mdx +37 -0
  284. package/docs/content/external-agents.mdx +62 -7
  285. package/docs/content/locales/ar-SA/external-agents.mdx +38 -7
  286. package/docs/content/locales/ar-SA/tracking.mdx +2 -0
  287. package/docs/content/locales/de-DE/external-agents.mdx +39 -7
  288. package/docs/content/locales/de-DE/tracking.mdx +2 -0
  289. package/docs/content/locales/es-ES/external-agents.mdx +39 -7
  290. package/docs/content/locales/es-ES/tracking.mdx +2 -0
  291. package/docs/content/locales/fr-FR/external-agents.mdx +39 -7
  292. package/docs/content/locales/fr-FR/tracking.mdx +4 -2
  293. package/docs/content/locales/hi-IN/external-agents.mdx +38 -7
  294. package/docs/content/locales/hi-IN/tracking.mdx +2 -0
  295. package/docs/content/locales/ja-JP/external-agents.mdx +38 -7
  296. package/docs/content/locales/ja-JP/tracking.mdx +2 -0
  297. package/docs/content/locales/ko-KR/external-agents.mdx +38 -7
  298. package/docs/content/locales/ko-KR/tracking.mdx +2 -0
  299. package/docs/content/locales/pt-BR/external-agents.mdx +39 -7
  300. package/docs/content/locales/pt-BR/tracking.mdx +2 -0
  301. package/docs/content/locales/zh-CN/external-agents.mdx +37 -7
  302. package/docs/content/locales/zh-CN/tracking.mdx +2 -0
  303. package/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  304. package/docs/content/locales/zh-TW/external-agents.mdx +37 -7
  305. package/docs/content/locales/zh-TW/tracking.mdx +2 -0
  306. package/docs/content/tracking.mdx +2 -0
  307. package/package.json +2 -2
  308. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  309. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  310. package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
  311. package/dist/observability/hosted-model-experiment.d.ts +0 -39
  312. package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
  313. package/dist/observability/hosted-model-experiment.js +0 -90
  314. package/dist/observability/hosted-model-experiment.js.map +0 -1
@@ -1,3 +1,12 @@
1
+ import {
2
+ SESSION_REPLAY_IFRAME_ATTRIBUTE,
3
+ SESSION_REPLAY_IFRAME_PROBE,
4
+ SESSION_REPLAY_IFRAME_START,
5
+ SESSION_REPLAY_IFRAME_STOP,
6
+ type SessionReplayIframePrivacyOptions,
7
+ type SessionReplayIframeStartMessage,
8
+ type SessionReplayIframeStopMessage,
9
+ } from "../session-replay-iframe-protocol.js";
1
10
  import {
2
11
  getOrCreateAnalyticsAnonymousId,
3
12
  getOrCreateAnalyticsSessionId,
@@ -7,6 +16,7 @@ import { scrubUrl } from "./url-scrub.js";
7
16
  type ReplayEvent = Record<string, unknown>;
8
17
  type QueuedReplayEvent = {
9
18
  json: string;
19
+ byteLength: number;
10
20
  timestampMs: number;
11
21
  type: number | null;
12
22
  };
@@ -65,9 +75,17 @@ interface SessionReplayState {
65
75
  flushTimer: number | null;
66
76
  maxDurationTimer: number | null;
67
77
  flushing: boolean;
78
+ /** Highest-priority flush requested while an upload was already in flight. */
79
+ pendingFlushReason: string | null;
80
+ /** Callers awaiting the coalesced flush tail. */
81
+ pendingFlushWaiters: Array<() => void>;
82
+ /** Drop incremental events until a new FullSnapshot can re-anchor the DOM. */
83
+ awaitingFullSnapshot: boolean;
68
84
  stopRecorder: ReplayStopFn | null;
69
85
  restoreUrlMonitor: (() => void) | null;
70
86
  removeLifecycleListeners: (() => void) | null;
87
+ /** Stops the cooperative child-iframe bridge and notifies active children. */
88
+ restoreIframeBridge: (() => void) | null;
71
89
  /** rrweb's `record.addCustomEvent`, captured at start (null when absent). */
72
90
  addCustomEvent: ((tag: string, payload: unknown) => void) | null;
73
91
  /** Uninstalls console/network interceptors and flushes pending duplicates. */
@@ -291,7 +309,9 @@ const SESSION_REPLAY_STATE_KEY = Symbol.for(
291
309
  "agent-native.client.sessionReplay",
292
310
  );
293
311
  const SESSION_REPLAY_ID_STORAGE_KEY = "agent-native.session_replay_id";
312
+ const SESSION_REPLAY_IFRAME_BLOCK_SELECTOR = `iframe[${SESSION_REPLAY_IFRAME_ATTRIBUTE}]`;
294
313
  const DEFAULT_BLOCK_SELECTOR = [
314
+ SESSION_REPLAY_IFRAME_BLOCK_SELECTOR,
295
315
  "[data-sensitive]",
296
316
  "[data-an-block]",
297
317
  "[data-an-private]",
@@ -311,11 +331,29 @@ const DEFAULT_BLOCK_SELECTOR = [
311
331
  const DEFAULT_IGNORE_SELECTOR = ".an-ignore, [data-an-ignore]";
312
332
  const DEFAULT_MASK_TEXT_CLASS = "an-mask";
313
333
  const DEFAULT_MASK_TEXT_SELECTOR = "[data-an-mask]";
334
+ const DEFAULT_MASK_INPUT_OPTIONS: Record<string, boolean> = {
335
+ color: true,
336
+ date: true,
337
+ "datetime-local": true,
338
+ email: true,
339
+ month: true,
340
+ number: true,
341
+ password: true,
342
+ range: true,
343
+ search: true,
344
+ tel: true,
345
+ text: true,
346
+ time: true,
347
+ url: true,
348
+ week: true,
349
+ };
314
350
  const DEFAULT_FLUSH_INTERVAL_MS = 5000;
315
351
  const DEFAULT_MAX_DURATION_MS = 30 * 60 * 1000;
316
352
  const DEFAULT_MAX_EVENTS_PER_BATCH = 50;
317
353
  const DEFAULT_MAX_BATCH_BYTES = 256 * 1024;
318
354
  const MAX_KEEPALIVE_REPLAY_UPLOAD_BYTES = 60 * 1024;
355
+ const REPLAY_TEXT_ENCODER =
356
+ typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
319
357
  const RRWEB_FULL_SNAPSHOT_EVENT_TYPE = 2;
320
358
  /** Cross-tab channel name used by the duplicated-tab claim guard. */
321
359
  const SESSION_REPLAY_BROADCAST_CHANNEL_NAME = "agent-native-session-replay";
@@ -403,9 +441,13 @@ function getState(): SessionReplayState {
403
441
  flushTimer: null,
404
442
  maxDurationTimer: null,
405
443
  flushing: false,
444
+ pendingFlushReason: null,
445
+ pendingFlushWaiters: [],
446
+ awaitingFullSnapshot: false,
406
447
  stopRecorder: null,
407
448
  restoreUrlMonitor: null,
408
449
  removeLifecycleListeners: null,
450
+ restoreIframeBridge: null,
409
451
  addCustomEvent: null,
410
452
  restoreCaptures: null,
411
453
  options: null,
@@ -418,6 +460,10 @@ function getState(): SessionReplayState {
418
460
  const state = g[SESSION_REPLAY_STATE_KEY]!;
419
461
  // Keep Vite HMR safe when an older recorder state survives a module reload.
420
462
  state.resourceNodes ??= new Map();
463
+ state.restoreIframeBridge ??= null;
464
+ state.pendingFlushReason ??= null;
465
+ state.pendingFlushWaiters ??= [];
466
+ state.awaitingFullSnapshot ??= false;
421
467
  return state;
422
468
  }
423
469
 
@@ -881,13 +927,20 @@ function normalizeOptions(
881
927
  checkoutEveryNth: options.checkoutEveryNth,
882
928
  checkoutEveryNms: options.checkoutEveryNms,
883
929
  inlineStylesheet: options.inlineStylesheet ?? true,
884
- blockSelector: options.blockSelector || DEFAULT_BLOCK_SELECTOR,
930
+ blockSelector: mergeReplayBlockSelector(
931
+ options.blockSelector || DEFAULT_BLOCK_SELECTOR,
932
+ ),
885
933
  ignoreSelector: options.ignoreSelector || DEFAULT_IGNORE_SELECTOR,
886
934
  maskTextClass: options.maskTextClass || DEFAULT_MASK_TEXT_CLASS,
887
935
  maskTextSelector: options.maskTextSelector || DEFAULT_MASK_TEXT_SELECTOR,
888
936
  maskAllInputs: options.maskAllInputs ?? true,
889
937
  recordCanvas: options.recordCanvas ?? false,
890
- recordCrossOriginIframes: options.recordCrossOriginIframes ?? false,
938
+ // A top-level app can safely aggregate cooperative child recorders. An
939
+ // app embedded by an unrelated cross-origin host must continue emitting
940
+ // and uploading its own events, so it only forwards to a parent when the
941
+ // caller explicitly opts in.
942
+ recordCrossOriginIframes:
943
+ options.recordCrossOriginIframes ?? window.parent === window,
891
944
  collectFonts: options.collectFonts ?? false,
892
945
  inlineImages: options.inlineImages ?? false,
893
946
  eventSampling: options.eventSampling ?? DEFAULT_EVENT_SAMPLING,
@@ -905,6 +958,12 @@ function normalizeOptions(
905
958
  };
906
959
  }
907
960
 
961
+ function mergeReplayBlockSelector(blockSelector: string): string {
962
+ return blockSelector.includes(SESSION_REPLAY_IFRAME_BLOCK_SELECTOR)
963
+ ? blockSelector
964
+ : `${blockSelector}, ${SESSION_REPLAY_IFRAME_BLOCK_SELECTOR}`;
965
+ }
966
+
908
967
  /**
909
968
  * Console/network capture default to ON whenever session replay records;
910
969
  * `false` disables a category, an object form overrides its caps. Network
@@ -1163,9 +1222,14 @@ function enqueueReplayEvent(
1163
1222
  event: ReplayEvent,
1164
1223
  ): void {
1165
1224
  if (!state.options) return;
1225
+ const eventType = typeof event.type === "number" ? event.type : null;
1226
+ if (state.awaitingFullSnapshot) {
1227
+ if (eventType !== RRWEB_FULL_SNAPSHOT_EVENT_TYPE) return;
1228
+ state.awaitingFullSnapshot = false;
1229
+ }
1166
1230
  const serialized = serializeReplayEvent(event, state.resourceNodes);
1167
1231
  if (!serialized) return;
1168
- const estimatedBytes = serialized.length;
1232
+ const estimatedBytes = replaySerializedBytes(serialized);
1169
1233
  if (
1170
1234
  state.queue.length > 0 &&
1171
1235
  state.queuedBytes + estimatedBytes > state.options.maxBatchBytes
@@ -1174,8 +1238,9 @@ function enqueueReplayEvent(
1174
1238
  }
1175
1239
  state.queue.push({
1176
1240
  json: serialized,
1241
+ byteLength: estimatedBytes,
1177
1242
  timestampMs: replayEventTimestampMs(event),
1178
- type: typeof event.type === "number" ? event.type : null,
1243
+ type: eventType,
1179
1244
  });
1180
1245
  state.queuedBytes += estimatedBytes;
1181
1246
  flushQueuedReplayIfNeeded(state);
@@ -1443,6 +1508,25 @@ function isFinalFlushReason(reason: string): boolean {
1443
1508
  ].includes(reason);
1444
1509
  }
1445
1510
 
1511
+ function flushReasonPriority(reason: string): number {
1512
+ // Unload reasons must retain their keepalive sequence reservation even when
1513
+ // another final request (for example, an explicit manual stop) is coalesced.
1514
+ if (reason === "pagehide" || reason === "beforeunload") return 3;
1515
+ if (isFinalFlushReason(reason)) return 2;
1516
+ if (reason === "visibility-hidden") return 1;
1517
+ return 0;
1518
+ }
1519
+
1520
+ function mergePendingFlushReason(
1521
+ current: string | null,
1522
+ requested: string,
1523
+ ): string {
1524
+ if (!current) return requested;
1525
+ return flushReasonPriority(requested) > flushReasonPriority(current)
1526
+ ? requested
1527
+ : current;
1528
+ }
1529
+
1446
1530
  function shouldReserveSequenceBeforeKeepalive(reason: string): boolean {
1447
1531
  return (
1448
1532
  reason === "pagehide" ||
@@ -1482,7 +1566,99 @@ function flushQueuedReplayIfNeeded(state: SessionReplayState): void {
1482
1566
  }
1483
1567
 
1484
1568
  function queuedReplayBytes(events: QueuedReplayEvent[]): number {
1485
- return events.reduce((total, event) => total + event.json.length, 0);
1569
+ return events.reduce(
1570
+ (total, event) => total + queuedReplayEventBytes(event),
1571
+ 0,
1572
+ );
1573
+ }
1574
+
1575
+ function replaySerializedBytes(value: string): number {
1576
+ if (REPLAY_TEXT_ENCODER) return REPLAY_TEXT_ENCODER.encode(value).byteLength;
1577
+ if (typeof Blob !== "undefined") return new Blob([value]).size;
1578
+ return value.length;
1579
+ }
1580
+
1581
+ function queuedReplayEventBytes(event: QueuedReplayEvent): number {
1582
+ return Number.isFinite(event.byteLength)
1583
+ ? event.byteLength
1584
+ : replaySerializedBytes(event.json);
1585
+ }
1586
+
1587
+ /**
1588
+ * Remove one bounded FIFO prefix from the live queue.
1589
+ *
1590
+ * Threshold-triggered flushes may arrive while another upload is active. The
1591
+ * old `queue.splice(0)` drained that entire accumulated backlog on the next
1592
+ * flush, bypassing both byte and event caps. Keep FullSnapshots isolated and
1593
+ * always take at least one event so an individually large snapshot can still
1594
+ * make progress.
1595
+ */
1596
+ function takeQueuedReplayBatch(state: SessionReplayState): QueuedReplayEvent[] {
1597
+ const options = state.options;
1598
+ if (!options || state.queue.length === 0) return [];
1599
+
1600
+ let count = 0;
1601
+ let bytes = 0;
1602
+ for (const event of state.queue) {
1603
+ if (count > 0 && event.type === RRWEB_FULL_SNAPSHOT_EVENT_TYPE) break;
1604
+ if (
1605
+ count > 0 &&
1606
+ (count >= options.maxEventsPerBatch ||
1607
+ bytes + queuedReplayEventBytes(event) > options.maxBatchBytes)
1608
+ ) {
1609
+ break;
1610
+ }
1611
+ count += 1;
1612
+ bytes += queuedReplayEventBytes(event);
1613
+ if (event.type === RRWEB_FULL_SNAPSHOT_EVENT_TYPE) break;
1614
+ }
1615
+
1616
+ const events = state.queue.splice(0, Math.max(1, count));
1617
+ state.queuedBytes = queuedReplayBytes(state.queue);
1618
+ return events;
1619
+ }
1620
+
1621
+ function splitReplayBatch(
1622
+ events: QueuedReplayEvent[],
1623
+ ): [QueuedReplayEvent[], QueuedReplayEvent[]] | null {
1624
+ if (events.length < 2) return null;
1625
+ const targetBytes = queuedReplayBytes(events) / 2;
1626
+ let splitAt = 1;
1627
+ let bytes = events[0] ? queuedReplayEventBytes(events[0]) : 0;
1628
+ while (splitAt < events.length - 1 && bytes < targetBytes) {
1629
+ const event = events[splitAt];
1630
+ if (event) bytes += queuedReplayEventBytes(event);
1631
+ splitAt += 1;
1632
+ }
1633
+ return [events.slice(0, splitAt), events.slice(splitAt)];
1634
+ }
1635
+
1636
+ function replayBatchNeedsDomReset(events: QueuedReplayEvent[]): boolean {
1637
+ return events.some((event) => {
1638
+ if (event.type === RRWEB_FULL_SNAPSHOT_EVENT_TYPE) return true;
1639
+ if (event.type !== 3) return false;
1640
+ try {
1641
+ const parsed = JSON.parse(event.json) as { data?: { source?: unknown } };
1642
+ // rrweb IncrementalSource.Mutation is zero. Dropping one may leave later
1643
+ // node references dangling until another FullSnapshot resets the mirror.
1644
+ return parsed.data?.source === 0;
1645
+ } catch {
1646
+ return true;
1647
+ }
1648
+ });
1649
+ }
1650
+
1651
+ function quarantinePendingReplayUntilFullSnapshot(
1652
+ state: SessionReplayState,
1653
+ ): void {
1654
+ const pending = [...state.retryBatches.flat(), ...state.queue];
1655
+ const resetAt = pending.findIndex(
1656
+ (event) => event.type === RRWEB_FULL_SNAPSHOT_EVENT_TYPE,
1657
+ );
1658
+ state.retryBatches = [];
1659
+ state.queue = resetAt >= 0 ? pending.slice(resetAt) : [];
1660
+ state.queuedBytes = queuedReplayBytes(state.queue);
1661
+ state.awaitingFullSnapshot = resetAt < 0;
1486
1662
  }
1487
1663
 
1488
1664
  function restoreReplayEvents(
@@ -1523,9 +1699,18 @@ function rollbackReplaySequenceReservation(
1523
1699
 
1524
1700
  export async function flushSessionReplay(reason = "manual"): Promise<void> {
1525
1701
  const state = getState();
1526
- if (!state.options || !hasPendingReplayBatch(state) || state.flushing) return;
1527
- const events = state.retryBatches.shift() ?? state.queue.splice(0);
1528
- state.queuedBytes = queuedReplayBytes(state.queue);
1702
+ if (!state.options) return;
1703
+ if (state.flushing) {
1704
+ state.pendingFlushReason = mergePendingFlushReason(
1705
+ state.pendingFlushReason,
1706
+ reason,
1707
+ );
1708
+ return new Promise<void>((resolve) => {
1709
+ state.pendingFlushWaiters.push(resolve);
1710
+ });
1711
+ }
1712
+ if (!hasPendingReplayBatch(state)) return;
1713
+ const events = state.retryBatches.shift() ?? takeQueuedReplayBatch(state);
1529
1714
  const payload = buildReplayBody(state, reason, events);
1530
1715
  if (!payload || !state.options) {
1531
1716
  restoreReplayEvents(state, events);
@@ -1534,6 +1719,8 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
1534
1719
  state.flushing = true;
1535
1720
  let uploaded = false;
1536
1721
  let reservedSequence = false;
1722
+ let splitRejectedBatch = false;
1723
+ let droppedOversizedBatch = false;
1537
1724
  let definitiveClientErrorStatus: number | null = null;
1538
1725
  try {
1539
1726
  await sendReplayUpload(state.options, payload.body, {
@@ -1553,10 +1740,33 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
1553
1740
  // never succeed by retrying the exact same batch -- requeuing it would
1554
1741
  // just spin forever, blocking every later batch behind it (flushes are
1555
1742
  // FIFO via `retryBatches`). Drop it and move on instead.
1743
+ const rejectedStatus =
1744
+ error instanceof ReplayUploadHttpError ? error.status : null;
1745
+ const splitBatch = rejectedStatus === 413 ? splitReplayBatch(events) : null;
1746
+ const isUnsplittableOversizedBatch =
1747
+ rejectedStatus === 413 && splitBatch === null;
1556
1748
  const isDefinitiveClientError =
1557
1749
  error instanceof ReplayUploadHttpError &&
1558
- isDefinitiveReplayUploadClientError(error.status);
1559
- if (isDefinitiveClientError) {
1750
+ isDefinitiveReplayUploadClientError(error.status) &&
1751
+ !splitBatch &&
1752
+ !isUnsplittableOversizedBatch;
1753
+ if (splitBatch) {
1754
+ // A server or platform can enforce a stricter decompressed-body limit
1755
+ // than the recorder's configured queue cap. Bisect in FIFO order and
1756
+ // retry both halves at the same sequence; only successful halves advance
1757
+ // it, so no event is duplicated or skipped.
1758
+ state.retryBatches.unshift(...splitBatch);
1759
+ splitRejectedBatch = true;
1760
+ } else if (isUnsplittableOversizedBatch) {
1761
+ // This one event cannot be made any smaller. Drop only the rejected
1762
+ // singleton and keep later retry halves/live batches in FIFO order. If
1763
+ // it carried DOM structure, quarantine dependent mutations until a new
1764
+ // FullSnapshot can safely re-anchor rrweb's node mirror.
1765
+ droppedOversizedBatch = true;
1766
+ if (replayBatchNeedsDomReset(events)) {
1767
+ quarantinePendingReplayUntilFullSnapshot(state);
1768
+ }
1769
+ } else if (isDefinitiveClientError) {
1560
1770
  // Continuing after a checksum/sequence conflict would reuse the same
1561
1771
  // rejected sequence forever. More importantly, advancing past it would
1562
1772
  // append mutations to a replay whose DOM stream may belong to another
@@ -1575,7 +1785,17 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
1575
1785
  const previousInternal = replayCaptureInternal;
1576
1786
  replayCaptureInternal = true;
1577
1787
  try {
1578
- if (isDefinitiveClientError) {
1788
+ if (splitRejectedBatch) {
1789
+ console.warn(
1790
+ "[session-replay] splitting oversized upload (HTTP 413)",
1791
+ error,
1792
+ );
1793
+ } else if (droppedOversizedBatch) {
1794
+ console.warn(
1795
+ "[session-replay] dropping oversized replay event (HTTP 413)",
1796
+ error,
1797
+ );
1798
+ } else if (isDefinitiveClientError) {
1579
1799
  console.warn(
1580
1800
  `[session-replay] dropping upload (HTTP ${(error as ReplayUploadHttpError).status})`,
1581
1801
  error,
@@ -1589,8 +1809,33 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
1589
1809
  } finally {
1590
1810
  state.flushing = false;
1591
1811
  }
1592
- if (uploaded && hasPendingReplayBatch(state)) {
1593
- flushQueuedReplayIfNeeded(state);
1812
+ const coalescedReason = state.pendingFlushReason;
1813
+ const coalescedWaiters = coalescedReason
1814
+ ? state.pendingFlushWaiters.splice(0)
1815
+ : [];
1816
+ if (coalescedReason) state.pendingFlushReason = null;
1817
+
1818
+ if (coalescedReason) {
1819
+ // A stop/unload request that arrived during this upload owns the tail.
1820
+ // Its higher-priority reason must replace threshold/internal reasons so a
1821
+ // small final batch is not stranded or mislabeled as active.
1822
+ await flushSessionReplay(coalescedReason);
1823
+ } else if (splitRejectedBatch || droppedOversizedBatch) {
1824
+ // Preserve final-status and unload semantics through every half. In
1825
+ // particular, pagehide/beforeunload retries must still reserve their
1826
+ // sequence before a keepalive fetch, and completed flushes must not be
1827
+ // rewritten to an internal recovery reason/status.
1828
+ await flushSessionReplay(reason);
1829
+ } else if (uploaded && hasPendingReplayBatch(state)) {
1830
+ const mustContinue =
1831
+ state.retryBatches.length > 0 ||
1832
+ isFinalFlushReason(reason) ||
1833
+ shouldFlushQueuedReplay(state);
1834
+ if (mustContinue) {
1835
+ if (isFinalFlushReason(reason)) {
1836
+ await flushSessionReplay(reason);
1837
+ } else void flushSessionReplay(reason);
1838
+ }
1594
1839
  }
1595
1840
  if (
1596
1841
  definitiveClientErrorStatus !== null &&
@@ -1639,6 +1884,7 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
1639
1884
  // best-effort telemetry must never interfere with recording recovery
1640
1885
  }
1641
1886
  }
1887
+ for (const resolve of coalescedWaiters) resolve();
1642
1888
  }
1643
1889
 
1644
1890
  /**
@@ -1737,6 +1983,101 @@ function installLifecycleListeners(state: SessionReplayState): void {
1737
1983
  };
1738
1984
  }
1739
1985
 
1986
+ function markedSessionReplayIframes(): HTMLIFrameElement[] {
1987
+ if (typeof document.querySelectorAll !== "function") return [];
1988
+ return Array.from(
1989
+ document.querySelectorAll<HTMLIFrameElement>(
1990
+ `iframe[${SESSION_REPLAY_IFRAME_ATTRIBUTE}]`,
1991
+ ),
1992
+ );
1993
+ }
1994
+
1995
+ function markedSessionReplayIframeForSource(
1996
+ source: MessageEventSource | null,
1997
+ ): HTMLIFrameElement | null {
1998
+ if (!source) return null;
1999
+ return (
2000
+ markedSessionReplayIframes().find(
2001
+ (iframe) => iframe.contentWindow === source,
2002
+ ) ?? null
2003
+ );
2004
+ }
2005
+
2006
+ function sessionReplayIframePrivacyOptions(
2007
+ options: NormalizedSessionReplayOptions,
2008
+ ): SessionReplayIframePrivacyOptions {
2009
+ return {
2010
+ blockSelector: options.blockSelector,
2011
+ ignoreSelector: options.ignoreSelector,
2012
+ maskTextClass: options.maskTextClass,
2013
+ maskTextSelector: options.maskTextSelector,
2014
+ maskAllInputs: options.maskAllInputs,
2015
+ maskInputOptions: DEFAULT_MASK_INPUT_OPTIONS,
2016
+ recordCanvas: options.recordCanvas,
2017
+ collectFonts: options.collectFonts,
2018
+ inlineImages: options.inlineImages,
2019
+ sampling: options.eventSampling,
2020
+ };
2021
+ }
2022
+
2023
+ function postSessionReplayIframeMessage(
2024
+ iframe: HTMLIFrameElement,
2025
+ message: SessionReplayIframeStartMessage | SessionReplayIframeStopMessage,
2026
+ ): void {
2027
+ try {
2028
+ iframe.contentWindow?.postMessage(message, "*");
2029
+ } catch {
2030
+ // The frame may have navigated or detached between discovery and send.
2031
+ }
2032
+ }
2033
+
2034
+ /**
2035
+ * Cooperative opaque/cross-origin iframe recording.
2036
+ *
2037
+ * rrweb cannot inspect these documents from the host. Framework-owned child
2038
+ * frames carry a marker and inject a tiny recorder that probes its direct
2039
+ * parent. Only an active host recorder responds, and only when the probe's
2040
+ * source is the contentWindow of a currently marked direct iframe.
2041
+ */
2042
+ function installSessionReplayIframeBridge(
2043
+ state: SessionReplayState,
2044
+ options: NormalizedSessionReplayOptions,
2045
+ ): void {
2046
+ if (!options.recordCrossOriginIframes || state.restoreIframeBridge) return;
2047
+
2048
+ const startMessage: SessionReplayIframeStartMessage = {
2049
+ type: SESSION_REPLAY_IFRAME_START,
2050
+ options: sessionReplayIframePrivacyOptions(options),
2051
+ };
2052
+ const stopMessage: SessionReplayIframeStopMessage = {
2053
+ type: SESSION_REPLAY_IFRAME_STOP,
2054
+ };
2055
+ const sendStart = (iframe: HTMLIFrameElement) =>
2056
+ postSessionReplayIframeMessage(iframe, startMessage);
2057
+ const onMessage = (event: MessageEvent) => {
2058
+ if (!state.active) return;
2059
+ if (
2060
+ !event.data ||
2061
+ typeof event.data !== "object" ||
2062
+ event.data.type !== SESSION_REPLAY_IFRAME_PROBE
2063
+ ) {
2064
+ return;
2065
+ }
2066
+ const iframe = markedSessionReplayIframeForSource(event.source);
2067
+ if (iframe) sendStart(iframe);
2068
+ };
2069
+
2070
+ window.addEventListener("message", onMessage);
2071
+ for (const iframe of markedSessionReplayIframes()) sendStart(iframe);
2072
+ state.restoreIframeBridge = () => {
2073
+ window.removeEventListener("message", onMessage);
2074
+ for (const iframe of markedSessionReplayIframes()) {
2075
+ postSessionReplayIframeMessage(iframe, stopMessage);
2076
+ }
2077
+ state.restoreIframeBridge = null;
2078
+ };
2079
+ }
2080
+
1740
2081
  function truncateCaptureText(value: string, maxLength: number): string {
1741
2082
  return value.length > maxLength ? value.slice(0, maxLength) : value;
1742
2083
  }
@@ -2616,6 +2957,9 @@ async function startSessionReplayRecorder(
2616
2957
  state.queue = [];
2617
2958
  state.queuedBytes = 0;
2618
2959
  state.retryBatches = [];
2960
+ state.pendingFlushReason = null;
2961
+ for (const resolve of state.pendingFlushWaiters.splice(0)) resolve();
2962
+ state.awaitingFullSnapshot = false;
2619
2963
  state.resourceNodes.clear();
2620
2964
  state.stopRecorder = null;
2621
2965
  state.broadcastChannel = replayChannel;
@@ -2640,22 +2984,7 @@ async function startSessionReplayRecorder(
2640
2984
  recordCrossOriginIframes: normalized.recordCrossOriginIframes,
2641
2985
  collectFonts: normalized.collectFonts,
2642
2986
  inlineImages: normalized.inlineImages,
2643
- maskInputOptions: {
2644
- color: true,
2645
- date: true,
2646
- "datetime-local": true,
2647
- email: true,
2648
- month: true,
2649
- number: true,
2650
- password: true,
2651
- range: true,
2652
- search: true,
2653
- tel: true,
2654
- text: true,
2655
- time: true,
2656
- url: true,
2657
- week: true,
2658
- },
2987
+ maskInputOptions: DEFAULT_MASK_INPUT_OPTIONS,
2659
2988
  });
2660
2989
  if (typeof stopRecorder !== "function") {
2661
2990
  state.active = false;
@@ -2682,6 +3011,7 @@ async function startSessionReplayRecorder(
2682
3011
  );
2683
3012
  installUrlMonitor(state);
2684
3013
  installLifecycleListeners(state);
3014
+ installSessionReplayIframeBridge(state, normalized);
2685
3015
  state.addCustomEvent =
2686
3016
  typeof rrweb.record.addCustomEvent === "function"
2687
3017
  ? rrweb.record.addCustomEvent
@@ -2700,6 +3030,7 @@ async function startSessionReplayRecorder(
2700
3030
  // best-effort interceptor teardown
2701
3031
  }
2702
3032
  state.restoreCaptures = null;
3033
+ state.restoreIframeBridge?.();
2703
3034
  state.addCustomEvent = null;
2704
3035
  state.active = false;
2705
3036
  state.options = null;
@@ -2728,6 +3059,7 @@ export async function stopSessionReplay(reason = "manual"): Promise<void> {
2728
3059
  // best-effort interceptor teardown
2729
3060
  }
2730
3061
  state.restoreCaptures = null;
3062
+ state.restoreIframeBridge?.();
2731
3063
  state.active = false;
2732
3064
  state.addCustomEvent = null;
2733
3065
  try {
@@ -707,6 +707,22 @@ function dispatchActivityClear(tabId: string | undefined) {
707
707
  );
708
708
  }
709
709
 
710
+ // Fires once per chunk (see ProcessEventState.streamProgressDispatched) the
711
+ // moment a chunk produces real model output (visible text or a reasoning
712
+ // delta). Unlike `agent-chat:activity-clear` — which also fires for
713
+ // old-chunk `tool_done` replays and server-retry `clear` events, and so
714
+ // cannot be used to detect genuine forward progress — this event exists
715
+ // solely so listeners can distinguish "the run is actually producing new
716
+ // output" from those replay/retry cases.
717
+ function dispatchStreamProgress(tabId: string | undefined) {
718
+ if (typeof window === "undefined") return;
719
+ window.dispatchEvent(
720
+ new CustomEvent("agent-chat:stream-progress", {
721
+ detail: { tabId },
722
+ }),
723
+ );
724
+ }
725
+
710
726
  function dispatchMissingApiKey(tabId: string | undefined) {
711
727
  if (typeof window === "undefined") return;
712
728
  window.dispatchEvent(
@@ -946,6 +962,11 @@ function completedToolOnlyMessage(toolNames: string[]): string | null {
946
962
 
947
963
  interface ProcessEventState {
948
964
  completedToolsAfterLastAssistantText: Set<string>;
965
+ /** Set once `agent-chat:stream-progress` has been dispatched for the
966
+ * current chunk, so a burst of per-token text/reasoning deltas only fires
967
+ * it once. Cleared by `resetProcessEventState` on a server `clear` retry
968
+ * so the next batch of real output re-arms it. */
969
+ streamProgressDispatched: boolean;
949
970
  }
950
971
 
951
972
  function markAssistantText(state: ProcessEventState | undefined) {
@@ -961,6 +982,18 @@ function markCompletedToolAfterAssistantText(
961
982
 
962
983
  function resetProcessEventState(state: ProcessEventState | undefined) {
963
984
  state?.completedToolsAfterLastAssistantText.clear();
985
+ if (state) state.streamProgressDispatched = false;
986
+ }
987
+
988
+ // Returns true the first time it's called for a given state (or every time
989
+ // when state is unavailable, since there is nothing to dedupe against) and
990
+ // false on subsequent calls until `resetProcessEventState` re-arms it.
991
+ function shouldDispatchStreamProgress(
992
+ state: ProcessEventState | undefined,
993
+ ): boolean {
994
+ if (state?.streamProgressDispatched) return false;
995
+ if (state) state.streamProgressDispatched = true;
996
+ return true;
964
997
  }
965
998
 
966
999
  /**
@@ -1005,7 +1038,13 @@ export function processEvent(
1005
1038
  // activity label so a transient "Contacting model" / "Still generating
1006
1039
  // image" doesn't linger beside streamed text. Idempotent (clears once, then
1007
1040
  // no-ops) so per-token text deltas stay cheap.
1008
- if (ev.text) dispatchActivityClear(tabId);
1041
+ if (ev.text) {
1042
+ dispatchActivityClear(tabId);
1043
+ // Real output resumed — let listeners (e.g. the auto-resume "Resuming…"
1044
+ // indicator) clear state that must NOT be cleared by activity-clear
1045
+ // alone, since activity-clear also fires for old-chunk/retry replays.
1046
+ if (shouldDispatchStreamProgress(state)) dispatchStreamProgress(tabId);
1047
+ }
1009
1048
  if (ev.text?.trim()) markAssistantText(state);
1010
1049
  const lastPart = content[content.length - 1];
1011
1050
  if (lastPart && lastPart.type === "text") {
@@ -1024,6 +1063,7 @@ export function processEvent(
1024
1063
  // part so the UI can render a single collapsible "Thinking" cell.
1025
1064
  const delta = ev.text ?? "";
1026
1065
  if (!delta) return { action: "continue" };
1066
+ if (shouldDispatchStreamProgress(state)) dispatchStreamProgress(tabId);
1027
1067
  const lastPart = content[content.length - 1];
1028
1068
  if (lastPart && lastPart.type === "reasoning") {
1029
1069
  lastPart.text += delta;
@@ -1567,6 +1607,7 @@ export async function* readSSEStream(
1567
1607
  options?.preparingActionState ?? {};
1568
1608
  const processEventState: ProcessEventState = {
1569
1609
  completedToolsAfterLastAssistantText: new Set(),
1610
+ streamProgressDispatched: false,
1570
1611
  };
1571
1612
 
1572
1613
  const withStreamMetadata = (r: ChatModelRunResult): ChatModelRunResult => {
@@ -1769,6 +1810,7 @@ export async function readSSEStreamRaw(
1769
1810
  options?.preparingActionState ?? {};
1770
1811
  const processEventState: ProcessEventState = {
1771
1812
  completedToolsAfterLastAssistantText: new Set(),
1813
+ streamProgressDispatched: false,
1772
1814
  };
1773
1815
  // Tracks whether the most recent content state was already pushed via
1774
1816
  // onUpdate inside the loop, so the post-loop flush below doesn't emit the
@@ -5,7 +5,7 @@ import { writeAppState } from "../../application-state/script-helpers.js";
5
5
 
6
6
  export default defineAction({
7
7
  description:
8
- "Turn demo mode on or off. When demo mode is on, the UI replaces email addresses with realistic fake data and supported dashboards reshape chart values for presentations. Agent-visible results replace emails and numbers while keeping names, free text, labels, IDs, dates, and structure intact. Use when the user asks to 'hide my data', 'turn on demo mode', 'anonymize this for a screen share / recording', or similar. This is the same toggle as the Demo mode switch in settings.",
8
+ "Turn demo mode on or off. When demo mode is on, the UI replaces every email address with anonymous@builder.io and supported dashboards reshape chart values for presentations. Agent-visible results use the same anonymous email and anonymize numbers while keeping names, free text, labels, IDs, dates, and structure intact. Use when the user asks to 'hide my data', 'turn on demo mode', 'anonymize this for a screen share / recording', or similar. This is the same toggle as the Demo mode switch in settings.",
9
9
  schema: z.object({
10
10
  enabled: z
11
11
  .boolean()