@agent-native/core 0.84.67 → 0.85.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 (145) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +55 -0
  4. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
  5. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
  6. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
  7. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
  8. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
  9. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
  10. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
  11. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
  12. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
  13. package/corpus/core/docs/content/tracking.mdx +2 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/production-agent.ts +199 -38
  16. package/corpus/core/src/agent/run-manager.ts +79 -0
  17. package/corpus/core/src/agent/run-store.ts +117 -5
  18. package/corpus/core/src/agent/types.ts +13 -0
  19. package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
  20. package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
  21. package/corpus/core/src/client/analytics.ts +2 -0
  22. package/corpus/core/src/client/error-format.ts +4 -0
  23. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
  24. package/corpus/core/src/client/session-replay.ts +774 -2
  25. package/corpus/core/src/client/sse-event-processor.ts +65 -9
  26. package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
  27. package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
  28. package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
  29. package/corpus/core/src/server/core-routes-plugin.ts +91 -4
  30. package/corpus/core/src/server/http-response-telemetry.ts +189 -0
  31. package/corpus/core/src/server/self-dispatch.ts +29 -0
  32. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
  33. package/corpus/templates/analytics/AGENTS.md +13 -0
  34. package/corpus/templates/analytics/actions/view-screen.ts +25 -0
  35. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
  36. package/corpus/templates/analytics/app/global.css +55 -0
  37. package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
  38. package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
  39. package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
  40. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
  42. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
  45. package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
  46. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
  47. package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
  52. package/corpus/templates/analytics/server/db/schema.ts +3 -0
  53. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
  54. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
  55. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
  56. package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
  57. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
  58. package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
  59. package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
  60. package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
  61. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  62. package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
  63. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  64. package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
  65. package/corpus/templates/plan/server/plans.ts +26 -9
  66. package/dist/agent/production-agent.d.ts.map +1 -1
  67. package/dist/agent/production-agent.js +155 -34
  68. package/dist/agent/production-agent.js.map +1 -1
  69. package/dist/agent/run-manager.d.ts +29 -0
  70. package/dist/agent/run-manager.d.ts.map +1 -1
  71. package/dist/agent/run-manager.js +73 -0
  72. package/dist/agent/run-manager.js.map +1 -1
  73. package/dist/agent/run-store.d.ts +41 -0
  74. package/dist/agent/run-store.d.ts.map +1 -1
  75. package/dist/agent/run-store.js +100 -4
  76. package/dist/agent/run-store.js.map +1 -1
  77. package/dist/agent/types.d.ts +13 -0
  78. package/dist/agent/types.d.ts.map +1 -1
  79. package/dist/agent/types.js.map +1 -1
  80. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  81. package/dist/client/RunStuckBanner.js +13 -0
  82. package/dist/client/RunStuckBanner.js.map +1 -1
  83. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  84. package/dist/client/agent-chat-adapter.js +410 -0
  85. package/dist/client/agent-chat-adapter.js.map +1 -1
  86. package/dist/client/analytics.d.ts +1 -1
  87. package/dist/client/analytics.d.ts.map +1 -1
  88. package/dist/client/analytics.js.map +1 -1
  89. package/dist/client/error-format.d.ts.map +1 -1
  90. package/dist/client/error-format.js +2 -0
  91. package/dist/client/error-format.js.map +1 -1
  92. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  93. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  94. package/dist/client/session-replay.d.ts +36 -0
  95. package/dist/client/session-replay.d.ts.map +1 -1
  96. package/dist/client/session-replay.js +579 -1
  97. package/dist/client/session-replay.js.map +1 -1
  98. package/dist/client/sse-event-processor.d.ts +20 -1
  99. package/dist/client/sse-event-processor.d.ts.map +1 -1
  100. package/dist/client/sse-event-processor.js +42 -11
  101. package/dist/client/sse-event-processor.js.map +1 -1
  102. package/dist/client/use-run-stuck-detection.d.ts +14 -4
  103. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  104. package/dist/client/use-run-stuck-detection.js +18 -4
  105. package/dist/client/use-run-stuck-detection.js.map +1 -1
  106. package/dist/collab/awareness.d.ts +2 -2
  107. package/dist/collab/awareness.d.ts.map +1 -1
  108. package/dist/collab/routes.d.ts +2 -2
  109. package/dist/db/runtime-diagnostics.d.ts +52 -0
  110. package/dist/db/runtime-diagnostics.d.ts.map +1 -0
  111. package/dist/db/runtime-diagnostics.js +250 -0
  112. package/dist/db/runtime-diagnostics.js.map +1 -0
  113. package/dist/notifications/routes.d.ts +1 -1
  114. package/dist/observability/routes.d.ts +3 -3
  115. package/dist/progress/routes.d.ts +1 -1
  116. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/secrets/routes.d.ts +3 -3
  119. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  120. package/dist/server/agent-chat-plugin.js +101 -2
  121. package/dist/server/agent-chat-plugin.js.map +1 -1
  122. package/dist/server/core-routes-plugin.d.ts +17 -0
  123. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  124. package/dist/server/core-routes-plugin.js +65 -5
  125. package/dist/server/core-routes-plugin.js.map +1 -1
  126. package/dist/server/http-response-telemetry.d.ts +4 -0
  127. package/dist/server/http-response-telemetry.d.ts.map +1 -0
  128. package/dist/server/http-response-telemetry.js +174 -0
  129. package/dist/server/http-response-telemetry.js.map +1 -0
  130. package/dist/server/self-dispatch.d.ts +17 -0
  131. package/dist/server/self-dispatch.d.ts.map +1 -1
  132. package/dist/server/self-dispatch.js +11 -0
  133. package/dist/server/self-dispatch.js.map +1 -1
  134. package/docs/content/locales/ar-SA/tracking.mdx +55 -0
  135. package/docs/content/locales/de-DE/tracking.mdx +55 -0
  136. package/docs/content/locales/es-ES/tracking.mdx +55 -0
  137. package/docs/content/locales/fr-FR/tracking.mdx +55 -0
  138. package/docs/content/locales/hi-IN/tracking.mdx +55 -0
  139. package/docs/content/locales/ja-JP/tracking.mdx +55 -0
  140. package/docs/content/locales/ko-KR/tracking.mdx +55 -0
  141. package/docs/content/locales/pt-BR/tracking.mdx +55 -0
  142. package/docs/content/locales/zh-CN/tracking.mdx +55 -0
  143. package/docs/content/locales/zh-TW/tracking.mdx +55 -0
  144. package/docs/content/tracking.mdx +2 -0
  145. package/package.json +1 -1
@@ -188,6 +188,61 @@ track("checkout.completed", { total: 49.99, items: 3 });
188
188
 
189
189
  这与框架的内部浏览器遥测(`trackEvent()` /自动页面浏览量 - 请参阅下面的 [Browser defaults](#browser-defaults))不同,后者为 Agent Native 自己的产品分析提供支持。将 `track()` 用于您的应用自己的分析事件,这些事件应到达您配置的提供商。
190
190
 
191
+ ## 会话重放 {#session-replay}
192
+
193
+ Agent Native 应用无需添加第二个分析 SDK,即可接入第一方浏览器会话重放功能。在浏览器根部调用一次 `configureTracking()`,并传入 Analytics 公钥和采集端点:
194
+
195
+ ```ts
196
+ import { configureTracking } from "@agent-native/core/client";
197
+
198
+ configureTracking({
199
+ key: "anpk_...",
200
+ endpoint: "https://analytics.example.com/api/analytics/track",
201
+ sessionReplay: {
202
+ enabled: true,
203
+ sampleRate: 0.1,
204
+ },
205
+ getDefaultProps: (_event, props) => ({
206
+ ...props,
207
+ app: "my-app",
208
+ template: "my-template",
209
+ }),
210
+ });
211
+ ```
212
+
213
+ 当 `sessionReplay.enabled` 为真值时,客户端会在启动后动态导入 `@rrweb/record`,并将重放数据块发送到重放端点。如果 `endpoint` 以 `/api/analytics/track` 或 `/track` 结尾,重放端点会自动推导为 `/api/analytics/replay`。当重放采集器位于其他位置时,可通过 `sessionReplay.endpoint` 显式覆盖。
214
+
215
+ Agent Native 模板根已经调用了 `configureTracking()`。托管模板部署可以通过 Vite/Netlify 环境变量开启重放,而库使用者则应优先使用上面显式的 `configureTracking({ key, endpoint, sessionReplay })` 形式:
216
+
217
+ ```bash
218
+ VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY=anpk_...
219
+ VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=https://analytics.example.com/api/analytics/track
220
+ VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=true
221
+ VITE_AGENT_NATIVE_SESSION_REPLAY_SAMPLE_RATE=0.1
222
+ ```
223
+
224
+ 浏览器端辅助函数还会尽力地、非阻塞地读取当前 Agent Native 认证会话。默认情况下,重放仅限已登录用户:启用 `sessionReplay` 后,除非会话解析出用户邮箱地址,否则不会开始录制。已登录用户的重放会包含基于邮箱的 `userId`/`userEmail` 以及 `orgId`。
225
+
226
+ 仅在有意进行匿名重放部署时,才设置 `sessionReplay.requireSignedInUser: false` 或 `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false`。禁用认证门控后,匿名录制仍可按匿名访客、会话、应用/模板、主机名和路径进行查询。
227
+
228
+ 会话重放按浏览器会话确定性采样。`sampleRate` 为 `0.1` 时,会记录约 10% 的符合条件的会话;当符合条件的用户群体本就很小时(例如仅限登录用户的内部测试),可使用 `1`。
229
+
230
+ 隐私默认设置刻意保持保守,但仍能满足回放的可用性:
231
+
232
+ - 默认屏蔽输入内容(`maskAllInputs: true`)。
233
+ - 页面文本默认保持可见,除非元素被标记为 `.an-mask` 或 `data-an-mask`。
234
+ - 敏感区域会通过诸如 `[data-sensitive]`、`.an-block`、`.an-private`、`data-an-block`、`data-an-private` 等选择器以及类似信用卡/密码/社会安全号码的字段进行屏蔽。
235
+ - URL 会使用与浏览器分析相同的 `scrubUrl()` 辅助函数进行清理。
236
+ - 重放采集仅限 Web 端且为选择性加入(opt-in);不会录制原生桌面屏幕。
237
+
238
+ 录制期间,会话重放还会捕获浏览器控制台输出(`log`、`info`、`warn`、`error`、`debug`,以及 window 的 `error` / `unhandledrejection`)和网络请求元数据(`fetch` 和 XHR),并将其作为带标签的 rrweb 自定义事件记录下来,以便代理和重放查看器调试用户报告的问题。启用重放后,此项捕获默认开启;可通过 `sessionReplay.console` 和 `sessionReplay.network` 选项进行调整或禁用,两者均可接受布尔值或选项对象。请求/响应的正文和请求头永远不会被捕获,URL 会被清理,消息会被截断,录制器自身的上报/跟踪流量会被排除,并且每个会话的配额(1000 条控制台事件 / 2000 条网络事件)在超出时会附加截断提示。
239
+
240
+ Analytics 模板将重放元数据存储在 SQL(`session_recordings`)中,并通过私有 blob 引用(`session_replay_chunks`)存储数据块。浏览器和代理永远不会收到提供商的 URL。回放通过受限的服务器路由进行,默认的代理工具返回的是摘要或有限范围的重放事件,而非原始数据块表的直接访问权限。
241
+
242
+ 当需要将私有录制内容提供给外部代理时,可使用会话详情页面的**复制给代理**控件。它会生成一个两小时有效期、仅限该录制的 `agent_access` 链接,在 `/sessions/:recordingId` 上以服务器端渲染方式生成一个小型发现载荷,并只暴露代理上下文/事件的 JSON API。该录制本身的可见性不会改变。
243
+
244
+ 仅在本地开发环境下,当私有 blob 存储不可用时,Analytics 可以回退为使用有上限的 SQL 内联数据块。生产部署应配置私有或加密的 blob 存储,而不是依赖 Postgres 来存放重放数据。
245
+
191
246
  ## 高级:自定义提供程序和内部结构 {#advanced}
192
247
 
193
248
  大多数应用程序只需要 `track()` / `identify()` 和内置提供程序。表面的其余部分 - 注册自定义提供程序、`TrackingProvider` 接口、批处理内部结构以及框架自己的浏览器遥测 - 如下。
@@ -188,6 +188,61 @@ track("checkout.completed", { total: 49.99, items: 3 });
188
188
 
189
189
  這與框架的內部瀏覽器遙測(`trackEvent()` /自動頁面瀏覽量 - 請參閱下面的 [Browser defaults](#browser-defaults))不同,後者為 Agent Native 自己的產品分析提供支援。將 `track()` 用於您的應用自己的分析事件,這些事件應到達您設定的提供者。
190
190
 
191
+ ## 工作階段重播 {#session-replay}
192
+
193
+ Agent Native 應用程式可以選擇加入第一方瀏覽器工作階段重播,而無需新增第二個分析 SDK。在瀏覽器根目錄中呼叫一次 `configureTracking()`,並傳入 Analytics 公開金鑰以及收集器端點:
194
+
195
+ ```ts
196
+ import { configureTracking } from "@agent-native/core/client";
197
+
198
+ configureTracking({
199
+ key: "anpk_...",
200
+ endpoint: "https://analytics.example.com/api/analytics/track",
201
+ sessionReplay: {
202
+ enabled: true,
203
+ sampleRate: 0.1,
204
+ },
205
+ getDefaultProps: (_event, props) => ({
206
+ ...props,
207
+ app: "my-app",
208
+ template: "my-template",
209
+ }),
210
+ });
211
+ ```
212
+
213
+ 當 `sessionReplay.enabled` 為真值時,用戶端會在啟動後動態匯入 `@rrweb/record`,並將重播區塊發佈到重播端點。如果 `endpoint` 以 `/api/analytics/track` 或 `/track` 結尾,重播端點會自動衍生為 `/api/analytics/replay`。當重播收集器位於其他位置時,請使用 `sessionReplay.endpoint` 明確覆寫它。
214
+
215
+ Agent Native 範本根目錄已經呼叫了 `configureTracking()`。託管範本部署可以透過 Vite/Netlify 環境變數開啟重播,而函式庫使用者則應優先使用上面明確的 `configureTracking({ key, endpoint, sessionReplay })` 形式:
216
+
217
+ ```bash
218
+ VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY=anpk_...
219
+ VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=https://analytics.example.com/api/analytics/track
220
+ VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=true
221
+ VITE_AGENT_NATIVE_SESSION_REPLAY_SAMPLE_RATE=0.1
222
+ ```
223
+
224
+ 瀏覽器輔助程式也會盡力執行、非阻塞式讀取目前的 Agent Native 驗證工作階段。重播預設僅限已登入使用者:當 `sessionReplay` 啟用時,除非工作階段解析出使用者電子郵件地址,否則不會開始錄製。已登入使用者的重播包含以電子郵件為基礎的 `userId`/`userEmail` 以及 `orgId`。
225
+
226
+ 僅在刻意進行匿名重播部署時,才設定 `sessionReplay.requireSignedInUser: false` 或 `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false`。停用驗證閘控後,匿名錄製仍可依匿名訪客、工作階段、應用程式/範本、主機名稱與路徑進行查詢。
227
+
228
+ 工作階段重播是依瀏覽器工作階段進行確定性取樣的。`sampleRate` 為 `0.1` 表示大約記錄 10% 符合條件的工作階段;當符合條件的母體刻意較小時(例如僅限已登入使用者的內部試用),可使用 `1`。
229
+
230
+ 隱私預設值刻意設計得保守,但仍足以支援播放:
231
+
232
+ - 輸入欄位預設會被遮罩(`maskAllInputs: true`)。
233
+ - 頁面文字保持可見,除非元素被標記為 `.an-mask` 或 `data-an-mask`。
234
+ - 敏感區域會透過選擇器(例如 `[data-sensitive]`、`.an-block`、`.an-private`、`data-an-block`、`data-an-private`,以及類似信用卡/密碼/社會安全號碼的欄位)進行封鎖。
235
+ - URL 會使用瀏覽器分析所用的同一個 `scrubUrl()` 輔助函式進行清理。
236
+ - 重播擷取僅限於網頁,且為選擇加入;它不會錄製原生桌面畫面。
237
+
238
+ 在錄製期間,工作階段重播還會擷取瀏覽器主控台輸出(`log`、`info`、`warn`、`error`、`debug`,以及視窗的 `error`/`unhandledrejection`)與網路請求元資料(`fetch` 與 XHR),並以帶標籤的 rrweb 自訂事件形式儲存,讓代理與重播檢視器能夠偵錯使用者回報的問題。啟用重播後,擷取功能預設為開啟;可透過 `sessionReplay.console` 與 `sessionReplay.network` 選項進行調整或停用,這兩個選項都接受布林值或選項物件。請求/回應主體與標頭永遠不會被擷取,URL 會被清理,訊息會被截斷,記錄器自身的擷取/追蹤流量會被排除,且每個工作階段的預算(1000 個主控台事件/2000 個網路事件)在超過時會加上截斷提示。
239
+
240
+ Analytics 範本將重播中繼資料儲存在 SQL(`session_recordings`)中,並透過私有 blob 參照(`session_replay_chunks`)儲存區塊。瀏覽器與代理永遠不會收到提供者 URL。播放透過範圍受限的伺服器路由進行,且預設代理工具傳回摘要或範圍受限的重播事件,而非原始區塊資料表存取權。
241
+
242
+ 當您需要將私人錄製內容交給外部代理時,請使用工作階段詳細資料頁面的**複製給代理**控制項。它會產生一個兩小時、僅限該筆錄製的 `agent_access` 連結,在 `/sessions/:recordingId` 上以伺服器端轉譯(SSR)一個小型探索酬載,並僅公開代理內容/事件 JSON API。該筆錄製的可見性不會改變。
243
+
244
+ 僅在本機開發時,Analytics 可以在私有 blob 儲存體無法使用時,回退到有上限的 SQL 內嵌區塊。正式環境部署應設定私有或加密的 blob 儲存體,而不是依賴 Postgres 來儲存重播酬載。
245
+
191
246
  ## 進階:自訂提供者和內部結構 {#advanced}
192
247
 
193
248
  大多數應用程式只需要 `track()` / `identify()` 和內建提供者。表面的其餘部分 - 註冊自訂提供者、`TrackingProvider` 介面、批處理內部結構以及框架自己的瀏覽器遙測 - 如下。
@@ -235,6 +235,8 @@ Privacy defaults are intentionally conservative but still useful for playback:
235
235
  - URLs are scrubbed with the same `scrubUrl()` helper used by browser analytics.
236
236
  - Replay capture is web-only and opt-in; it does not record native desktop screens.
237
237
 
238
+ While recording, session replay also captures browser console output (`log`, `info`, `warn`, `error`, `debug`, plus window `error` / `unhandledrejection`) and network request metadata (`fetch` and XHR) as tagged rrweb custom events, so agents and the replay viewer can debug user-reported issues. Capture is on by default when replay is enabled; tune or disable it with the `sessionReplay.console` and `sessionReplay.network` options, each accepting a boolean or an options object. Request/response bodies and headers are never captured, URLs are scrubbed, messages are truncated, the recorder's own ingest/tracking traffic is excluded, and per-session budgets (1000 console / 2000 network events) add a truncation notice when exceeded.
239
+
238
240
  The Analytics template stores replay metadata in SQL (`session_recordings`) and stores chunks through private blob refs (`session_replay_chunks`). Browsers and agents never receive provider URLs. Playback goes through scoped server routes and the default agent tools return summaries or bounded replay events, not raw chunk table access.
239
241
 
240
242
  When you need to hand a private recording to an external agent, use the session detail page's **Copy for agent** control. It mints a two-hour, recording-scoped `agent_access` link, SSRs a small discovery payload on `/sessions/:recordingId`, and exposes only the agent context/events JSON APIs. The recording's visibility does not change.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.67",
3
+ "version": "0.85.0",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -130,6 +130,7 @@ import {
130
130
  claimBackgroundRun,
131
131
  readBackgroundRunClaim,
132
132
  recordRunDiagnostic,
133
+ countRunsForTurn,
133
134
  RUN_DIAG_STAGE,
134
135
  UNCLAIMED_BACKGROUND_RUN_GRACE_MS,
135
136
  } from "./run-store.js";
@@ -5527,8 +5528,13 @@ export function createProductionAgentHandler(
5527
5528
  // Insert the run row up front so /runs/active sees it immediately and
5528
5529
  // the slot stays held while the background function cold-starts. Mark
5529
5530
  // it background-dispatched so the stale reaper uses the wider window.
5531
+ // The full request body is persisted ON the row (dispatch_payload) so
5532
+ // the self-POST below can carry only the tiny marker — Netlify caps
5533
+ // background-function request bodies at 256KB, and a large chat
5534
+ // history (inline attachments especially) silently exceeded that.
5530
5535
  await insertRun(runId, effectiveThreadId, effectiveTurnId, {
5531
5536
  dispatchMode: "background",
5537
+ dispatchPayload: JSON.stringify(body),
5532
5538
  });
5533
5539
  backgroundRowInserted = true;
5534
5540
  } catch (err) {
@@ -5560,16 +5566,31 @@ export function createProductionAgentHandler(
5560
5566
  // the Authorization Bearer HMAC is preserved either way.
5561
5567
  path: backgroundDispatchPath,
5562
5568
  taskId: runId,
5563
- body: {
5564
- ...body,
5565
- // Carry the pre-claimed identity so the worker reuses this run.
5566
- [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5567
- runId,
5568
- turnId: effectiveTurnId,
5569
- backgroundFunctionRuntimeExpected:
5570
- expectsNetlifyBackgroundFunction,
5571
- },
5572
- },
5569
+ // When the row (and its persisted payload) landed, send only the
5570
+ // marker — the worker rehydrates the body from dispatch_payload
5571
+ // (`payloadRef`), keeping the self-POST far under Netlify's 256KB
5572
+ // background-function body cap. If the insert failed we fall back to
5573
+ // carrying the full body inline, exactly as before.
5574
+ body: backgroundRowInserted
5575
+ ? {
5576
+ [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5577
+ runId,
5578
+ turnId: effectiveTurnId,
5579
+ backgroundFunctionRuntimeExpected:
5580
+ expectsNetlifyBackgroundFunction,
5581
+ payloadRef: true,
5582
+ },
5583
+ }
5584
+ : {
5585
+ ...body,
5586
+ // Carry the pre-claimed identity so the worker reuses this run.
5587
+ [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5588
+ runId,
5589
+ turnId: effectiveTurnId,
5590
+ backgroundFunctionRuntimeExpected:
5591
+ expectsNetlifyBackgroundFunction,
5592
+ },
5593
+ },
5573
5594
  });
5574
5595
  dispatched = true;
5575
5596
  } catch (err) {
@@ -5835,6 +5856,38 @@ export function createProductionAgentHandler(
5835
5856
  // foreground fallback, which is not a worker and rides the
5836
5857
  // connected client's auto_continue instead.)
5837
5858
  if (willChainBackgroundContinuation(run)) {
5859
+ // DURABLE PER-TURN LEDGER: bound the total number of runs one
5860
+ // logical turn may consume, counted in SQL. The in-marker
5861
+ // `continuationCount` resets whenever a fresh POST starts a new
5862
+ // chain for the same turn (client recovery, duplicate delivery),
5863
+ // so it cannot bound cross-chain loops — the SQL count survives
5864
+ // every recovery path and is what actually kills a pathological
5865
+ // turn (the "dozens of runs on one prompt" incident class).
5866
+ const turnRunCount = await countRunsForTurn(
5867
+ effectiveThreadId,
5868
+ effectiveTurnId,
5869
+ ).catch(() => null);
5870
+ if (
5871
+ turnRunCount !== null &&
5872
+ turnRunCount > MAX_BACKGROUND_RUN_CONTINUATIONS + 5
5873
+ ) {
5874
+ console.error(
5875
+ `[agent-chat] turn ${effectiveTurnId} consumed ${turnRunCount} runs — refusing to chain further`,
5876
+ runId,
5877
+ );
5878
+ const statusUpdated = await updateRunStatusIfRunning(
5879
+ runId,
5880
+ "errored",
5881
+ ).catch(() => false);
5882
+ if (statusUpdated) {
5883
+ await setRunTerminalReason(
5884
+ runId,
5885
+ "turn_continuation_budget_exhausted",
5886
+ ).catch(() => {});
5887
+ }
5888
+ return;
5889
+ }
5890
+
5838
5891
  // Mint the next chunk's runId here and sign the dispatch token
5839
5892
  // over it, so the `_process-run` route's HMAC check and the
5840
5893
  // worker's run identity agree. Fresh runId (not this chunk's) so
@@ -5851,41 +5904,149 @@ export function createProductionAgentHandler(
5851
5904
  dispatchPathTargetsNetlifyBackgroundFunction(
5852
5905
  continuationDispatchPath,
5853
5906
  );
5907
+ const continuationMarker = {
5908
+ runId: nextRunId,
5909
+ turnId: effectiveTurnId,
5910
+ continuationCount: backgroundContinuationCount + 1,
5911
+ continuationReason,
5912
+ ...(actionPreparationTool ? { actionPreparationTool } : {}),
5913
+ backgroundFunctionRuntimeExpected:
5914
+ continuationExpectsNetlifyBackgroundFunction,
5915
+ };
5916
+ // Strip this chunk's own marker before persisting/forwarding —
5917
+ // the next chunk gets the fresh marker above.
5918
+ const continuationBody: Record<string, unknown> = {
5919
+ ...(body as unknown as Record<string, unknown>),
5920
+ internalContinuation: true,
5921
+ };
5922
+ delete continuationBody[AGENT_CHAT_BACKGROUND_RUN_FIELD];
5854
5923
  try {
5855
5924
  await recordRunDiagnostic(
5856
5925
  run.runId,
5857
5926
  RUN_DIAG_STAGE.workerSetupStep,
5858
5927
  `chain_dispatch_start nextRunId=${nextRunId} reason=${continuationReason} path=${continuationDispatchPath}`,
5859
5928
  ).catch(() => {});
5860
- await fireInternalDispatch({
5861
- event,
5862
- // Continuation chunks use the same path resolution as the
5863
- // initial dispatch: on hosted Netlify the background
5864
- // function's DEFAULT url (no custom config.path; async via
5865
- // background:true; never shadowed because /.netlify/* is
5866
- // excluded from the /* catch-all) so each chunk keeps the
5867
- // 15-min budget; off-Netlify the in-process framework route.
5868
- path: continuationDispatchPath,
5869
- taskId: nextRunId,
5870
- body: {
5871
- ...body,
5872
- internalContinuation: true,
5873
- [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5874
- runId: nextRunId,
5875
- turnId: effectiveTurnId,
5876
- continuationCount: backgroundContinuationCount + 1,
5877
- continuationReason,
5878
- ...(actionPreparationTool
5879
- ? { actionPreparationTool }
5880
- : {}),
5881
- backgroundFunctionRuntimeExpected:
5882
- continuationExpectsNetlifyBackgroundFunction,
5929
+ // ── TRANSACTIONAL HANDOFF ──────────────────────────────────
5930
+ // 1. Insert the successor row (with its rehydration payload)
5931
+ // BEFORE firing the dispatch, so:
5932
+ // - /runs/active shows an active run continuously across
5933
+ // the chunk boundary (no idle gap for the client to
5934
+ // misread as "the turn ended"), and
5935
+ // - a lost dispatch leaves a row the unclaimed-run sweep
5936
+ // reaps into a LOUD error instead of a silent hang.
5937
+ // 2. Await the dispatch response fully (`awaitResponse`) —
5938
+ // this worker's Lambda is about to finish, and the old
5939
+ // 250ms settle race let a still-in-flight handoff fetch be
5940
+ // killed by the post-return freeze WITHOUT rejecting: the
5941
+ // turn just stopped, silently. A Netlify background
5942
+ // function 202s on enqueue (normally well under a second),
5943
+ // and this chunk has minutes of budget headroom left, so
5944
+ // awaiting is cheap. Retried with backoff for transient
5945
+ // network blips.
5946
+ let nextRowInserted = false;
5947
+ try {
5948
+ await insertRun(
5949
+ nextRunId,
5950
+ effectiveThreadId,
5951
+ effectiveTurnId,
5952
+ {
5953
+ dispatchMode: "background",
5954
+ dispatchPayload: JSON.stringify(continuationBody),
5883
5955
  },
5884
- },
5885
- settleMs: continuationExpectsNetlifyBackgroundFunction
5886
- ? BACKGROUND_CLAIM_GRACE_MS
5887
- : undefined,
5888
- });
5956
+ );
5957
+ nextRowInserted = true;
5958
+ } catch (insertErr) {
5959
+ await recordRunDiagnostic(
5960
+ run.runId,
5961
+ RUN_DIAG_STAGE.workerSetupStep,
5962
+ `chain_successor_insert_failed nextRunId=${nextRunId} ${
5963
+ insertErr instanceof Error
5964
+ ? insertErr.message
5965
+ : String(insertErr)
5966
+ }`,
5967
+ ).catch(() => {});
5968
+ console.error(
5969
+ "[agent-chat] continuation insertRun failed; dispatching with inline body:",
5970
+ insertErr instanceof Error
5971
+ ? insertErr.message
5972
+ : insertErr,
5973
+ );
5974
+ }
5975
+ const dispatchBody = nextRowInserted
5976
+ ? {
5977
+ internalContinuation: true,
5978
+ [AGENT_CHAT_BACKGROUND_RUN_FIELD]: {
5979
+ ...continuationMarker,
5980
+ payloadRef: true,
5981
+ },
5982
+ }
5983
+ : {
5984
+ ...continuationBody,
5985
+ [AGENT_CHAT_BACKGROUND_RUN_FIELD]: continuationMarker,
5986
+ };
5987
+ let dispatched = false;
5988
+ let lastDispatchErr: unknown;
5989
+ for (let attempt = 0; attempt < 3 && !dispatched; attempt++) {
5990
+ try {
5991
+ if (attempt > 0) {
5992
+ await new Promise<void>((resolve) =>
5993
+ setTimeout(resolve, 500 * 2 ** attempt),
5994
+ );
5995
+ // Keep the pre-inserted successor row visibly alive
5996
+ // while we retry: the awaited attempts + backoff can
5997
+ // outlast UNCLAIMED_BACKGROUND_RUN_GRACE_MS (25s), and
5998
+ // without a fresh heartbeat the unclaimed-run reaper /
5999
+ // sweep could error a handoff we are still delivering.
6000
+ if (nextRowInserted) {
6001
+ await updateRunHeartbeat(nextRunId).catch(() => {});
6002
+ }
6003
+ }
6004
+ await fireInternalDispatch({
6005
+ event,
6006
+ // Continuation chunks use the same path resolution as
6007
+ // the initial dispatch: on hosted Netlify the background
6008
+ // function's DEFAULT url (no custom config.path; async
6009
+ // via background:true; never shadowed because
6010
+ // /.netlify/* is excluded from the /* catch-all) so each
6011
+ // chunk keeps the 15-min budget; off-Netlify the
6012
+ // in-process framework route.
6013
+ path: continuationDispatchPath,
6014
+ taskId: nextRunId,
6015
+ body: dispatchBody,
6016
+ awaitResponse: true,
6017
+ responseTimeoutMs: 15_000,
6018
+ });
6019
+ dispatched = true;
6020
+ } catch (dispatchErr) {
6021
+ lastDispatchErr = dispatchErr;
6022
+ console.error(
6023
+ `[agent-chat] background continuation dispatch attempt ${attempt + 1} failed:`,
6024
+ dispatchErr instanceof Error
6025
+ ? dispatchErr.message
6026
+ : dispatchErr,
6027
+ );
6028
+ }
6029
+ }
6030
+ if (!dispatched) {
6031
+ // The pre-inserted successor row would otherwise sit
6032
+ // unclaimed until the sweep reaps it — error it now so the
6033
+ // failure is immediate and truthful.
6034
+ if (nextRowInserted) {
6035
+ const nextStatusUpdated = await updateRunStatusIfRunning(
6036
+ nextRunId,
6037
+ "errored",
6038
+ ).catch(() => false);
6039
+ if (nextStatusUpdated) {
6040
+ await setRunTerminalReason(
6041
+ nextRunId,
6042
+ "background_continuation_dispatch_failed",
6043
+ ).catch(() => {});
6044
+ }
6045
+ }
6046
+ throw lastDispatchErr instanceof Error
6047
+ ? lastDispatchErr
6048
+ : new Error(String(lastDispatchErr));
6049
+ }
5889
6050
  await recordRunDiagnostic(
5890
6051
  run.runId,
5891
6052
  RUN_DIAG_STAGE.workerSetupStep,
@@ -105,6 +105,29 @@ export const BACKGROUND_SOFT_TIMEOUT_CEILING_MS = 13 * 60_000; // 780_000
105
105
  export const DEFAULT_BACKGROUND_RUN_SOFT_TIMEOUT_MS =
106
106
  BACKGROUND_SOFT_TIMEOUT_CEILING_MS;
107
107
 
108
+ /**
109
+ * AUTHORITATIVE no-progress backstop for a run, enforced by the run manager
110
+ * itself (timer-driven, independent of any layer below).
111
+ *
112
+ * The finer-grained watchdogs inside the agent loop (model-stream and
113
+ * action-preparation no-progress, both 90s) only guard the model event stream
114
+ * — a stall in any segment OUTSIDE that guarded loop (engine-call
115
+ * establishment, worker setup between continuation chunks, a wedged transport
116
+ * that emits keepalives while the loop never runs) previously hung forever
117
+ * with the client watching keepalives. This backstop covers every segment by
118
+ * construction: if no REAL progress event (see `shouldBumpProgressForEvent`;
119
+ * keepalives and zero-byte prep activity don't count) lands for this long —
120
+ * and no tool call is in flight (tool execution legitimately emits nothing
121
+ * for minutes and has its own 12-min timeout) — the run manager emits
122
+ * `auto_continue { reason: "no_progress" }` and aborts the chunk, exactly
123
+ * like the soft timeout, so the normal continuation machinery recovers it.
124
+ *
125
+ * Sits above the 90s in-loop watchdogs (they get first chance to recover with
126
+ * better context) and far below the 13-min background budget. Only armed when
127
+ * a soft-timeout regime is active (hosted runs); local dev stays unbounded.
128
+ */
129
+ export const RUN_NO_PROGRESS_HARD_TIMEOUT_MS = 150_000;
130
+
108
131
  /** Default SQL retention for completed run event logs (24 hours). */
109
132
  export const DEFAULT_COMPLETED_RUN_RETENTION_MS = 24 * 60 * 60 * 1000;
110
133
 
@@ -220,6 +243,13 @@ export interface StartRunOptions {
220
243
  * decision in production-agent.ts.
221
244
  */
222
245
  backgroundFunction?: boolean;
246
+ /**
247
+ * Override the run-manager-level no-progress backstop
248
+ * (`RUN_NO_PROGRESS_HARD_TIMEOUT_MS`). `0` disables it. Defaults to the
249
+ * backstop constant whenever a soft-timeout regime is active (hosted runs)
250
+ * and to disabled otherwise (local dev stays unbounded).
251
+ */
252
+ noProgressTimeoutMs?: number;
223
253
  }
224
254
 
225
255
  export interface ResolveRunSoftTimeoutOptions {
@@ -499,6 +529,46 @@ export function startRun(
499
529
  return true;
500
530
  };
501
531
 
532
+ // ── No-progress backstop (see RUN_NO_PROGRESS_HARD_TIMEOUT_MS) ──────────
533
+ // Timer-driven and independent of the agent loop, so it fires even when the
534
+ // stall is in a segment the in-loop watchdogs never see (engine-call
535
+ // establishment, setup, a wedged transport emitting keepalives). Tool calls
536
+ // and sub-agent calls in flight suspend it — tool execution legitimately
537
+ // emits nothing for minutes and carries its own 12-min timeout.
538
+ let lastRealProgressAt = Date.now();
539
+ let inFlightWorkCount = 0;
540
+ const trackInFlightWork = (event: AgentChatEvent) => {
541
+ if (event.type === "tool_start") {
542
+ inFlightWorkCount += 1;
543
+ } else if (event.type === "tool_done") {
544
+ inFlightWorkCount = Math.max(0, inFlightWorkCount - 1);
545
+ } else if (event.type === "agent_call") {
546
+ if (event.status === "start") {
547
+ inFlightWorkCount += 1;
548
+ } else {
549
+ inFlightWorkCount = Math.max(0, inFlightWorkCount - 1);
550
+ }
551
+ }
552
+ };
553
+ const checkNoProgressBackstop = () => {
554
+ if (noProgressTimeoutMs <= 0) return;
555
+ if (run.status !== "running" || abort.signal.aborted) return;
556
+ if (inFlightWorkCount > 0) return;
557
+ if (Date.now() - lastRealProgressAt < noProgressTimeoutMs) return;
558
+ console.error(
559
+ `[run-manager] no real progress for ${noProgressTimeoutMs}ms with no tool in flight — ` +
560
+ `checkpointing run for continuation`,
561
+ runId,
562
+ );
563
+ // Mirror the soft-timeout semantics exactly: the chunk completes (not
564
+ // aborts) at an auto_continue boundary, so the continuation machinery —
565
+ // server-chained for background workers, client-driven for foreground —
566
+ // recovers the turn.
567
+ softTimedOut = true;
568
+ send({ type: "auto_continue", reason: "no_progress" });
569
+ abort.abort("no_progress");
570
+ };
571
+
502
572
  // Periodic SQL abort check interval (for cross-isolate abort on Workers).
503
573
  // Also self-aborts when our row is no longer status='running' — catches the
504
574
  // false-stale-reap zombie scenario where the reaper flipped the row while
@@ -534,11 +604,18 @@ export function startRun(
534
604
  const heartbeatTimer: ReturnType<typeof setInterval> = setInterval(() => {
535
605
  updateRunHeartbeat(runId).catch(() => {});
536
606
  checkSqlAbort();
607
+ checkNoProgressBackstop();
537
608
  }, 1500);
538
609
  const softTimeoutMs = resolveRunSoftTimeoutMs(options?.softTimeoutMs, {
539
610
  useHostedDefault: options?.useHostedSoftTimeoutDefault === true,
540
611
  backgroundFunction: options?.backgroundFunction === true,
541
612
  });
613
+ // Armed only when a soft-timeout regime is active (hosted): local dev keeps
614
+ // unbounded runs. For 40s foreground chunks the soft timeout always fires
615
+ // first, so in practice this guards the long background chunks.
616
+ const noProgressTimeoutMs =
617
+ options?.noProgressTimeoutMs ??
618
+ (softTimeoutMs > 0 ? RUN_NO_PROGRESS_HARD_TIMEOUT_MS : 0);
542
619
  const softTimeoutTimer =
543
620
  softTimeoutMs > 0
544
621
  ? setTimeout(() => {
@@ -610,7 +687,9 @@ export function startRun(
610
687
  // a hung run from a healthy one. Keepalive and zero-byte action prep are
611
688
  // liveness only; streamed input bytes, text, and tool lifecycle events are
612
689
  // real progress.
690
+ trackInFlightWork(runEvent.event);
613
691
  if (shouldBumpProgressForEvent(runEvent.event)) {
692
+ lastRealProgressAt = Date.now();
614
693
  bumpProgressIfDue();
615
694
  }
616
695