@agent-native/core 0.134.1 → 0.135.1

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 (463) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +113 -0
  3. package/corpus/core/package.json +3 -2
  4. package/corpus/core/src/automations/service.ts +41 -4
  5. package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
  6. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
  7. package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  8. package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  9. package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
  10. package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
  12. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
  13. package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  14. package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
  15. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
  16. package/corpus/core/src/client/settings/index.ts +6 -0
  17. package/corpus/core/src/integrations/identity.ts +2 -12
  18. package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
  19. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
  20. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
  21. package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
  22. package/corpus/core/src/jobs/cron.ts +52 -6
  23. package/corpus/core/src/jobs/frontmatter.ts +20 -0
  24. package/corpus/core/src/jobs/run-history.ts +242 -0
  25. package/corpus/core/src/jobs/scheduler.ts +73 -18
  26. package/corpus/core/src/jobs/tools.ts +54 -6
  27. package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
  28. package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
  29. package/corpus/core/src/localization/default-messages.ts +27 -0
  30. package/corpus/core/src/localization/shared.ts +39 -6
  31. package/corpus/core/src/localization/user-timezone.ts +36 -0
  32. package/corpus/core/src/org/app-roles-handlers.ts +1 -9
  33. package/corpus/core/src/org/index.ts +2 -0
  34. package/corpus/core/src/org/membership.ts +18 -0
  35. package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
  36. package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
  37. package/corpus/core/src/review/index.ts +5 -0
  38. package/corpus/core/src/review/notifications.ts +163 -0
  39. package/corpus/core/src/review/types.ts +7 -0
  40. package/corpus/core/src/server/action-discovery.ts +4 -0
  41. package/corpus/core/src/server/activity-notifications.ts +174 -0
  42. package/corpus/core/src/server/app-name.ts +31 -0
  43. package/corpus/core/src/server/better-auth-instance.ts +4 -4
  44. package/corpus/core/src/server/email-templates.ts +70 -14
  45. package/corpus/core/src/server/email.ts +66 -3
  46. package/corpus/core/src/server/index.ts +10 -0
  47. package/corpus/core/src/sharing/access.ts +44 -2
  48. package/corpus/core/src/sharing/index.ts +5 -0
  49. package/corpus/core/src/sharing/recipients.ts +67 -0
  50. package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
  51. package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
  52. package/corpus/core/src/triggers/actions.ts +16 -4
  53. package/corpus/core/src/triggers/dispatcher.ts +44 -30
  54. package/corpus/core/src/triggers/routes.ts +19 -6
  55. package/corpus/core/src/vitest-config.ts +68 -0
  56. package/corpus/core/vitest.config.ts +5 -0
  57. package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
  58. package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
  59. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
  60. package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
  61. package/corpus/templates/analytics/vitest.config.ts +16 -12
  62. package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
  63. package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
  64. package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
  65. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  66. package/corpus/templates/assets/app/routes/settings.tsx +58 -55
  67. package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
  68. package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
  69. package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
  70. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
  71. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
  72. package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
  73. package/corpus/templates/assets/vitest.config.ts +16 -12
  74. package/corpus/templates/brain/app/routes/settings.tsx +15 -15
  75. package/corpus/templates/brain/vitest.config.ts +16 -12
  76. package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
  77. package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
  78. package/corpus/templates/calendar/vitest.config.ts +16 -12
  79. package/corpus/templates/chat/app/routes/settings.tsx +14 -22
  80. package/corpus/templates/chat/vitest.config.ts +9 -0
  81. package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
  82. package/corpus/templates/clips/AGENTS.md +1 -0
  83. package/corpus/templates/clips/actions/add-comment.ts +12 -1
  84. package/corpus/templates/clips/actions/create-meeting.ts +2 -2
  85. package/corpus/templates/clips/actions/create-recording.ts +2 -2
  86. package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
  87. package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
  88. package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
  89. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
  90. package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
  91. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
  92. package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
  93. package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
  94. package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
  95. package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
  96. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
  97. package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
  98. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
  99. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
  100. package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
  101. package/corpus/templates/clips/app/components/settings/types.ts +14 -0
  102. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
  103. package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
  104. package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
  105. package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
  106. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
  107. package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  113. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  114. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  115. package/corpus/templates/clips/desktop/package.json +1 -0
  116. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  117. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  118. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  119. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  120. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  121. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  122. package/corpus/templates/clips/server/db/schema.ts +17 -0
  123. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  124. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  125. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  126. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  127. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  128. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  129. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  130. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  131. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  132. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  133. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  134. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  135. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  136. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  137. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  138. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  139. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  140. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  141. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  142. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  143. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  144. package/corpus/templates/clips/vitest.config.ts +23 -19
  145. package/corpus/templates/content/actions/_database-utils.ts +113 -4
  146. package/corpus/templates/content/actions/add-comment.ts +15 -1
  147. package/corpus/templates/content/actions/get-document.ts +84 -3
  148. package/corpus/templates/content/actions/list-documents.ts +1 -0
  149. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
  150. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +34 -15
  151. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +55 -13
  152. package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
  153. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
  154. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
  155. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  156. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
  157. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  158. package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
  159. package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
  160. package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
  161. package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
  162. package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
  163. package/corpus/templates/content/app/i18n-data.ts +72 -1
  164. package/corpus/templates/content/app/lib/document-query.ts +36 -0
  165. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  166. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  167. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  168. package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
  169. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  170. package/corpus/templates/content/server/lib/document-context.ts +11 -6
  171. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  172. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  173. package/corpus/templates/content/shared/api.ts +8 -0
  174. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  175. package/corpus/templates/content/vitest.config.ts +20 -16
  176. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  177. package/corpus/templates/crm/vitest.config.ts +8 -4
  178. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  179. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  180. package/corpus/templates/design/vitest.config.ts +18 -12
  181. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  182. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  183. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  184. package/corpus/templates/factory/vitest.config.ts +9 -0
  185. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  186. package/corpus/templates/forms/vitest.config.ts +27 -23
  187. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  188. package/corpus/templates/macros/vitest.config.ts +16 -12
  189. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  190. package/corpus/templates/mail/vitest.config.ts +16 -12
  191. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  192. package/corpus/templates/plan/vitest.config.ts +24 -20
  193. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  194. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  195. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  196. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  198. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  199. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  200. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  201. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  202. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  203. package/corpus/templates/slides/vitest.config.ts +17 -13
  204. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  205. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  206. package/corpus/templates/tasks/vitest.config.ts +17 -13
  207. package/corpus/toolkit/CHANGELOG.md +9 -0
  208. package/corpus/toolkit/package.json +1 -1
  209. package/corpus/toolkit/vitest.config.ts +3 -0
  210. package/dist/automations/service.d.ts +2 -0
  211. package/dist/automations/service.d.ts.map +1 -1
  212. package/dist/automations/service.js +29 -4
  213. package/dist/automations/service.js.map +1 -1
  214. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  215. package/dist/cli/mcp-config-writers.js +24 -23
  216. package/dist/cli/mcp-config-writers.js.map +1 -1
  217. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  218. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  219. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  220. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  221. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  222. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  223. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  224. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  225. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  226. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  227. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  228. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  229. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  230. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  231. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  232. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  233. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  234. package/dist/client/agent-page/use-jobs.js +24 -4
  235. package/dist/client/agent-page/use-jobs.js.map +1 -1
  236. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  237. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  238. package/dist/client/org/OrgSwitcher.js +8 -2
  239. package/dist/client/org/OrgSwitcher.js.map +1 -1
  240. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  241. package/dist/client/settings/AccountSettingsCard.js +10 -9
  242. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  243. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  244. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  245. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  246. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  247. package/dist/client/settings/SettingsRow.d.ts +36 -0
  248. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  249. package/dist/client/settings/SettingsRow.js +15 -0
  250. package/dist/client/settings/SettingsRow.js.map +1 -0
  251. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  252. package/dist/client/settings/agent-settings-search.js +1 -1
  253. package/dist/client/settings/agent-settings-search.js.map +1 -1
  254. package/dist/client/settings/index.d.ts +1 -0
  255. package/dist/client/settings/index.d.ts.map +1 -1
  256. package/dist/client/settings/index.js +1 -0
  257. package/dist/client/settings/index.js.map +1 -1
  258. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  259. package/dist/integrations/identity.d.ts.map +1 -1
  260. package/dist/integrations/identity.js +2 -11
  261. package/dist/integrations/identity.js.map +1 -1
  262. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  263. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  264. package/dist/jobs/actions/list-automation-runs.js +29 -0
  265. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  266. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  267. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  268. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  269. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  270. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  271. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  272. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  273. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  274. package/dist/jobs/background-automation-runner.js +60 -0
  275. package/dist/jobs/background-automation-runner.js.map +1 -1
  276. package/dist/jobs/cron.d.ts +16 -2
  277. package/dist/jobs/cron.d.ts.map +1 -1
  278. package/dist/jobs/cron.js +43 -6
  279. package/dist/jobs/cron.js.map +1 -1
  280. package/dist/jobs/frontmatter.d.ts +12 -0
  281. package/dist/jobs/frontmatter.d.ts.map +1 -1
  282. package/dist/jobs/frontmatter.js +8 -0
  283. package/dist/jobs/frontmatter.js.map +1 -1
  284. package/dist/jobs/run-history.d.ts +56 -0
  285. package/dist/jobs/run-history.d.ts.map +1 -0
  286. package/dist/jobs/run-history.js +169 -0
  287. package/dist/jobs/run-history.js.map +1 -0
  288. package/dist/jobs/scheduler.d.ts.map +1 -1
  289. package/dist/jobs/scheduler.js +51 -18
  290. package/dist/jobs/scheduler.js.map +1 -1
  291. package/dist/jobs/tools.d.ts.map +1 -1
  292. package/dist/jobs/tools.js +37 -6
  293. package/dist/jobs/tools.js.map +1 -1
  294. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  295. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  296. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  297. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  298. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  299. package/dist/localization/actions/set-localization-preference.js +33 -8
  300. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  301. package/dist/localization/default-messages.d.ts +25 -0
  302. package/dist/localization/default-messages.d.ts.map +1 -1
  303. package/dist/localization/default-messages.js +25 -0
  304. package/dist/localization/default-messages.js.map +1 -1
  305. package/dist/localization/shared.d.ts +16 -1
  306. package/dist/localization/shared.d.ts.map +1 -1
  307. package/dist/localization/shared.js +22 -4
  308. package/dist/localization/shared.js.map +1 -1
  309. package/dist/localization/user-timezone.d.ts +11 -0
  310. package/dist/localization/user-timezone.d.ts.map +1 -0
  311. package/dist/localization/user-timezone.js +31 -0
  312. package/dist/localization/user-timezone.js.map +1 -0
  313. package/dist/notifications/routes.d.ts +4 -4
  314. package/dist/observability/routes.d.ts +3 -3
  315. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  316. package/dist/org/app-roles-handlers.js +1 -8
  317. package/dist/org/app-roles-handlers.js.map +1 -1
  318. package/dist/org/index.d.ts +1 -0
  319. package/dist/org/index.d.ts.map +1 -1
  320. package/dist/org/index.js +1 -0
  321. package/dist/org/index.js.map +1 -1
  322. package/dist/org/membership.d.ts +6 -0
  323. package/dist/org/membership.d.ts.map +1 -0
  324. package/dist/org/membership.js +16 -0
  325. package/dist/org/membership.js.map +1 -0
  326. package/dist/progress/routes.d.ts +1 -1
  327. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  328. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  329. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  330. package/dist/review/actions/create-review-comment.d.ts +32 -1
  331. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  332. package/dist/review/actions/create-review-comment.js +3 -1
  333. package/dist/review/actions/create-review-comment.js.map +1 -1
  334. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  335. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  336. package/dist/review/actions/reply-review-comment.js +3 -1
  337. package/dist/review/actions/reply-review-comment.js.map +1 -1
  338. package/dist/review/index.d.ts +1 -0
  339. package/dist/review/index.d.ts.map +1 -1
  340. package/dist/review/index.js +1 -0
  341. package/dist/review/index.js.map +1 -1
  342. package/dist/review/notifications.d.ts +24 -0
  343. package/dist/review/notifications.d.ts.map +1 -0
  344. package/dist/review/notifications.js +123 -0
  345. package/dist/review/notifications.js.map +1 -0
  346. package/dist/review/types.d.ts +5 -0
  347. package/dist/review/types.d.ts.map +1 -1
  348. package/dist/review/types.js.map +1 -1
  349. package/dist/server/action-discovery.d.ts.map +1 -1
  350. package/dist/server/action-discovery.js +4 -0
  351. package/dist/server/action-discovery.js.map +1 -1
  352. package/dist/server/activity-notifications.d.ts +61 -0
  353. package/dist/server/activity-notifications.d.ts.map +1 -0
  354. package/dist/server/activity-notifications.js +103 -0
  355. package/dist/server/activity-notifications.js.map +1 -0
  356. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  357. package/dist/server/app-name.d.ts +18 -0
  358. package/dist/server/app-name.d.ts.map +1 -1
  359. package/dist/server/app-name.js +31 -0
  360. package/dist/server/app-name.js.map +1 -1
  361. package/dist/server/better-auth-instance.js +4 -4
  362. package/dist/server/better-auth-instance.js.map +1 -1
  363. package/dist/server/email-templates.d.ts +12 -0
  364. package/dist/server/email-templates.d.ts.map +1 -1
  365. package/dist/server/email-templates.js +57 -14
  366. package/dist/server/email-templates.js.map +1 -1
  367. package/dist/server/email.d.ts +12 -0
  368. package/dist/server/email.d.ts.map +1 -1
  369. package/dist/server/email.js +52 -5
  370. package/dist/server/email.js.map +1 -1
  371. package/dist/server/index.d.ts +1 -0
  372. package/dist/server/index.d.ts.map +1 -1
  373. package/dist/server/index.js +1 -0
  374. package/dist/server/index.js.map +1 -1
  375. package/dist/server/transcribe-voice.d.ts +1 -1
  376. package/dist/sharing/access.d.ts.map +1 -1
  377. package/dist/sharing/access.js +32 -2
  378. package/dist/sharing/access.js.map +1 -1
  379. package/dist/sharing/index.d.ts +1 -0
  380. package/dist/sharing/index.d.ts.map +1 -1
  381. package/dist/sharing/index.js +1 -0
  382. package/dist/sharing/index.js.map +1 -1
  383. package/dist/sharing/recipients.d.ts +31 -0
  384. package/dist/sharing/recipients.d.ts.map +1 -0
  385. package/dist/sharing/recipients.js +40 -0
  386. package/dist/sharing/recipients.js.map +1 -0
  387. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  388. package/dist/templates/chat/vitest.config.ts +9 -0
  389. package/dist/triggers/actions/list-automations.d.ts +2 -0
  390. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  391. package/dist/triggers/actions/list-automations.js +19 -8
  392. package/dist/triggers/actions/list-automations.js.map +1 -1
  393. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  394. package/dist/triggers/actions/manage-automation.js +12 -6
  395. package/dist/triggers/actions/manage-automation.js.map +1 -1
  396. package/dist/triggers/actions.d.ts.map +1 -1
  397. package/dist/triggers/actions.js +15 -4
  398. package/dist/triggers/actions.js.map +1 -1
  399. package/dist/triggers/dispatcher.d.ts.map +1 -1
  400. package/dist/triggers/dispatcher.js +19 -17
  401. package/dist/triggers/dispatcher.js.map +1 -1
  402. package/dist/triggers/routes.d.ts.map +1 -1
  403. package/dist/triggers/routes.js +6 -6
  404. package/dist/triggers/routes.js.map +1 -1
  405. package/dist/vitest-config.d.ts +10 -0
  406. package/dist/vitest-config.d.ts.map +1 -0
  407. package/dist/vitest-config.js +55 -0
  408. package/dist/vitest-config.js.map +1 -0
  409. package/package.json +5 -4
  410. package/src/automations/service.ts +41 -4
  411. package/src/cli/mcp-config-writers.ts +27 -21
  412. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  413. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  414. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  415. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  416. package/src/client/agent-page/use-jobs.ts +50 -10
  417. package/src/client/org/OrgSwitcher.tsx +26 -0
  418. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  419. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  420. package/src/client/settings/SettingsRow.tsx +113 -0
  421. package/src/client/settings/agent-settings-search.ts +2 -1
  422. package/src/client/settings/index.ts +6 -0
  423. package/src/integrations/identity.ts +2 -12
  424. package/src/jobs/actions/list-automation-runs.ts +33 -0
  425. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  426. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  427. package/src/jobs/background-automation-runner.ts +88 -0
  428. package/src/jobs/cron.ts +52 -6
  429. package/src/jobs/frontmatter.ts +20 -0
  430. package/src/jobs/run-history.ts +242 -0
  431. package/src/jobs/scheduler.ts +73 -18
  432. package/src/jobs/tools.ts +54 -6
  433. package/src/localization/actions/get-localization-preference.ts +2 -2
  434. package/src/localization/actions/set-localization-preference.ts +49 -11
  435. package/src/localization/default-messages.ts +27 -0
  436. package/src/localization/shared.ts +39 -6
  437. package/src/localization/user-timezone.ts +36 -0
  438. package/src/org/app-roles-handlers.ts +1 -9
  439. package/src/org/index.ts +2 -0
  440. package/src/org/membership.ts +18 -0
  441. package/src/review/actions/create-review-comment.ts +4 -1
  442. package/src/review/actions/reply-review-comment.ts +4 -1
  443. package/src/review/index.ts +5 -0
  444. package/src/review/notifications.ts +163 -0
  445. package/src/review/types.ts +7 -0
  446. package/src/server/action-discovery.ts +4 -0
  447. package/src/server/activity-notifications.ts +174 -0
  448. package/src/server/app-name.ts +31 -0
  449. package/src/server/better-auth-instance.ts +4 -4
  450. package/src/server/email-templates.ts +70 -14
  451. package/src/server/email.ts +66 -3
  452. package/src/server/index.ts +10 -0
  453. package/src/sharing/access.ts +44 -2
  454. package/src/sharing/index.ts +5 -0
  455. package/src/sharing/recipients.ts +67 -0
  456. package/src/templates/chat/app/routes/settings.tsx +14 -22
  457. package/src/templates/chat/vitest.config.ts +9 -0
  458. package/src/triggers/actions/list-automations.ts +27 -8
  459. package/src/triggers/actions/manage-automation.ts +17 -6
  460. package/src/triggers/actions.ts +16 -4
  461. package/src/triggers/dispatcher.ts +44 -30
  462. package/src/triggers/routes.ts +19 -6
  463. package/src/vitest-config.ts +68 -0
@@ -24,15 +24,19 @@
24
24
  * Route: POST /api/uploads/:recordingId/reset-chunks
25
25
  */
26
26
 
27
+ import { randomUUID } from "node:crypto";
28
+
27
29
  import {
30
+ compareAndSetAppState,
31
+ readAppState,
28
32
  writeAppState,
29
- deleteAppStateByPrefix,
30
33
  } from "@agent-native/core/application-state";
34
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
31
35
  import { getActiveFileUploadProviderForRequest } from "@agent-native/core/file-upload";
32
36
  import { runWithRequestContext } from "@agent-native/core/server";
33
37
  import type { UploadMode } from "@shared/recording-core.js";
34
38
  import { MAX_UPLOAD_BYTES as MAX_RECORDING_UPLOAD_BYTES } from "@shared/upload-limits.js";
35
- import { and, eq } from "drizzle-orm";
39
+ import { and, eq, isNull } from "drizzle-orm";
36
40
  import {
37
41
  defineEventHandler,
38
42
  getRouterParam,
@@ -41,8 +45,10 @@ import {
41
45
  type H3Event,
42
46
  } from "h3";
43
47
 
48
+ import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
44
49
  import { getDb, schema } from "../../../../db/index.js";
45
50
  import { isMediaVerificationPending } from "../../../../lib/media-verification-state.js";
51
+ import { deleteRecordingChunks } from "../../../../lib/recording-upload-state.js";
46
52
  import {
47
53
  getEventOwnerContext,
48
54
  ownerEmailMatches,
@@ -52,7 +58,10 @@ import {
52
58
  setResumableSession,
53
59
  } from "../../../../lib/resumable-session.js";
54
60
  import { shouldEnableStreamingUpload } from "../../../../lib/streaming-upload-mode.js";
55
- import { uploadLeaseExpiry } from "../../../../lib/upload-lease.js";
61
+ import {
62
+ renewUploadLease,
63
+ uploadLeaseExpiry,
64
+ } from "../../../../lib/upload-lease.js";
56
65
  import { allowsSqlRecordingChunkScratch } from "../../../../lib/video-storage.js";
57
66
 
58
67
  interface CompressionMeta {
@@ -98,7 +107,15 @@ export default defineEventHandler(async (event: H3Event) => {
98
107
  compression?: CompressionMeta | null;
99
108
  requestStreaming?: boolean;
100
109
  mimeType?: string;
110
+ attemptId?: string;
111
+ uploadGenerationId?: string;
112
+ useGenerationFence?: boolean;
101
113
  } | null;
114
+ const recoveryEnabled = await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
115
+ userEmail: ownerEmail,
116
+ userKey: ownerEmail,
117
+ orgId,
118
+ });
102
119
 
103
120
  // Sanitize compression metadata. The recorder is the only client we trust
104
121
  // here, but the values land in Sentry extras — so we still bound them to
@@ -121,6 +138,8 @@ export default defineEventHandler(async (event: H3Event) => {
121
138
  id: schema.recordings.id,
122
139
  status: schema.recordings.status,
123
140
  videoUrl: schema.recordings.videoUrl,
141
+ uploadAttemptId: schema.recordings.uploadAttemptId,
142
+ uploadGenerationId: schema.recordings.uploadGenerationId,
124
143
  })
125
144
  .from(schema.recordings)
126
145
  .where(
@@ -140,6 +159,35 @@ export default defineEventHandler(async (event: H3Event) => {
140
159
  return { error: "Recording is already ready" };
141
160
  }
142
161
 
162
+ const requestedAttemptId =
163
+ typeof body?.attemptId === "string" &&
164
+ body.attemptId.length > 0 &&
165
+ body.attemptId.length <= 128
166
+ ? body.attemptId
167
+ : null;
168
+ const existingAttemptId = existing.uploadAttemptId ?? null;
169
+ const existingGenerationId = existing.uploadGenerationId ?? null;
170
+ if (recoveryEnabled && existingAttemptId !== requestedAttemptId) {
171
+ setResponseStatus(event, 409);
172
+ return {
173
+ error: "A newer upload retry is already active.",
174
+ staleAttempt: true,
175
+ };
176
+ }
177
+ const requestedGenerationId =
178
+ typeof body?.uploadGenerationId === "string" &&
179
+ body.uploadGenerationId.length > 0 &&
180
+ body.uploadGenerationId.length <= 128
181
+ ? body.uploadGenerationId
182
+ : null;
183
+ if (recoveryEnabled && existingGenerationId !== requestedGenerationId) {
184
+ setResponseStatus(event, 409);
185
+ return {
186
+ error: "A newer upload generation is already active.",
187
+ staleAttempt: true,
188
+ };
189
+ }
190
+
143
191
  if (
144
192
  await isMediaVerificationPending({
145
193
  ownerEmail,
@@ -150,15 +198,72 @@ export default defineEventHandler(async (event: H3Event) => {
150
198
  setResponseStatus(event, 409);
151
199
  return { error: "Recording is still being verified" };
152
200
  }
201
+ if (existing.status !== "uploading" && existing.status !== "failed") {
202
+ setResponseStatus(event, 409);
203
+ return { error: "Recording upload is no longer resettable" };
204
+ }
153
205
 
154
- const cleared = await deleteAppStateByPrefix(
155
- `recording-chunks-${recordingId}-`,
206
+ // Fence this reset before deleting any provider or buffered state. A
207
+ // retry that lost the token race must not tear down the winner's session.
208
+ const now = new Date().toISOString();
209
+ // Only clients that can carry the returned generation may opt into the
210
+ // fence. Retry claims always opt in; legacy reset callers keep the null
211
+ // generation wire contract until they are upgraded.
212
+ const useGenerationFence =
213
+ recoveryEnabled &&
214
+ (requestedAttemptId !== null ||
215
+ requestedGenerationId !== null ||
216
+ body?.useGenerationFence === true);
217
+ const nextGenerationId = useGenerationFence ? randomUUID() : null;
218
+ const uploadStateKey = `recording-upload-${recordingId}`;
219
+ const uploadStateSnapshot = await readAppState(uploadStateKey);
220
+ const reset = await db
221
+ .update(schema.recordings)
222
+ .set({
223
+ status: "uploading",
224
+ failureReason: null,
225
+ uploadProgress: 0,
226
+ uploadGenerationId: nextGenerationId,
227
+ ...(!recoveryEnabled ? { uploadAttemptId: null } : {}),
228
+ uploadLeaseExpiresAt: uploadLeaseExpiry(),
229
+ updatedAt: now,
230
+ })
231
+ .where(
232
+ and(
233
+ eq(schema.recordings.id, recordingId),
234
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
235
+ eq(schema.recordings.status, existing.status),
236
+ existingAttemptId === null
237
+ ? isNull(schema.recordings.uploadAttemptId)
238
+ : eq(schema.recordings.uploadAttemptId, existingAttemptId),
239
+ existingGenerationId === null
240
+ ? isNull(schema.recordings.uploadGenerationId)
241
+ : eq(schema.recordings.uploadGenerationId, existingGenerationId),
242
+ ),
243
+ )
244
+ .returning({ id: schema.recordings.id });
245
+
246
+ if (reset.length !== 1) {
247
+ setResponseStatus(event, 409);
248
+ return {
249
+ error: "A newer upload retry is already active.",
250
+ staleAttempt: true,
251
+ };
252
+ }
253
+
254
+ const cleared = await deleteRecordingChunks(
255
+ ownerEmail,
256
+ recordingId,
257
+ existingGenerationId,
156
258
  );
157
259
  // Clear any stale resumable session so a buffered retry does not
158
260
  // accidentally route through handleResumableChunk with stale offsets.
159
- await deleteResumableSession(recordingId).catch(() => {});
261
+ await deleteResumableSession(recordingId, existingGenerationId).catch(
262
+ () => {},
263
+ );
160
264
 
161
265
  let uploadMode: UploadMode = "buffered";
266
+ let compensateStartedSession: (() => Promise<void>) | null = null;
162
267
  if (body?.requestStreaming === true) {
163
268
  const mimeType = normalizeVideoMimeType(body.mimeType);
164
269
  if (!mimeType) {
@@ -184,17 +289,33 @@ export default defineEventHandler(async (event: H3Event) => {
184
289
  mimeType,
185
290
  MAX_RECORDING_UPLOAD_BYTES,
186
291
  );
187
- await setResumableSession(recordingId, {
188
- providerId: uploadProvider.id,
189
- sessionId: session.sessionId,
190
- meta: {
191
- ...session.meta,
192
- stableUrl: true,
193
- recordAsset: false,
292
+ await setResumableSession(
293
+ recordingId,
294
+ {
295
+ providerId: uploadProvider.id,
296
+ sessionId: session.sessionId,
297
+ meta: {
298
+ ...session.meta,
299
+ stableUrl: true,
300
+ recordAsset: false,
301
+ },
302
+ bytesUploaded: 0,
303
+ lastCommittedIndex: -1,
194
304
  },
195
- bytesUploaded: 0,
196
- lastCommittedIndex: -1,
197
- });
305
+ nextGenerationId,
306
+ );
307
+ compensateStartedSession = async () => {
308
+ if (!uploadProvider.resumable?.abortSession) {
309
+ throw new Error(
310
+ `Resumable upload provider ${uploadProvider.id} cannot abort the discarded retry session`,
311
+ );
312
+ }
313
+ await uploadProvider.resumable.abortSession({
314
+ sessionId: session.sessionId,
315
+ meta: session.meta,
316
+ });
317
+ await deleteResumableSession(recordingId, nextGenerationId);
318
+ };
198
319
  uploadMode = "streaming";
199
320
  } catch (err) {
200
321
  if (!bufferedFallbackAvailable) {
@@ -219,19 +340,64 @@ export default defineEventHandler(async (event: H3Event) => {
219
340
  }
220
341
  }
221
342
 
343
+ const resetLease = await renewUploadLease(recordingId, {
344
+ attemptId: recoveryEnabled ? existingAttemptId : null,
345
+ generationId: nextGenerationId,
346
+ });
347
+ if (!resetLease.held) {
348
+ await compensateStartedSession?.();
349
+ setResponseStatus(event, 409);
350
+ return {
351
+ error: "Recording upload changed while its retry was starting.",
352
+ staleAttempt: true,
353
+ };
354
+ }
355
+
222
356
  // Reset the per-recording upload progress so the UI poller sees the
223
357
  // re-upload restart from 0 and doesn't briefly show "100% then
224
358
  // re-running" on the post-compression chunked upload pass.
225
- const now = new Date().toISOString();
226
- await writeAppState(`recording-upload-${recordingId}`, {
227
- recordingId,
228
- status: "uploading",
229
- progress: 0,
230
- chunksReceived: 0,
231
- bytesReceived: 0,
232
- maxBytes: MAX_RECORDING_UPLOAD_BYTES,
233
- updatedAt: now,
234
- });
359
+ const uploadStateUpdated = await compareAndSetAppState(
360
+ uploadStateKey,
361
+ uploadStateSnapshot,
362
+ {
363
+ recordingId,
364
+ status: "uploading",
365
+ progress: 0,
366
+ chunksReceived: 0,
367
+ bytesReceived: 0,
368
+ uploadAttemptId: recoveryEnabled ? existingAttemptId : null,
369
+ uploadGenerationId: nextGenerationId,
370
+ maxBytes: MAX_RECORDING_UPLOAD_BYTES,
371
+ updatedAt: now,
372
+ },
373
+ );
374
+ if (!uploadStateUpdated) {
375
+ const [current] = await db
376
+ .select({
377
+ status: schema.recordings.status,
378
+ uploadAttemptId: schema.recordings.uploadAttemptId,
379
+ uploadGenerationId: schema.recordings.uploadGenerationId,
380
+ })
381
+ .from(schema.recordings)
382
+ .where(
383
+ and(
384
+ eq(schema.recordings.id, recordingId),
385
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
386
+ ),
387
+ );
388
+ if (
389
+ current?.status !== "uploading" ||
390
+ (current.uploadAttemptId ?? null) !==
391
+ (recoveryEnabled ? existingAttemptId : null) ||
392
+ (current.uploadGenerationId ?? null) !== nextGenerationId
393
+ ) {
394
+ setResponseStatus(event, 409);
395
+ return {
396
+ error: "Recording upload changed while its retry was starting.",
397
+ staleAttempt: true,
398
+ };
399
+ }
400
+ }
235
401
 
236
402
  // Stash compression metadata under its own key. We don't merge it into
237
403
  // `recording-upload-{id}` because the recorder client overwrites that
@@ -245,25 +411,13 @@ export default defineEventHandler(async (event: H3Event) => {
245
411
  });
246
412
  }
247
413
 
248
- await db
249
- .update(schema.recordings)
250
- .set({
251
- status: "uploading",
252
- failureReason: null,
253
- uploadProgress: 0,
254
- // A retry re-takes the lease. Without this the row would go back to
255
- // 'uploading' still carrying the expired lease that killed it.
256
- uploadLeaseExpiresAt: uploadLeaseExpiry(),
257
- updatedAt: now,
258
- })
259
- .where(eq(schema.recordings.id, recordingId));
260
-
261
414
  return {
262
415
  ok: true,
263
416
  recordingId,
264
417
  chunksCleared: cleared,
265
418
  compressionRecorded: !!compression,
266
419
  uploadMode,
420
+ uploadGenerationId: nextGenerationId,
267
421
  };
268
422
  });
269
423
  });
@@ -11,17 +11,25 @@
11
11
  * Route: GET /api/uploads/:recordingId/resume
12
12
  */
13
13
 
14
+ import {
15
+ compareAndSetAppState,
16
+ readAppState,
17
+ writeAppState,
18
+ } from "@agent-native/core/application-state";
19
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
14
20
  import { runWithRequestContext } from "@agent-native/core/server";
15
- import { and, eq } from "drizzle-orm";
21
+ import { and, eq, isNull } from "drizzle-orm";
16
22
  import {
17
23
  createError,
18
24
  defineEventHandler,
19
25
  getRouterParam,
26
+ getQuery,
20
27
  setResponseHeader,
21
28
  setResponseStatus,
22
29
  type H3Event,
23
30
  } from "h3";
24
31
 
32
+ import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
25
33
  import { getDb, schema } from "../../../../db/index.js";
26
34
  import {
27
35
  listRecordingChunkKeys,
@@ -33,7 +41,11 @@ import {
33
41
  ownerEmailMatches,
34
42
  } from "../../../../lib/recordings.js";
35
43
  import { getResumableSession } from "../../../../lib/resumable-session.js";
36
- import { renewUploadLease } from "../../../../lib/upload-lease.js";
44
+ import {
45
+ isRetryableUploadInterruption,
46
+ RETRYABLE_UPLOAD_INTERRUPTION_REASON,
47
+ } from "../../../../lib/upload-interruption.js";
48
+ import { uploadLeaseExpiry } from "../../../../lib/upload-lease.js";
37
49
 
38
50
  export default defineEventHandler(async (event: H3Event) => {
39
51
  setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
@@ -41,6 +53,21 @@ export default defineEventHandler(async (event: H3Event) => {
41
53
  if (!recordingId) {
42
54
  throw createError({ statusCode: 400, message: "Missing recordingId" });
43
55
  }
56
+ const requestedAttemptIdValue = getQuery(event).attemptId;
57
+ const requestedAttemptId = Array.isArray(requestedAttemptIdValue)
58
+ ? requestedAttemptIdValue[0]
59
+ : requestedAttemptIdValue;
60
+ if (
61
+ typeof requestedAttemptId !== "string" ||
62
+ requestedAttemptId.length < 16 ||
63
+ requestedAttemptId.length > 128 ||
64
+ !/^[A-Za-z0-9_-]+$/.test(requestedAttemptId)
65
+ ) {
66
+ throw createError({
67
+ statusCode: 400,
68
+ message: "A valid upload retry attemptId is required",
69
+ });
70
+ }
44
71
 
45
72
  let ownerEmail: string;
46
73
  let orgId: string | undefined;
@@ -52,6 +79,21 @@ export default defineEventHandler(async (event: H3Event) => {
52
79
  throw createError({ statusCode: 401, message: "Unauthorized" });
53
80
  }
54
81
 
82
+ const recoveryEnabled = await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
83
+ userEmail: ownerEmail,
84
+ userKey: ownerEmail,
85
+ orgId,
86
+ });
87
+ if (!recoveryEnabled) {
88
+ return {
89
+ recoveryEnabled: false,
90
+ resumable: false,
91
+ recordingId,
92
+ status: null,
93
+ reason: "feature_disabled",
94
+ };
95
+ }
96
+
55
97
  return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
56
98
  const [recording] = await getDb()
57
99
  .select({
@@ -59,6 +101,9 @@ export default defineEventHandler(async (event: H3Event) => {
59
101
  status: schema.recordings.status,
60
102
  failureReason: schema.recordings.failureReason,
61
103
  videoUrl: schema.recordings.videoUrl,
104
+ uploadProgress: schema.recordings.uploadProgress,
105
+ uploadAttemptId: schema.recordings.uploadAttemptId,
106
+ uploadGenerationId: schema.recordings.uploadGenerationId,
62
107
  })
63
108
  .from(schema.recordings)
64
109
  .where(
@@ -73,31 +118,160 @@ export default defineEventHandler(async (event: H3Event) => {
73
118
  return { error: "Recording not found" };
74
119
  }
75
120
 
76
- const lease = await renewUploadLease(recordingId);
77
- if (!lease.held) {
121
+ // Legacy rows keep their null generation and unscoped scratch. A reset
122
+ // upgrades them by installing a fresh generation before it deletes data.
123
+ const existingGenerationId = recording.uploadGenerationId ?? null;
124
+ const generationId = existingGenerationId;
125
+ const session = generationId
126
+ ? await getResumableSession(recordingId, generationId)
127
+ : await getResumableSession(recordingId);
128
+ const retryableFailure =
129
+ recording.status === "failed" &&
130
+ isRetryableUploadInterruption(recording.failureReason);
131
+ const existingAttemptId = recording.uploadAttemptId ?? null;
132
+ if (
133
+ recording.status === "uploading" &&
134
+ existingAttemptId !== null &&
135
+ existingAttemptId !== requestedAttemptId
136
+ ) {
137
+ setResponseStatus(event, 409);
138
+ return {
139
+ resumable: false,
140
+ recoveryEnabled: true,
141
+ recordingId,
142
+ status: "uploading",
143
+ reason: "retry_already_active",
144
+ };
145
+ }
146
+ if (recording.status !== "uploading" && !retryableFailure) {
147
+ return {
148
+ resumable: false,
149
+ recoveryEnabled: true,
150
+ recordingId,
151
+ status: recording.status,
152
+ failureReason: recording.failureReason,
153
+ videoUrl: recording.videoUrl,
154
+ };
155
+ }
156
+
157
+ const uploadStateKey = `recording-upload-${recordingId}`;
158
+ const uploadStateRaw = await readAppState(uploadStateKey);
159
+ const uploadState = uploadStateRaw ?? {};
160
+ const attemptId = requestedAttemptId;
161
+ const now = new Date().toISOString();
162
+ const claimed = await getDb()
163
+ .update(schema.recordings)
164
+ .set({
165
+ status: "uploading",
166
+ failureReason: null,
167
+ uploadAttemptId: attemptId,
168
+ ...(generationId ? { uploadGenerationId: generationId } : {}),
169
+ uploadLeaseExpiresAt: uploadLeaseExpiry(),
170
+ updatedAt: now,
171
+ })
172
+ .where(
173
+ and(
174
+ eq(schema.recordings.id, recordingId),
175
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
176
+ retryableFailure
177
+ ? eq(schema.recordings.status, "failed")
178
+ : eq(schema.recordings.status, "uploading"),
179
+ retryableFailure
180
+ ? eq(
181
+ schema.recordings.failureReason,
182
+ RETRYABLE_UPLOAD_INTERRUPTION_REASON,
183
+ )
184
+ : undefined,
185
+ existingAttemptId === null
186
+ ? isNull(schema.recordings.uploadAttemptId)
187
+ : eq(schema.recordings.uploadAttemptId, existingAttemptId),
188
+ existingGenerationId === null
189
+ ? isNull(schema.recordings.uploadGenerationId)
190
+ : eq(schema.recordings.uploadGenerationId, existingGenerationId),
191
+ ),
192
+ )
193
+ .returning({ id: schema.recordings.id });
194
+
195
+ if (claimed.length !== 1) {
196
+ setResponseStatus(event, 409);
78
197
  return {
79
198
  resumable: false,
199
+ recoveryEnabled: true,
80
200
  recordingId,
81
- status: lease.status,
82
- failureReason: lease.failureReason,
83
- videoUrl: lease.videoUrl,
201
+ status: "uploading",
202
+ reason: "retry_already_active",
84
203
  };
85
204
  }
86
205
 
87
- const session = await getResumableSession(recordingId).catch(() => null);
206
+ const uploadStateUpdated = await compareAndSetAppState(
207
+ uploadStateKey,
208
+ uploadStateRaw,
209
+ {
210
+ ...uploadState,
211
+ recordingId,
212
+ status: "uploading",
213
+ failureReason: null,
214
+ retryableInterruption: false,
215
+ progress: recording.uploadProgress,
216
+ uploadAttemptId: attemptId,
217
+ uploadGenerationId: generationId,
218
+ ...(session ? { bytesReceived: session.bytesUploaded } : {}),
219
+ updatedAt: now,
220
+ },
221
+ );
222
+ if (!uploadStateUpdated) {
223
+ const [current] = await getDb()
224
+ .select({
225
+ status: schema.recordings.status,
226
+ uploadAttemptId: schema.recordings.uploadAttemptId,
227
+ uploadGenerationId: schema.recordings.uploadGenerationId,
228
+ })
229
+ .from(schema.recordings)
230
+ .where(
231
+ and(
232
+ eq(schema.recordings.id, recordingId),
233
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
234
+ ),
235
+ );
236
+ if (
237
+ current?.status !== "uploading" ||
238
+ (current.uploadAttemptId ?? null) !== attemptId ||
239
+ (current.uploadGenerationId ?? null) !== generationId
240
+ ) {
241
+ setResponseStatus(event, 409);
242
+ return {
243
+ resumable: false,
244
+ recoveryEnabled: true,
245
+ recordingId,
246
+ status: current?.status ?? null,
247
+ reason: "upload_state_changed",
248
+ };
249
+ }
250
+ console.info(
251
+ `[upload-resume] upload state advanced under the same claim; preserving newer progress for ${recordingId}`,
252
+ );
253
+ }
254
+ await writeAppState("refresh-signal", { ts: Date.now() });
255
+
88
256
  if (session) {
89
257
  return {
90
258
  resumable: true,
259
+ recoveryEnabled: true,
91
260
  recordingId,
92
- status: recording.status,
261
+ status: "uploading",
93
262
  uploadMode: "streaming" as const,
263
+ attemptId,
264
+ ...(generationId ? { uploadGenerationId: generationId } : {}),
94
265
  bytesReceived: session.bytesUploaded,
95
266
  nextChunkIndex: (session.lastCommittedIndex ?? -1) + 1,
96
267
  };
97
268
  }
98
269
 
99
270
  const stored = new Set(
100
- (await listRecordingChunkKeys(ownerEmail, recordingId))
271
+ (generationId
272
+ ? await listRecordingChunkKeys(ownerEmail, recordingId, generationId)
273
+ : await listRecordingChunkKeys(ownerEmail, recordingId)
274
+ )
101
275
  .map(recordingChunkIndexFromKey)
102
276
  .filter((index): index is number => index !== null),
103
277
  );
@@ -108,10 +282,15 @@ export default defineEventHandler(async (event: H3Event) => {
108
282
 
109
283
  return {
110
284
  resumable: true,
285
+ recoveryEnabled: true,
111
286
  recordingId,
112
- status: recording.status,
287
+ status: "uploading",
113
288
  uploadMode: "buffered" as const,
114
- bytesReceived: await sumRecordingChunkBytes(ownerEmail, recordingId),
289
+ attemptId,
290
+ ...(generationId ? { uploadGenerationId: generationId } : {}),
291
+ bytesReceived: generationId
292
+ ? await sumRecordingChunkBytes(ownerEmail, recordingId, generationId)
293
+ : await sumRecordingChunkBytes(ownerEmail, recordingId),
115
294
  nextChunkIndex,
116
295
  };
117
296
  });
@@ -27,10 +27,16 @@ export type ClipsAiPrefs = {
27
27
 
28
28
  export type ClipsUserPrefs = ClipsAiPrefs & {
29
29
  defaultPlaybackSpeed?: string;
30
+ /** Activity emails (comments, replies, reactions) only — never share invites. */
30
31
  emailNotifications?: boolean;
31
32
  transcriptCleanupEnabled?: boolean;
33
+ /** Overrides the organization default visibility for new recordings. */
34
+ defaultRecordingVisibility?: ClipsDefaultVisibility;
32
35
  };
33
36
 
37
+ /** Visibility applied to new recordings unless the creator picks another. */
38
+ export type ClipsDefaultVisibility = "private" | "org" | "public";
39
+
34
40
  export function isIncludeFullVideoInAiEnabled(
35
41
  prefs: ClipsAiPrefs | Record<string, unknown> | null | undefined,
36
42
  ): boolean {
@@ -28,7 +28,19 @@ export const CUSTOM_SCK_LIVE_UPLOAD_FLAG = defineFeatureFlag({
28
28
  "Upload recording chunks while capture is still in progress. Requires the custom ScreenCaptureKit pipeline.",
29
29
  });
30
30
 
31
+ /**
32
+ * Desktop app and hosted upload routes: preserve interrupted resumable
33
+ * sessions and continue saved retries from the provider-confirmed offset.
34
+ */
35
+ export const UPLOAD_RETRY_RESUME_FLAG = defineFeatureFlag({
36
+ key: "uploadRetryResume",
37
+ displayName: "Resumable upload retry",
38
+ description:
39
+ "Resume interrupted desktop uploads from their last confirmed byte instead of replaying the full local backup.",
40
+ });
41
+
31
42
  export const CLIPS_FEATURE_FLAGS = defineFeatureFlags([
32
43
  USE_CUSTOM_SCK_PIPELINE_FLAG,
33
44
  CUSTOM_SCK_LIVE_UPLOAD_FLAG,
45
+ UPLOAD_RETRY_RESUME_FLAG,
34
46
  ]);
@@ -76,6 +76,7 @@ export type ChunkUploadParams = {
76
76
  height?: number | null;
77
77
  hasAudio?: boolean;
78
78
  hasCamera?: boolean;
79
+ uploadGenerationId?: string;
79
80
  };
80
81
 
81
82
  export function normalizeChunkUploadNumber(value: unknown): number | undefined {
@@ -115,6 +116,9 @@ export function chunkUploadQuery(params: ChunkUploadParams): string {
115
116
  if (params.hasCamera !== undefined) {
116
117
  q.set("hasCamera", params.hasCamera ? "1" : "0");
117
118
  }
119
+ if (params.uploadGenerationId) {
120
+ q.set("uploadGenerationId", params.uploadGenerationId);
121
+ }
118
122
  return q.toString();
119
123
  }
120
124