@agent-native/core 0.146.8 → 0.148.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 (317) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +115 -68
  3. package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +18 -6
  4. package/corpus/templates/analytics/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  5. package/corpus/templates/analytics/DEVELOPING.md +50 -3
  6. package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +190 -0
  7. package/corpus/templates/analytics/app/i18n/index.ts +14 -29
  8. package/corpus/templates/analytics/app/i18n-data.ts +10 -10
  9. package/corpus/templates/analytics/app/pages/Settings.tsx +5 -21
  10. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -3
  11. package/corpus/templates/analytics/app/routes/status.$slug.tsx +4 -2
  12. package/corpus/templates/analytics/scripts/first-party-analytics-bigquery-backfill.ts +253 -24
  13. package/corpus/templates/analytics/server/db/schema.ts +23 -0
  14. package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +3 -1
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +1 -1
  16. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +113 -13
  17. package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +140 -0
  18. package/corpus/templates/analytics/server/lib/first-party-analytics-volume.ts +207 -0
  19. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +40 -9
  20. package/corpus/templates/analytics/server/lib/gcloud.ts +148 -20
  21. package/corpus/templates/analytics/server/plugins/db.ts +42 -0
  22. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +115 -68
  23. package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +18 -6
  24. package/corpus/templates/assets/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  25. package/corpus/templates/assets/app/i18n/index.ts +14 -29
  26. package/corpus/templates/brain/.agents/skills/adding-a-feature/SKILL.md +9 -0
  27. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +115 -68
  28. package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +18 -6
  29. package/corpus/templates/brain/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  30. package/corpus/templates/brain/app/i18n/index.ts +14 -29
  31. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +115 -68
  32. package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +18 -6
  33. package/corpus/templates/calendar/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  34. package/corpus/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
  35. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
  36. package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
  37. package/corpus/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  38. package/corpus/templates/chat/app/i18n/index.ts +14 -29
  39. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +115 -68
  40. package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +18 -6
  41. package/corpus/templates/clips/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  42. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -7
  43. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +18 -13
  44. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +87 -370
  45. package/corpus/templates/clips/app/components/settings/slack-section.tsx +59 -101
  46. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +136 -213
  47. package/corpus/templates/clips/app/i18n/en-US.ts +11 -8
  48. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +16 -4
  49. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +14 -5
  50. package/corpus/templates/clips/desktop/src/app.tsx +1 -1
  51. package/corpus/templates/clips/package.json +1 -0
  52. package/corpus/templates/clips/react-router.config.ts +7 -0
  53. package/corpus/templates/clips/server/plugins/auth.ts +6 -1
  54. package/corpus/templates/clips/shared/prerendered-public-paths.ts +21 -0
  55. package/corpus/templates/clips/shared/share-meta.ts +8 -3
  56. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +115 -68
  57. package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +18 -6
  58. package/corpus/templates/content/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  59. package/corpus/templates/content/app/i18n/index.ts +14 -29
  60. package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +18 -6
  61. package/corpus/templates/crm/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  62. package/corpus/templates/crm/app/i18n/index.ts +14 -29
  63. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +115 -68
  64. package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +18 -6
  65. package/corpus/templates/design/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  66. package/corpus/templates/design/actions/analyze-brand-assets.ts +15 -15
  67. package/corpus/templates/design/actions/import-code.ts +2 -43
  68. package/corpus/templates/design/app/i18n/index.ts +14 -29
  69. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +115 -68
  70. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +18 -6
  71. package/corpus/templates/dispatch/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  72. package/corpus/templates/dispatch/app/chat-first-pages.ts +747 -0
  73. package/corpus/templates/dispatch/app/i18n-data.ts +182 -1
  74. package/corpus/templates/dispatch/app/routes/settings.tsx +62 -1
  75. package/corpus/templates/dispatch/changelog/2026-08-08-chat-first-navigation-keeps-conversations-central-while-open.md +6 -0
  76. package/corpus/templates/factory/.agents/skills/adding-a-feature/SKILL.md +9 -0
  77. package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +115 -68
  78. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +18 -6
  79. package/corpus/templates/factory/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  80. package/corpus/templates/factory/AGENTS.md +40 -49
  81. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +66 -0
  82. package/corpus/templates/factory/actions/evaluate-triage-item.ts +37 -0
  83. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -0
  84. package/corpus/templates/factory/actions/get-triage-item.ts +25 -4
  85. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +55 -0
  86. package/corpus/templates/factory/actions/ingest-github-observation.ts +21 -0
  87. package/corpus/templates/factory/actions/list-factory-audit.ts +118 -0
  88. package/corpus/templates/factory/actions/list-factory-automations.ts +2 -1
  89. package/corpus/templates/factory/actions/list-triage-items.ts +24 -2
  90. package/corpus/templates/factory/actions/poll-github-sources.ts +60 -0
  91. package/corpus/templates/factory/actions/poll-sentry-errors.ts +39 -0
  92. package/corpus/templates/factory/actions/poll-slack-channel.ts +45 -1
  93. package/corpus/templates/factory/actions/run-factory-automation.ts +1 -1
  94. package/corpus/templates/factory/actions/save-factory-automation.ts +2 -2
  95. package/corpus/templates/factory/actions/start-builder-for-item.ts +51 -0
  96. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +685 -0
  97. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +6 -0
  98. package/corpus/templates/factory/app/i18n/en-US.ts +25 -0
  99. package/corpus/templates/factory/app/routes/factory.tsx +203 -118
  100. package/corpus/templates/factory/changelog/2026-08-08-scheduler-health-moves-to-settings.md +6 -0
  101. package/corpus/templates/factory/server/db/schema.ts +37 -0
  102. package/corpus/templates/factory/server/lib/require-factory-automation.ts +5 -1
  103. package/corpus/templates/factory/server/plugins/factory-migrations.ts +29 -0
  104. package/corpus/templates/factory/server/triage/audit.ts +79 -0
  105. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +115 -68
  106. package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +18 -6
  107. package/corpus/templates/forms/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  108. package/corpus/templates/forms/app/i18n/index.ts +14 -29
  109. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +115 -68
  110. package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +18 -6
  111. package/corpus/templates/macros/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  112. package/corpus/templates/macros/app/i18n/index.ts +14 -29
  113. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +115 -68
  114. package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +18 -6
  115. package/corpus/templates/mail/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  116. package/corpus/templates/mail/app/i18n/index.ts +14 -29
  117. package/corpus/templates/plan/.agents/skills/adding-a-feature/SKILL.md +9 -0
  118. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +115 -68
  119. package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +18 -6
  120. package/corpus/templates/plan/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  121. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +7 -7
  122. package/corpus/templates/plan/app/i18n/index.ts +14 -29
  123. package/corpus/templates/plan/app/lib/plan-document-title.ts +3 -1
  124. package/corpus/templates/plan/app/root.tsx +1 -0
  125. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +115 -68
  126. package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +18 -6
  127. package/corpus/templates/slides/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  128. package/corpus/templates/slides/actions/analyze-brand-assets.ts +15 -15
  129. package/corpus/templates/slides/actions/import-code.ts +2 -43
  130. package/corpus/templates/slides/app/i18n/index.ts +14 -29
  131. package/corpus/templates/slides/server/routes/api/preview-images.get.ts +4 -1
  132. package/corpus/templates/tasks/.agents/skills/adding-a-feature/SKILL.md +9 -0
  133. package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +115 -68
  134. package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +18 -6
  135. package/corpus/templates/tasks/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  136. package/corpus/templates/tasks/app/components/layout/HeaderActions.tsx +1 -84
  137. package/corpus/templates/tasks/app/hooks/use-mobile.tsx +1 -21
  138. package/corpus/templates/tasks/app/i18n/index.ts +14 -29
  139. package/dist/agent/engine/ai-sdk-engine.js +2 -1
  140. package/dist/agent/engine/builder-engine.d.ts +11 -1
  141. package/dist/agent/engine/builder-engine.js +11 -4
  142. package/dist/agent/engine/openai-compatible-endpoint.d.ts +2 -0
  143. package/dist/agent/engine/openai-compatible-endpoint.js +5 -1
  144. package/dist/agent/engine/provider-endpoint-validation.d.ts +9 -0
  145. package/dist/agent/engine/provider-endpoint-validation.js +26 -0
  146. package/dist/agent/engine/provider-env-vars.d.ts +1 -1
  147. package/dist/agent/engine/provider-env-vars.js +1 -1
  148. package/dist/agent/engine/registry.d.ts +8 -6
  149. package/dist/agent/engine/registry.js +37 -24
  150. package/dist/agent/tool-search.d.ts +1 -0
  151. package/dist/agent/tool-search.js +17 -1
  152. package/dist/automations/service.js +4 -10
  153. package/dist/brand-kit/brand-signals.d.ts +13 -0
  154. package/dist/brand-kit/brand-signals.js +15 -0
  155. package/dist/brand-kit/index.d.ts +2 -2
  156. package/dist/brand-kit/index.js +1 -1
  157. package/dist/chat-threads/store.d.ts +2 -0
  158. package/dist/chat-threads/store.js +1 -1
  159. package/dist/cli/agent-plugin.d.ts +5 -2
  160. package/dist/cli/agent-plugin.js +17 -9
  161. package/dist/cli/code-agent-executor.js +25 -5
  162. package/dist/cli/code-agent-mcp-config.d.ts +21 -0
  163. package/dist/cli/code-agent-mcp-config.js +103 -0
  164. package/dist/cli/plan-blocks.d.ts +3 -18
  165. package/dist/cli/plan-blocks.js +2 -95
  166. package/dist/cli/plan-publish-store.d.ts +2 -61
  167. package/dist/cli/plan-publish-store.js +2 -127
  168. package/dist/cli/skills-content/connection.d.ts +1 -1
  169. package/dist/cli/skills-content/connection.js +1 -52
  170. package/dist/cli/skills-content/visual-recap-skill.d.ts +1 -1
  171. package/dist/cli/skills-content/visual-recap-skill.js +1 -553
  172. package/dist/client/NewWorkspaceAppFlow.js +4 -1
  173. package/dist/client/agent-chat/index.d.ts +4 -1
  174. package/dist/client/agent-chat/index.js +4 -1
  175. package/dist/client/agent-engine-key.d.ts +14 -4
  176. package/dist/client/agent-engine-key.js +51 -5
  177. package/dist/client/agent-page/AgentTabsPage.d.ts +1 -0
  178. package/dist/client/agent-page/AgentTabsPage.js +4 -3
  179. package/dist/client/agent-provider-catalog.d.ts +20 -0
  180. package/dist/client/agent-provider-catalog.js +100 -0
  181. package/dist/client/app-providers.d.ts +3 -1
  182. package/dist/client/app-providers.js +50 -5
  183. package/dist/client/blocks/index.d.ts +1 -0
  184. package/dist/client/blocks/index.js +1 -0
  185. package/dist/client/chat/run-recovery.js +4 -48
  186. package/dist/client/chat/runtime.js +63 -0
  187. package/dist/client/chat-first/agents-pane.d.ts +2 -0
  188. package/dist/client/chat-first/agents-pane.js +6 -0
  189. package/dist/client/chat-first/app-pane.d.ts +2 -0
  190. package/dist/client/chat-first/app-pane.js +6 -0
  191. package/dist/client/chat-first/apps-rail.d.ts +2 -0
  192. package/dist/client/chat-first/apps-rail.js +103 -0
  193. package/dist/client/chat-first/browser-pane.d.ts +2 -0
  194. package/dist/client/chat-first/browser-pane.js +64 -0
  195. package/dist/client/chat-first/chat-history.d.ts +12 -0
  196. package/dist/client/chat-first/chat-history.js +10 -0
  197. package/dist/client/chat-first/copy.d.ts +2 -0
  198. package/dist/client/chat-first/copy.js +117 -0
  199. package/dist/client/chat-first/index.d.ts +13 -0
  200. package/dist/client/chat-first/index.js +12 -0
  201. package/dist/client/chat-first/primary-nav.d.ts +8 -0
  202. package/dist/client/chat-first/primary-nav.js +6 -0
  203. package/dist/client/chat-first/session-watch-pane.d.ts +2 -0
  204. package/dist/client/chat-first/session-watch-pane.js +9 -0
  205. package/dist/client/chat-first/surface-panel.d.ts +9 -0
  206. package/dist/client/chat-first/surface-panel.js +9 -0
  207. package/dist/client/chat-first/surface-tabs.d.ts +9 -0
  208. package/dist/client/chat-first/surface-tabs.js +74 -0
  209. package/dist/client/chat-first/types.d.ts +74 -0
  210. package/dist/client/chat-first/types.js +1 -0
  211. package/dist/client/chat-first-agent-activity.d.ts +11 -0
  212. package/dist/client/chat-first-agent-activity.js +51 -0
  213. package/dist/client/chat-first-surface-panel-toggle.d.ts +5 -0
  214. package/dist/client/chat-first-surface-panel-toggle.js +6 -0
  215. package/dist/client/chat-first.d.ts +208 -0
  216. package/dist/client/chat-first.js +817 -0
  217. package/dist/client/extensions/ExtensionViewer.js +8 -1
  218. package/dist/client/i18n.d.ts +14 -0
  219. package/dist/client/i18n.js +18 -0
  220. package/dist/client/resources/McpIntegrationDialog.js +3 -2
  221. package/dist/client/resources/McpServerDetail.js +6 -11
  222. package/dist/client/resources/index.d.ts +1 -0
  223. package/dist/client/resources/index.js +1 -0
  224. package/dist/client/resources/use-mcp-servers.d.ts +20 -0
  225. package/dist/client/resources/use-mcp-servers.js +82 -46
  226. package/dist/client/settings/ProviderSetupForm.d.ts +21 -0
  227. package/dist/client/settings/ProviderSetupForm.js +145 -0
  228. package/dist/client/settings/SettingsPanel.js +52 -49
  229. package/dist/client/settings/index.d.ts +3 -0
  230. package/dist/client/settings/index.js +3 -0
  231. package/dist/coding-tools/run-code.d.ts +5 -0
  232. package/dist/coding-tools/run-code.js +196 -36
  233. package/dist/coding-tools/tool-orchestration.d.ts +10 -0
  234. package/dist/coding-tools/tool-orchestration.js +107 -0
  235. package/dist/collab/routes.d.ts +1 -1
  236. package/dist/collab/struct-routes.d.ts +1 -1
  237. package/dist/jobs/background-automation-runner.js +19 -2
  238. package/dist/jobs/frontmatter.d.ts +9 -0
  239. package/dist/jobs/frontmatter.js +16 -0
  240. package/dist/jobs/scheduler-health.d.ts +20 -0
  241. package/dist/jobs/scheduler-health.js +86 -1
  242. package/dist/jobs/scheduler.js +46 -9
  243. package/dist/jobs/tools.d.ts +1 -1
  244. package/dist/jobs/tools.js +2 -7
  245. package/dist/localization/default-messages.d.ts +20 -0
  246. package/dist/localization/default-messages.js +25 -5
  247. package/dist/mcp-client/routes.d.ts +3 -0
  248. package/dist/mcp-client/routes.js +14 -0
  249. package/dist/notifications/routes.d.ts +1 -1
  250. package/dist/observability/routes.d.ts +3 -3
  251. package/dist/onboarding/app-profile.js +1 -1
  252. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  253. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  254. package/dist/scripts/agent-engines/test-agent-engine.js +34 -10
  255. package/dist/secrets/routes.d.ts +6 -6
  256. package/dist/server/agent-chat/run-code-tools.d.ts +1 -1
  257. package/dist/server/agent-chat/run-code-tools.js +3 -1
  258. package/dist/server/agent-chat-plugin.js +6 -6
  259. package/dist/server/agent-engine-api-key-route.js +43 -12
  260. package/dist/server/agents-bundle.d.ts +2 -0
  261. package/dist/server/agents-bundle.js +32 -2
  262. package/dist/server/auth.js +21 -5
  263. package/dist/server/credential-provider.d.ts +8 -2
  264. package/dist/server/credential-provider.js +14 -9
  265. package/dist/server/design-token-utils.d.ts +18 -0
  266. package/dist/server/design-token-utils.js +31 -0
  267. package/dist/server/onboarding-html.js +22 -2
  268. package/dist/server/transcribe-voice.d.ts +1 -1
  269. package/dist/shared/chat-first-app-creation.d.ts +19 -0
  270. package/dist/shared/chat-first-app-creation.js +75 -0
  271. package/dist/shared/document-title.d.ts +7 -0
  272. package/dist/shared/document-title.js +29 -0
  273. package/dist/shared/index.d.ts +2 -0
  274. package/dist/shared/index.js +2 -0
  275. package/dist/styles/chat-history-list.css +16 -0
  276. package/dist/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
  277. package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
  278. package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
  279. package/dist/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  280. package/dist/templates/chat/app/i18n/index.ts +14 -29
  281. package/dist/templates/default/.agents/skills/adding-a-feature/SKILL.md +9 -0
  282. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +115 -68
  283. package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +18 -6
  284. package/dist/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  285. package/dist/templates/default/app/i18n/index.ts +14 -29
  286. package/dist/templates/default/react-router.config.ts +18 -0
  287. package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +18 -6
  288. package/dist/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  289. package/dist/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +9 -0
  290. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +22 -9
  291. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +115 -68
  292. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +18 -6
  293. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  294. package/dist/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +7 -0
  295. package/dist/triggers/dispatcher.js +1 -8
  296. package/docs/content/environment-variables.mdx +39 -38
  297. package/package.json +5 -4
  298. package/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
  299. package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
  300. package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
  301. package/src/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  302. package/src/templates/chat/app/i18n/index.ts +14 -29
  303. package/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +9 -0
  304. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +115 -68
  305. package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +18 -6
  306. package/src/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  307. package/src/templates/default/app/i18n/index.ts +14 -29
  308. package/src/templates/default/react-router.config.ts +18 -0
  309. package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +18 -6
  310. package/src/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  311. package/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +9 -0
  312. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +22 -9
  313. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +115 -68
  314. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +18 -6
  315. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  316. package/src/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +7 -0
  317. package/corpus/templates/plan/app/components/plan/wireframe/sanitize-html.ts +0 -343
@@ -1314,7 +1314,7 @@ const enUS = {
1314
1314
  storageSetupDescription:
1315
1315
  "Session replay recordings need storage before chunks can be saved. Connect Builder.io for free-tier storage, or configure your own S3-compatible bucket.",
1316
1316
  storageConnected: "Storage connected",
1317
- connectBuilder: "Use Builder.io (free)",
1317
+ connectBuilder: "Use Builder.io",
1318
1318
  configureS3: "Configure S3 storage",
1319
1319
  devtools: "Dev Tools",
1320
1320
  devtoolsResize: "Resize Dev Tools",
@@ -10157,7 +10157,7 @@ const translatedSessionReplayTranslations = {
10157
10157
  storageSetupDescription:
10158
10158
  "保存会话回放录制片段前需要先配置存储。可连接 Builder.io 使用免费层存储,或配置您自己的 S3 兼容存储桶。",
10159
10159
  storageConnected: "存储已连接",
10160
- connectBuilder: "使用 Builder.io(免费)",
10160
+ connectBuilder: "使用 Builder.io",
10161
10161
  configureS3: "配置 S3 存储",
10162
10162
  title: "会话",
10163
10163
  description:
@@ -10279,7 +10279,7 @@ const translatedSessionReplayTranslations = {
10279
10279
  storageSetupDescription:
10280
10280
  "Las grabaciones de repetición de sesión necesitan almacenamiento antes de poder guardar los fragmentos. Conecta Builder.io para usar almacenamiento de nivel gratuito o configura tu propio bucket compatible con S3.",
10281
10281
  storageConnected: "Almacenamiento conectado",
10282
- connectBuilder: "Usar Builder.io (gratis)",
10282
+ connectBuilder: "Usar Builder.io",
10283
10283
  configureS3: "Configurar almacenamiento S3",
10284
10284
  title: "Sesiones",
10285
10285
  description:
@@ -10405,7 +10405,7 @@ const translatedSessionReplayTranslations = {
10405
10405
  storageSetupDescription:
10406
10406
  "Les enregistrements de relecture de session nécessitent un espace de stockage avant de pouvoir sauvegarder les fragments. Connectez Builder.io pour un stockage en formule gratuite, ou configurez votre propre bucket compatible S3.",
10407
10407
  storageConnected: "Stockage connecté",
10408
- connectBuilder: "Utiliser Builder.io (gratuit)",
10408
+ connectBuilder: "Utiliser Builder.io",
10409
10409
  configureS3: "Configurer le stockage S3",
10410
10410
  title: "Sessions",
10411
10411
  description:
@@ -10534,7 +10534,7 @@ const translatedSessionReplayTranslations = {
10534
10534
  storageSetupDescription:
10535
10535
  "Aufzeichnungen der Sitzungswiedergabe benötigen einen Speicher, bevor Chunks gesichert werden können. Verbinden Sie Builder.io für Speicher im kostenlosen Kontingent oder konfigurieren Sie Ihren eigenen S3-kompatiblen Bucket.",
10536
10536
  storageConnected: "Speicher verbunden",
10537
- connectBuilder: "Builder.io verwenden (kostenlos)",
10537
+ connectBuilder: "Builder.io verwenden",
10538
10538
  configureS3: "S3-Speicher konfigurieren",
10539
10539
  title: "Sitzungen",
10540
10540
  description:
@@ -10659,7 +10659,7 @@ const translatedSessionReplayTranslations = {
10659
10659
  storageSetupDescription:
10660
10660
  "セッションリプレイの録画を保存するには、まずストレージが必要です。Builder.io の無料枠ストレージを接続するか、独自の S3 互換バケットを設定してください。",
10661
10661
  storageConnected: "ストレージ接続済み",
10662
- connectBuilder: "Builder.io を使う(無料)",
10662
+ connectBuilder: "Builder.io を使う",
10663
10663
  configureS3: "S3 ストレージを設定",
10664
10664
  title: "セッション",
10665
10665
  description:
@@ -10785,7 +10785,7 @@ const translatedSessionReplayTranslations = {
10785
10785
  storageSetupDescription:
10786
10786
  "세션 재생 녹화를 저장하려면 먼저 저장소가 필요합니다. 무료 등급 저장소를 사용하려면 Builder.io를 연결하거나, 직접 S3 호환 버킷을 구성하세요.",
10787
10787
  storageConnected: "저장소 연결됨",
10788
- connectBuilder: "Builder.io 사용 (무료)",
10788
+ connectBuilder: "Builder.io 사용",
10789
10789
  configureS3: "S3 저장소 구성",
10790
10790
  title: "세션",
10791
10791
  description:
@@ -10910,7 +10910,7 @@ const translatedSessionReplayTranslations = {
10910
10910
  storageSetupDescription:
10911
10911
  "As gravações de replay de sessão precisam de armazenamento antes que os fragmentos possam ser salvos. Conecte o Builder.io para armazenamento no plano gratuito ou configure seu próprio bucket compatível com S3.",
10912
10912
  storageConnected: "Armazenamento conectado",
10913
- connectBuilder: "Usar Builder.io (gratuito)",
10913
+ connectBuilder: "Usar Builder.io",
10914
10914
  configureS3: "Configurar armazenamento S3",
10915
10915
  title: "Sessões",
10916
10916
  description:
@@ -11034,7 +11034,7 @@ const translatedSessionReplayTranslations = {
11034
11034
  storageSetupDescription:
11035
11035
  "चंक्स सेव करने से पहले सेशन रीप्ले रिकॉर्डिंग के लिए स्टोरेज ज़रूरी है। फ्री-टियर स्टोरेज के लिए Builder.io कनेक्ट करें, या अपना खुद का S3-संगत बकेट कॉन्फ़िगर करें।",
11036
11036
  storageConnected: "स्टोरेज कनेक्ट हो गया",
11037
- connectBuilder: "Builder.io इस्तेमाल करें (मुफ़्त)",
11037
+ connectBuilder: "Builder.io इस्तेमाल करें",
11038
11038
  configureS3: "S3 स्टोरेज कॉन्फ़िगर करें",
11039
11039
  title: "सत्र",
11040
11040
  description:
@@ -11155,7 +11155,7 @@ const translatedSessionReplayTranslations = {
11155
11155
  storageSetupDescription:
11156
11156
  "تحتاج تسجيلات إعادة عرض الجلسات إلى مساحة تخزين قبل أن يتسنى حفظ الأجزاء. اربط Builder.io للحصول على تخزين بالباقة المجانية، أو هيّئ حاوية متوافقة مع S3 خاصة بك.",
11157
11157
  storageConnected: "تم ربط مساحة التخزين",
11158
- connectBuilder: "استخدام Builder.io (مجاناً)",
11158
+ connectBuilder: "استخدام Builder.io",
11159
11159
  configureS3: "تهيئة تخزين S3",
11160
11160
  title: "الجلسات",
11161
11161
  description:
@@ -20,13 +20,6 @@ import { Link } from "react-router";
20
20
  import { toast } from "sonner";
21
21
 
22
22
  import { Button } from "@/components/ui/button";
23
- import {
24
- Card,
25
- CardContent,
26
- CardDescription,
27
- CardHeader,
28
- CardTitle,
29
- } from "@/components/ui/card";
30
23
  import { Label } from "@/components/ui/label";
31
24
  import { Switch } from "@/components/ui/switch";
32
25
 
@@ -218,22 +211,13 @@ export default function Settings() {
218
211
  </SettingsGroup>
219
212
 
220
213
  {replayStorageStatus.data?.configured ? (
221
- <Card
214
+ <SettingsGroup
222
215
  id="replay-storage"
223
- className="bg-card border-border/50 scroll-mt-16"
216
+ title={t("sessions.storageSetupTitle")}
217
+ description={t("sessions.storageSetupDescription")}
224
218
  >
225
- <CardHeader>
226
- <CardTitle className="text-base">
227
- {t("sessions.storageSetupTitle")}
228
- </CardTitle>
229
- <CardDescription>
230
- {t("sessions.storageSetupDescription")}
231
- </CardDescription>
232
- </CardHeader>
233
- <CardContent>
234
- <ReplayStorageHint embedded />
235
- </CardContent>
236
- </Card>
219
+ <ReplayStorageHint embedded />
220
+ </SettingsGroup>
237
221
  ) : null}
238
222
  </div>
239
223
  }
@@ -621,9 +621,7 @@ export function ReplayStorageHint({
621
621
  <IconLoader2 className="h-4 w-4 animate-spin" />
622
622
  ) : builderConnected ? (
623
623
  <IconCheck className="h-4 w-4" />
624
- ) : (
625
- <IconExternalLink className="h-4 w-4" />
626
- )}
624
+ ) : null}
627
625
  {builderConnected
628
626
  ? t("sessions.storageConnected")
629
627
  : t("sessions.connectBuilder")}
@@ -1,4 +1,5 @@
1
1
  import { useActionQuery } from "@agent-native/core/client/hooks";
2
+ import { normalizeDocumentTitle } from "@agent-native/core/shared";
2
3
  import { IconActivity } from "@tabler/icons-react";
3
4
  import { useMemo } from "react";
4
5
  import { data, useLoaderData, useParams } from "react-router";
@@ -43,15 +44,16 @@ export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
43
44
  { name: "robots", content: "noindex" },
44
45
  ];
45
46
  }
47
+ const pageTitle = normalizeDocumentTitle(page.title, "Status page");
46
48
  const description =
47
49
  page.description ||
48
50
  (page.overall === "operational"
49
51
  ? "All systems operational."
50
52
  : "Current service status.");
51
53
  return [
52
- { title: `${page.title} · Status` },
54
+ { title: `${pageTitle} · Status` },
53
55
  { name: "description", content: description },
54
- { property: "og:title", content: `${page.title} · Status` },
56
+ { property: "og:title", content: `${pageTitle} · Status` },
55
57
  { property: "og:description", content: description },
56
58
  { property: "og:type", content: "website" },
57
59
  ];
@@ -36,7 +36,13 @@ const MAX_CONCURRENCY = 4;
36
36
  const DEFAULT_MAX_BATCHES = 1_000;
37
37
  const MAX_MAX_BATCHES = 10_000;
38
38
  const DEFAULT_CHECKPOINT_PATH = ".analytics-first-party-bigquery-backfill.json";
39
+ const DEFAULT_LOOKBACK_DAYS = 60;
40
+ const MIN_LOOKBACK_DAYS = 30;
41
+ const MAX_LOOKBACK_DAYS = 60;
42
+ const DEFAULT_EXCLUDED_EVENT_NAMES = ["http.response"];
43
+ const MAX_EXCLUDED_EVENT_NAMES = 32;
39
44
  const QUERY_TIMEOUT_MS = 30_000;
45
+ const MAX_HYDRATION_IDS = 500;
40
46
  const JOB_TABLE = "analytics_bigquery_backfill_jobs";
41
47
  const FINALIZE_ALLOW_ENV =
42
48
  "AGENT_NATIVE_ANALYTICS_BIGQUERY_BACKFILL_FINALIZE_ALLOW";
@@ -58,13 +64,16 @@ export interface BranchCheckpoint {
58
64
  }
59
65
 
60
66
  export interface DedicatedBackfillCheckpoint {
61
- version: 1;
67
+ version: 1 | 2;
62
68
  scope: {
63
69
  userEmail: string;
64
70
  orgId: string;
65
71
  };
66
72
  table: string | null;
67
73
  branches: Record<BranchName, BranchCheckpoint>;
74
+ lookbackDays?: number;
75
+ lookbackStart?: string | null;
76
+ excludedEventNames?: string[];
68
77
  updatedAt: string;
69
78
  }
70
79
 
@@ -75,6 +84,8 @@ export interface DedicatedBackfillOptions {
75
84
  concurrency: number;
76
85
  maxBatches: number;
77
86
  checkpointPath: string;
87
+ lookbackDays?: number;
88
+ excludedEventNames?: string[];
78
89
  }
79
90
 
80
91
  export interface FinalizeResult {
@@ -126,6 +137,8 @@ interface BranchSpec {
126
137
  name: BranchName;
127
138
  predicate: string;
128
139
  predicateArgs: string[];
140
+ lookbackStart?: string | null;
141
+ excludedEventNames?: string[];
129
142
  }
130
143
 
131
144
  const PROJECTED_COLUMNS = FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS.join(", ");
@@ -144,6 +157,49 @@ function positiveInteger(
144
157
  return Math.min(value, maximum);
145
158
  }
146
159
 
160
+ function parseLookbackDays(raw: string | undefined): number {
161
+ if (raw === undefined) return DEFAULT_LOOKBACK_DAYS;
162
+ const value = Number(raw);
163
+ if (
164
+ !Number.isInteger(value) ||
165
+ value < MIN_LOOKBACK_DAYS ||
166
+ value > MAX_LOOKBACK_DAYS
167
+ ) {
168
+ throw new Error(
169
+ `--lookback-days must be an integer between ${MIN_LOOKBACK_DAYS} and ${MAX_LOOKBACK_DAYS}`,
170
+ );
171
+ }
172
+ return value;
173
+ }
174
+
175
+ function parseExcludedEventNames(raw: string | undefined): string[] {
176
+ if (raw === undefined) return [...DEFAULT_EXCLUDED_EVENT_NAMES];
177
+ const names = [...new Set(raw.split(",").map((name) => name.trim()))].filter(
178
+ Boolean,
179
+ );
180
+ if (names.length > MAX_EXCLUDED_EVENT_NAMES) {
181
+ throw new Error(
182
+ `--skip-events supports at most ${MAX_EXCLUDED_EVENT_NAMES} event names`,
183
+ );
184
+ }
185
+ return names;
186
+ }
187
+
188
+ function lookbackStart(now: string, days: number): string {
189
+ const date = new Date(now);
190
+ if (Number.isNaN(date.getTime())) {
191
+ throw new Error(`Invalid backfill clock value: ${now}`);
192
+ }
193
+ date.setUTCDate(date.getUTCDate() - days);
194
+ return date.toISOString();
195
+ }
196
+
197
+ interface BackfillQueryConfig {
198
+ lookbackDays: number;
199
+ lookbackStart: string | null;
200
+ excludedEventNames: string[];
201
+ }
202
+
147
203
  function requiredArg(args: Record<string, string>, name: string): string {
148
204
  const value = args[name]?.trim();
149
205
  if (!value) throw new Error(`--${name} is required`);
@@ -177,6 +233,8 @@ export function parseDedicatedBackfillOptions(
177
233
  MAX_MAX_BATCHES,
178
234
  ),
179
235
  checkpointPath: resolve(args.checkpoint || DEFAULT_CHECKPOINT_PATH),
236
+ lookbackDays: parseLookbackDays(args["lookback-days"]),
237
+ excludedEventNames: parseExcludedEventNames(args["skip-events"]),
180
238
  execute: args.execute === "true",
181
239
  finalize: args.finalize === "true",
182
240
  };
@@ -184,35 +242,70 @@ export function parseDedicatedBackfillOptions(
184
242
 
185
243
  function branchSpecs(
186
244
  scope: FirstPartyAnalyticsScope & { orgId: string },
245
+ queryConfig: BackfillQueryConfig = {
246
+ lookbackDays: DEFAULT_LOOKBACK_DAYS,
247
+ lookbackStart: null,
248
+ excludedEventNames: [],
249
+ },
187
250
  ): BranchSpec[] {
188
251
  return [
189
252
  {
190
253
  name: "org",
191
254
  predicate: "org_id = ?",
192
255
  predicateArgs: [scope.orgId],
256
+ ...queryConfig,
193
257
  },
194
258
  {
195
259
  name: "legacy",
196
260
  predicate: "org_id IS NULL AND owner_email = ?",
197
261
  predicateArgs: [scope.userEmail],
262
+ ...queryConfig,
198
263
  },
199
264
  ];
200
265
  }
201
266
 
267
+ function sourceFilter(spec: BranchSpec): {
268
+ clauses: string[];
269
+ args: string[];
270
+ } {
271
+ const clauses: string[] = [];
272
+ const args: string[] = [];
273
+ if (spec.lookbackStart) {
274
+ clauses.push("received_at >= ?");
275
+ args.push(spec.lookbackStart);
276
+ }
277
+ const excluded = spec.excludedEventNames ?? [];
278
+ if (excluded.length) {
279
+ if (
280
+ excluded.length === DEFAULT_EXCLUDED_EVENT_NAMES.length &&
281
+ excluded[0] === DEFAULT_EXCLUDED_EVENT_NAMES[0]
282
+ ) {
283
+ clauses.push("event_name IS DISTINCT FROM 'http.response'");
284
+ } else {
285
+ clauses.push(
286
+ `(event_name IS NULL OR event_name NOT IN (${excluded.map(() => "?").join(", ")}))`,
287
+ );
288
+ args.push(...excluded);
289
+ }
290
+ }
291
+ return { clauses, args };
292
+ }
293
+
202
294
  export function buildHighWaterMarkQuery(spec: BranchSpec): DbQuery {
295
+ const filters = sourceFilter(spec);
203
296
  return {
204
297
  sql: `SELECT received_at, id
205
298
  FROM analytics_events
206
- WHERE ${spec.predicate}
299
+ WHERE ${spec.predicate}${filters.clauses.length ? `\n AND ${filters.clauses.join("\n AND ")}` : ""}
207
300
  ORDER BY received_at DESC, id DESC
208
301
  LIMIT 1`,
209
- args: spec.predicateArgs,
302
+ args: [...spec.predicateArgs, ...filters.args],
210
303
  timeoutMs: QUERY_TIMEOUT_MS,
211
304
  maxAttempts: 1,
212
305
  };
213
306
  }
214
307
 
215
- export function buildPageQuery(
308
+ export function buildPageKeyQuery(
216
309
  spec: BranchSpec,
217
310
  state: BranchCheckpoint,
218
311
  batchSize: number,
@@ -220,27 +313,25 @@ export function buildPageQuery(
220
313
  if (!state.cutoff) {
221
314
  throw new Error(`Cannot page ${spec.name} without a high-water mark`);
222
315
  }
316
+ const filters = sourceFilter(spec);
223
317
  const args: unknown[] = [
224
318
  ...spec.predicateArgs,
225
- state.cutoff.receivedAt,
319
+ ...filters.args,
226
320
  state.cutoff.receivedAt,
227
321
  state.cutoff.id,
228
322
  ];
229
323
  let cursorClause = "";
230
324
  if (state.cursor) {
231
- cursorClause = " AND (received_at > ? OR (received_at = ? AND id > ?))";
232
- args.push(
233
- state.cursor.receivedAt,
234
- state.cursor.receivedAt,
235
- state.cursor.id,
236
- );
325
+ cursorClause = " AND (received_at, id) > (?, ?)";
326
+ args.push(state.cursor.receivedAt, state.cursor.id);
237
327
  }
238
328
  args.push(batchSize);
239
329
  return {
240
- sql: `SELECT ${PROJECTED_COLUMNS}
330
+ sql: `SELECT received_at, id
241
331
  FROM analytics_events
242
332
  WHERE ${spec.predicate}
243
- AND (received_at < ? OR (received_at = ? AND id <= ?))${cursorClause}
333
+ ${filters.clauses.map((clause) => `AND ${clause}`).join("\n ")}
334
+ AND (received_at, id) <= (?, ?)${cursorClause}
244
335
  ORDER BY received_at ASC, id ASC
245
336
  LIMIT ?`,
246
337
  args,
@@ -249,6 +340,22 @@ export function buildPageQuery(
249
340
  };
250
341
  }
251
342
 
343
+ export function buildHydrationQuery(ids: string[]): DbQuery {
344
+ if (!ids.length || ids.length > MAX_HYDRATION_IDS) {
345
+ throw new Error(
346
+ `BigQuery backfill hydration requires 1-${MAX_HYDRATION_IDS} ids`,
347
+ );
348
+ }
349
+ return {
350
+ sql: `SELECT ${PROJECTED_COLUMNS}
351
+ FROM analytics_events
352
+ WHERE id IN (${ids.map(() => "?").join(", ")})`,
353
+ args: ids,
354
+ timeoutMs: QUERY_TIMEOUT_MS,
355
+ maxAttempts: 1,
356
+ };
357
+ }
358
+
252
359
  function cursorFromRow(row: unknown, description: string): BackfillCursor {
253
360
  if (!row || typeof row !== "object") {
254
361
  throw new Error(`${description} is not an object`);
@@ -288,9 +395,10 @@ function emptyBranchCheckpoint(): BranchCheckpoint {
288
395
  function newCheckpoint(
289
396
  options: DedicatedBackfillOptions,
290
397
  now: string,
398
+ queryConfig: BackfillQueryConfig,
291
399
  ): DedicatedBackfillCheckpoint {
292
400
  return {
293
- version: 1,
401
+ version: 2,
294
402
  scope: {
295
403
  userEmail: options.scope.userEmail,
296
404
  orgId: options.scope.orgId,
@@ -300,15 +408,54 @@ function newCheckpoint(
300
408
  org: emptyBranchCheckpoint(),
301
409
  legacy: emptyBranchCheckpoint(),
302
410
  },
411
+ lookbackDays: queryConfig.lookbackDays,
412
+ lookbackStart: queryConfig.lookbackStart ?? undefined,
413
+ excludedEventNames: queryConfig.excludedEventNames,
303
414
  updatedAt: now,
304
415
  };
305
416
  }
306
417
 
418
+ function checkpointQueryConfig(
419
+ checkpoint: DedicatedBackfillCheckpoint,
420
+ ): BackfillQueryConfig {
421
+ return {
422
+ lookbackDays:
423
+ checkpoint.version === 2
424
+ ? (checkpoint.lookbackDays ?? DEFAULT_LOOKBACK_DAYS)
425
+ : DEFAULT_LOOKBACK_DAYS,
426
+ lookbackStart:
427
+ checkpoint.version === 2 ? (checkpoint.lookbackStart ?? null) : null,
428
+ excludedEventNames:
429
+ checkpoint.version === 2 ? (checkpoint.excludedEventNames ?? []) : [],
430
+ };
431
+ }
432
+
433
+ function upgradeCheckpoint(
434
+ checkpoint: DedicatedBackfillCheckpoint,
435
+ queryConfig: BackfillQueryConfig,
436
+ ): DedicatedBackfillCheckpoint {
437
+ if (checkpoint.version === 2) {
438
+ return checkpoint.lookbackDays === undefined
439
+ ? { ...checkpoint, lookbackDays: queryConfig.lookbackDays }
440
+ : checkpoint;
441
+ }
442
+ for (const branch of ["org", "legacy"] as const) {
443
+ checkpoint.branches[branch].complete = false;
444
+ }
445
+ return {
446
+ ...checkpoint,
447
+ version: 2,
448
+ lookbackDays: queryConfig.lookbackDays,
449
+ lookbackStart: queryConfig.lookbackStart ?? undefined,
450
+ excludedEventNames: queryConfig.excludedEventNames,
451
+ };
452
+ }
453
+
307
454
  function assertCheckpointMatches(
308
455
  checkpoint: DedicatedBackfillCheckpoint,
309
456
  options: DedicatedBackfillOptions,
310
457
  ): void {
311
- if (checkpoint.version !== 1) {
458
+ if (checkpoint.version !== 1 && checkpoint.version !== 2) {
312
459
  throw new Error(
313
460
  `Unsupported backfill checkpoint version: ${checkpoint.version}`,
314
461
  );
@@ -326,6 +473,23 @@ function assertCheckpointMatches(
326
473
  "Backfill checkpoint table does not match the current BigQuery table",
327
474
  );
328
475
  }
476
+ if (checkpoint.version === 2) {
477
+ const lookbackDays = checkpoint.lookbackDays;
478
+ if (
479
+ (checkpoint.lookbackStart !== undefined &&
480
+ typeof checkpoint.lookbackStart !== "string") ||
481
+ typeof lookbackDays !== "number" ||
482
+ !Number.isInteger(lookbackDays) ||
483
+ lookbackDays < MIN_LOOKBACK_DAYS ||
484
+ lookbackDays > MAX_LOOKBACK_DAYS ||
485
+ !Array.isArray(checkpoint.excludedEventNames) ||
486
+ checkpoint.excludedEventNames.some((name) => typeof name !== "string")
487
+ ) {
488
+ throw new Error(
489
+ "Backfill checkpoint has invalid lookback or event filter metadata",
490
+ );
491
+ }
492
+ }
329
493
  for (const branch of ["org", "legacy"] as const) {
330
494
  const state = checkpoint.branches[branch];
331
495
  if (!state)
@@ -370,12 +534,44 @@ function compareCursors(left: BackfillCursor, right: BackfillCursor): number {
370
534
  );
371
535
  }
372
536
 
537
+ async function hydratePageRows(
538
+ keyRows: Array<Record<string, unknown>>,
539
+ db: DbExecutor,
540
+ ): Promise<Array<Record<string, unknown>>> {
541
+ const rowsById = new Map<string, Record<string, unknown>>();
542
+ for (let offset = 0; offset < keyRows.length; offset += MAX_HYDRATION_IDS) {
543
+ const ids = keyRows
544
+ .slice(offset, offset + MAX_HYDRATION_IDS)
545
+ .map((row, index) => {
546
+ const cursor = cursorFromRow(row, `backfill key row ${offset + index}`);
547
+ return cursor.id;
548
+ });
549
+ const result = await db.execute(buildHydrationQuery(ids));
550
+ for (const row of result.rows as Array<Record<string, unknown>>) {
551
+ const id = row.id;
552
+ if (typeof id !== "string" || !id) {
553
+ throw new Error("Backfill hydration row is missing id");
554
+ }
555
+ rowsById.set(id, row);
556
+ }
557
+ }
558
+
559
+ return keyRows.map((keyRow, index) => {
560
+ const { id } = cursorFromRow(keyRow, `backfill key row ${index}`);
561
+ const row = rowsById.get(id);
562
+ if (!row) {
563
+ throw new Error(`Backfill row ${id} disappeared during hydration`);
564
+ }
565
+ return row;
566
+ });
567
+ }
568
+
373
569
  export async function assertSourceAtCheckpoint(
374
570
  scope: DedicatedBackfillOptions["scope"],
375
571
  checkpoint: DedicatedBackfillCheckpoint,
376
572
  db: DbExecutor = getDbExec(),
377
573
  ): Promise<void> {
378
- for (const spec of branchSpecs(scope)) {
574
+ for (const spec of branchSpecs(scope, checkpointQueryConfig(checkpoint))) {
379
575
  const result = await db.execute(buildHighWaterMarkQuery(spec));
380
576
  const latestRow = result.rows[0];
381
577
  if (!latestRow) continue;
@@ -483,16 +679,44 @@ export async function runDedicatedBackfill(
483
679
  dependencies: DedicatedBackfillDependencies,
484
680
  ): Promise<DedicatedBackfillResult> {
485
681
  const now = dependencies.now ?? (() => new Date().toISOString());
486
- const checkpoint =
487
- (await dependencies.checkpointStore.load()) ??
488
- newCheckpoint(options, now());
682
+ const checkpointNow = now();
683
+ const loadedCheckpoint = await dependencies.checkpointStore.load();
684
+ const requestedLookbackDays = options.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
685
+ const requestedExcludedEventNames = options.excludedEventNames ?? [
686
+ ...DEFAULT_EXCLUDED_EVENT_NAMES,
687
+ ];
688
+ const queryConfig: BackfillQueryConfig = loadedCheckpoint
689
+ ? checkpointQueryConfig(loadedCheckpoint)
690
+ : {
691
+ lookbackDays: requestedLookbackDays,
692
+ lookbackStart: lookbackStart(checkpointNow, requestedLookbackDays),
693
+ excludedEventNames: requestedExcludedEventNames,
694
+ };
695
+ const checkpoint = loadedCheckpoint
696
+ ? upgradeCheckpoint(loadedCheckpoint, queryConfig)
697
+ : newCheckpoint(options, checkpointNow, queryConfig);
489
698
  assertCheckpointMatches(checkpoint, options);
699
+ if (checkpoint.version === 2) {
700
+ if (checkpoint.lookbackDays !== requestedLookbackDays) {
701
+ throw new Error(
702
+ "Backfill checkpoint lookback window does not match the current --lookback-days setting",
703
+ );
704
+ }
705
+ if (
706
+ JSON.stringify(checkpoint.excludedEventNames) !==
707
+ JSON.stringify(requestedExcludedEventNames)
708
+ ) {
709
+ throw new Error(
710
+ "Backfill checkpoint event filter does not match the current --skip-events setting",
711
+ );
712
+ }
713
+ }
490
714
  await dependencies.checkpointStore.save(checkpoint);
491
715
 
492
716
  let batches = 0;
493
717
  let copiedThisRun = 0;
494
718
  let stop = false;
495
- for (const spec of branchSpecs(options.scope)) {
719
+ for (const spec of branchSpecs(options.scope, queryConfig)) {
496
720
  const state = checkpoint.branches[spec.name];
497
721
  if (state.complete) continue;
498
722
 
@@ -506,15 +730,16 @@ export async function runDedicatedBackfill(
506
730
  stop = true;
507
731
  break;
508
732
  }
509
- const result = await dependencies.db.execute(
510
- buildPageQuery(spec, state, options.batchSize),
733
+ const keyResult = await dependencies.db.execute(
734
+ buildPageKeyQuery(spec, state, options.batchSize),
511
735
  );
512
- const rows = result.rows as Array<Record<string, unknown>>;
513
- if (!rows.length) {
736
+ const keyRows = keyResult.rows as Array<Record<string, unknown>>;
737
+ if (!keyRows.length) {
514
738
  await refreshBranchCutoff(spec, state, checkpoint, dependencies, now);
515
739
  if (state.complete) break;
516
740
  continue;
517
741
  }
742
+ const rows = await hydratePageRows(keyRows, dependencies.db);
518
743
 
519
744
  const copied = await dependencies.upload(rows);
520
745
  if (copied !== rows.length) {
@@ -569,6 +794,8 @@ Options:
569
794
  --concurrency=<n> BigQuery requests in flight, default 2, max 4
570
795
  --max-batches=<n> Work budget per invocation, default 1000
571
796
  --checkpoint=<path> Atomic local checkpoint path
797
+ --lookback-days=<n> Source window, 30-60 days, default 60
798
+ --skip-events=<csv> Event names to skip, default http.response
572
799
 
573
800
  After the checkpoint is complete, reconcile it into the shipped durable job
574
801
  after quiescing source event writes and checking the final high-water marks:
@@ -798,6 +1025,8 @@ async function main(): Promise<void> {
798
1025
  batchSize: options.batchSize,
799
1026
  concurrency: options.concurrency,
800
1027
  maxBatches: options.maxBatches,
1028
+ lookbackDays: options.lookbackDays,
1029
+ excludedEventNames: options.excludedEventNames,
801
1030
  checkpointPath: options.checkpointPath,
802
1031
  message:
803
1032
  "No data was copied. Pass --execute and AGENT_NATIVE_ANALYTICS_BIGQUERY_BACKFILL_ALLOW=1 after production approval.",
@@ -311,6 +311,29 @@ export const analyticsUserDays = table("analytics_user_days", {
311
311
  userKey: text("user_key").notNull(),
312
312
  });
313
313
 
314
+ /** Atomic per-tenant event reservations used to cap future Postgres growth. */
315
+ export const analyticsEventVolumeUsage = table(
316
+ "analytics_event_volume_usage",
317
+ {
318
+ id: text("id").primaryKey(),
319
+ tenantKey: text("tenant_key").notNull(),
320
+ ownerEmail: text("owner_email").notNull(),
321
+ orgId: text("org_id"),
322
+ windowStart: text("window_start").notNull(),
323
+ eventCount: integer("event_count").notNull().default(0),
324
+ eventLimit: integer("event_limit").notNull(),
325
+ updatedAt: text("updated_at").notNull().default(now()),
326
+ },
327
+ (t) => ({
328
+ tenantWindowUnique: uniqueIndex(
329
+ "analytics_event_volume_usage_tenant_window_idx",
330
+ ).on(t.tenantKey, t.windowStart),
331
+ updatedAtIdx: index("analytics_event_volume_usage_updated_at_idx").on(
332
+ t.updatedAt,
333
+ ),
334
+ }),
335
+ );
336
+
314
337
  /**
315
338
  * Compact pressure signals for first-party queries that are already slow or
316
339
  * failing. Successful fast queries never write here, so the diagnostic path
@@ -49,7 +49,9 @@ export const handleAnalyticsTrack = defineEventHandler(async (event) => {
49
49
  } catch (err: any) {
50
50
  const message = err?.message || String(err);
51
51
  const invalidKey = /invalid analytics public key/i.test(message);
52
- setResponseStatus(event, invalidKey ? 401 : 400);
52
+ const statusCode =
53
+ typeof err?.statusCode === "number" ? err.statusCode : undefined;
54
+ setResponseStatus(event, invalidKey ? 401 : (statusCode ?? 400));
53
55
  return { error: message };
54
56
  }
55
57
  });
@@ -248,7 +248,7 @@ export async function acquireDedicatedFirstPartyAnalyticsBackfillLease(
248
248
  SET status = 'pending', lease_token = NULL,
249
249
  lease_expires_at = NULL, next_run_at = ?, updated_at = ?
250
250
  WHERE id = ? AND lease_token = ?`,
251
- args: [leaseExpiresAt, releasedAt, id, token],
251
+ args: [releasedAt, releasedAt, id, token],
252
252
  timeoutMs: 5_000,
253
253
  maxAttempts: 1,
254
254
  });