@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
@@ -17,6 +17,7 @@ import {
17
17
  readAppState,
18
18
  writeAppState,
19
19
  } from "@agent-native/core/application-state";
20
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
20
21
  import { runWithRequestContext } from "@agent-native/core/server";
21
22
  import { track } from "@agent-native/core/tracking";
22
23
  import { normalizeChunkUploadNumber } from "@shared/recording-core.js";
@@ -34,6 +35,7 @@ import {
34
35
  } from "h3";
35
36
 
36
37
  import finalizeRecording from "../../../../../actions/finalize-recording.js";
38
+ import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
37
39
  import { getDb, schema } from "../../../../db/index.js";
38
40
  import { debugLog } from "../../../../lib/debug.js";
39
41
  import {
@@ -45,6 +47,7 @@ import {
45
47
  ownerEmailMatches,
46
48
  } from "../../../../lib/recordings.js";
47
49
  import {
50
+ deleteResumableSession,
48
51
  getResumableSession,
49
52
  setResumableSession,
50
53
  type StoredResumableSession,
@@ -166,6 +169,26 @@ export default defineEventHandler(async (event: H3Event) => {
166
169
  }
167
170
 
168
171
  const query = getQuery(event);
172
+ const attemptIdValue = Array.isArray(query.attemptId)
173
+ ? query.attemptId[0]
174
+ : query.attemptId;
175
+ const attemptId =
176
+ typeof attemptIdValue === "string" &&
177
+ attemptIdValue.length > 0 &&
178
+ attemptIdValue.length <= 128 &&
179
+ !/[\r\n]/.test(attemptIdValue)
180
+ ? attemptIdValue
181
+ : null;
182
+ const uploadGenerationIdValue = Array.isArray(query.uploadGenerationId)
183
+ ? query.uploadGenerationId[0]
184
+ : query.uploadGenerationId;
185
+ const uploadGenerationId =
186
+ typeof uploadGenerationIdValue === "string" &&
187
+ uploadGenerationIdValue.length > 0 &&
188
+ uploadGenerationIdValue.length <= 128 &&
189
+ !/[\r\n]/.test(uploadGenerationIdValue)
190
+ ? uploadGenerationIdValue
191
+ : null;
169
192
  const index = Number(query.index ?? 0);
170
193
  const total = Number(query.total ?? 0);
171
194
  const isFinal = query.isFinal === "1" || query.isFinal === "true";
@@ -188,7 +211,12 @@ export default defineEventHandler(async (event: H3Event) => {
188
211
  mimeType,
189
212
  });
190
213
 
191
- if (!Number.isFinite(index) || !Number.isInteger(index) || index < 0) {
214
+ if (
215
+ !Number.isFinite(index) ||
216
+ !Number.isInteger(index) ||
217
+ index < 0 ||
218
+ index > 999_999
219
+ ) {
192
220
  throw createError({ statusCode: 400, message: "Invalid chunk index" });
193
221
  }
194
222
 
@@ -210,6 +238,23 @@ export default defineEventHandler(async (event: H3Event) => {
210
238
  }
211
239
  debugLog("[chunk] resolved owner:", ownerEmail);
212
240
 
241
+ if (
242
+ attemptId !== null &&
243
+ !(await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
244
+ userEmail: ownerEmail,
245
+ userKey: ownerEmail,
246
+ orgId,
247
+ }))
248
+ ) {
249
+ setResponseStatus(event, 409);
250
+ return {
251
+ ok: false,
252
+ error: "Resumable upload retry is disabled.",
253
+ restartRequired: true,
254
+ recoveryEnabled: false,
255
+ };
256
+ }
257
+
213
258
  return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
214
259
  const db = getDb();
215
260
 
@@ -219,6 +264,7 @@ export default defineEventHandler(async (event: H3Event) => {
219
264
  .select({
220
265
  id: schema.recordings.id,
221
266
  status: schema.recordings.status,
267
+ uploadGenerationId: schema.recordings.uploadGenerationId,
222
268
  })
223
269
  .from(schema.recordings)
224
270
  .where(
@@ -252,13 +298,31 @@ export default defineEventHandler(async (event: H3Event) => {
252
298
  // The first renewal admits the request. Later renewals immediately before
253
299
  // every durable write/finalization boundary close the body-read/provider
254
300
  // gap where /abort can otherwise commit while this request is in flight.
301
+ if ((existing.uploadGenerationId ?? null) !== uploadGenerationId) {
302
+ setResponseStatus(event, 409);
303
+ return {
304
+ ok: false,
305
+ error: "A newer upload generation is already active.",
306
+ staleAttempt: true,
307
+ };
308
+ }
255
309
  const lease = await renewUploadLease(recordingId, {
310
+ attemptId,
311
+ generationId: uploadGenerationId,
256
312
  uploadProgress:
257
313
  total > 0
258
314
  ? Math.min(100, Math.round(((index + 1) / total) * 100))
259
315
  : undefined,
260
316
  });
261
317
  if (!lease.held) {
318
+ if (lease.staleAttempt) {
319
+ setResponseStatus(event, 409);
320
+ return {
321
+ ok: false,
322
+ error: "A newer upload retry is already active.",
323
+ staleAttempt: true,
324
+ };
325
+ }
262
326
  if (lease.status === "ready") {
263
327
  const readyState = await readAppState(
264
328
  `recording-upload-${recordingId}`,
@@ -273,7 +337,11 @@ export default defineEventHandler(async (event: H3Event) => {
273
337
  sourceSizeBytes: stateNumber(readyState, "sourceSizeBytes"),
274
338
  };
275
339
  }
276
- await deleteRecordingChunks(ownerEmail, recordingId).catch((err) => {
340
+ await deleteRecordingChunks(
341
+ ownerEmail,
342
+ recordingId,
343
+ uploadGenerationId,
344
+ ).catch((err) => {
277
345
  console.warn("[chunk] failed upload chunk cleanup failed:", {
278
346
  recordingId,
279
347
  err: err instanceof Error ? err.message : String(err),
@@ -285,9 +353,16 @@ export default defineEventHandler(async (event: H3Event) => {
285
353
  }
286
354
 
287
355
  const rejectIfLeaseLost = async () => {
288
- const current = await renewUploadLease(recordingId);
356
+ const current = await renewUploadLease(recordingId, {
357
+ attemptId,
358
+ generationId: uploadGenerationId,
359
+ });
289
360
  if (current.held) return null;
290
- await deleteRecordingChunks(ownerEmail, recordingId).catch((err) => {
361
+ await deleteRecordingChunks(
362
+ ownerEmail,
363
+ recordingId,
364
+ uploadGenerationId,
365
+ ).catch((err) => {
291
366
  console.warn("[chunk] failed upload chunk cleanup failed:", {
292
367
  recordingId,
293
368
  err: err instanceof Error ? err.message : String(err),
@@ -308,7 +383,10 @@ export default defineEventHandler(async (event: H3Event) => {
308
383
  }
309
384
 
310
385
  // Resumable streaming path — forward chunks directly to the provider.
311
- const resumableSession = await getResumableSession(recordingId);
386
+ const resumableSession = await getResumableSession(
387
+ recordingId,
388
+ uploadGenerationId,
389
+ );
312
390
  if (resumableSession && isStreamingUploadDisabled()) {
313
391
  console.warn(
314
392
  `[chunk] streaming uploads are disabled, but preserving existing resumable session for in-flight recording: ${recordingId}`,
@@ -324,6 +402,8 @@ export default defineEventHandler(async (event: H3Event) => {
324
402
  mimeType,
325
403
  query,
326
404
  ownerEmail,
405
+ attemptId,
406
+ uploadGenerationId,
327
407
  );
328
408
  }
329
409
 
@@ -418,7 +498,11 @@ export default defineEventHandler(async (event: H3Event) => {
418
498
  maxBytes: MAX_RECORDING_UPLOAD_BYTES,
419
499
  updatedAt: now,
420
500
  });
421
- await deleteRecordingChunks(ownerEmail, recordingId).catch((err) => {
501
+ await deleteRecordingChunks(
502
+ ownerEmail,
503
+ recordingId,
504
+ uploadGenerationId,
505
+ ).catch((err) => {
422
506
  console.warn("[chunk] oversized upload chunk cleanup failed:", {
423
507
  recordingId,
424
508
  err: err instanceof Error ? err.message : String(err),
@@ -439,12 +523,13 @@ export default defineEventHandler(async (event: H3Event) => {
439
523
  // Pad index to 6 digits so string-sort order matches numeric order if the
440
524
  // finalize path ever sorts lexically. (finalize also parses back to a number.)
441
525
  const paddedIndex = String(index).padStart(6, "0");
442
- const chunkKey = `recording-chunks-${recordingId}-${paddedIndex}`;
526
+ const chunkKey = `recording-chunks-${recordingId}${uploadGenerationId ? `-${uploadGenerationId}` : ""}-${paddedIndex}`;
443
527
  const previousChunk = await readAppState(chunkKey);
444
528
  const previousBytes = stateNumber(previousChunk, "bytes") ?? 0;
445
529
  const persistedBytesBefore = await sumRecordingChunkBytes(
446
530
  ownerEmail,
447
531
  recordingId,
532
+ uploadGenerationId,
448
533
  );
449
534
  const nextBytes =
450
535
  Math.max(0, persistedBytesBefore - previousBytes) + bytes.byteLength;
@@ -463,7 +548,11 @@ export default defineEventHandler(async (event: H3Event) => {
463
548
  data: toBase64(bytes),
464
549
  createdAt: new Date().toISOString(),
465
550
  });
466
- bytesReceived = await sumRecordingChunkBytes(ownerEmail, recordingId);
551
+ bytesReceived = await sumRecordingChunkBytes(
552
+ ownerEmail,
553
+ recordingId,
554
+ uploadGenerationId,
555
+ );
467
556
  if (bytesReceived > MAX_RECORDING_UPLOAD_BYTES) {
468
557
  return failRecordingTooLarge(bytesReceived);
469
558
  }
@@ -487,6 +576,7 @@ export default defineEventHandler(async (event: H3Event) => {
487
576
  if (leaseFailure) return leaseFailure;
488
577
  await writeAppState(`recording-upload-${recordingId}`, {
489
578
  recordingId,
579
+ uploadGenerationId,
490
580
  status: isFinal ? "processing" : "uploading",
491
581
  progress,
492
582
  chunksReceived,
@@ -508,6 +598,7 @@ export default defineEventHandler(async (event: H3Event) => {
508
598
  if (leaseFailure) return leaseFailure;
509
599
  await writeAppState(`recording-upload-${recordingId}`, {
510
600
  recordingId,
601
+ uploadGenerationId,
511
602
  status: isFinal ? "processing" : "uploading",
512
603
  chunksReceived: Math.max(
513
604
  stateNumber(uploadState, "chunksReceived") ?? 0,
@@ -532,7 +623,11 @@ export default defineEventHandler(async (event: H3Event) => {
532
623
  if (isFinal) {
533
624
  const leaseFailure = await rejectIfLeaseLost();
534
625
  if (leaseFailure) return leaseFailure;
535
- bytesReceived = await sumRecordingChunkBytes(ownerEmail, recordingId);
626
+ bytesReceived = await sumRecordingChunkBytes(
627
+ ownerEmail,
628
+ recordingId,
629
+ uploadGenerationId,
630
+ );
536
631
  if (bytesReceived > MAX_RECORDING_UPLOAD_BYTES) {
537
632
  return failRecordingTooLarge(bytesReceived);
538
633
  }
@@ -541,7 +636,7 @@ export default defineEventHandler(async (event: H3Event) => {
541
636
  debugLog("[chunk] isFinal — invoking finalize", { recordingId });
542
637
  try {
543
638
  const result = await finalizeRecording.run(
544
- buildFinalizeArgs(recordingId, mimeType, query),
639
+ buildFinalizeArgs(recordingId, mimeType, query, uploadGenerationId),
545
640
  );
546
641
  debugLog("[chunk] finalize ok", {
547
642
  recordingId,
@@ -713,6 +808,7 @@ function buildFinalizeArgs(
713
808
  recordingId: string,
714
809
  mimeType: string,
715
810
  query: Record<string, unknown>,
811
+ uploadGenerationId: string | null,
716
812
  ) {
717
813
  const queryBoolean = (value: unknown): boolean | undefined => {
718
814
  if (value === undefined) return undefined;
@@ -729,6 +825,7 @@ function buildFinalizeArgs(
729
825
  hasCamera: queryBoolean(query.hasCamera),
730
826
  locallyTranscoded: queryBoolean(query.locallyTranscoded),
731
827
  mimeType,
828
+ ...(uploadGenerationId ? { uploadGenerationId } : {}),
732
829
  };
733
830
  }
734
831
 
@@ -743,6 +840,8 @@ async function handleResumableChunk(
743
840
  mimeType: string,
744
841
  query: Record<string, unknown>,
745
842
  ownerEmail: string,
843
+ attemptId: string | null,
844
+ uploadGenerationId: string | null,
746
845
  ) {
747
846
  const uploadProvider = await resolveResumableUploadProvider(
748
847
  session.providerId,
@@ -771,7 +870,10 @@ async function handleResumableChunk(
771
870
  error: "Cannot finalize an empty resumable upload",
772
871
  };
773
872
  }
774
- const lease = await renewUploadLease(recordingId);
873
+ const lease = await renewUploadLease(recordingId, {
874
+ attemptId,
875
+ generationId: uploadGenerationId,
876
+ });
775
877
  if (!lease.held) {
776
878
  setResponseStatus(event, 409);
777
879
  return {
@@ -798,7 +900,10 @@ async function handleResumableChunk(
798
900
  };
799
901
  }
800
902
  if (closeRes.updatedMeta) {
801
- const postCloseLease = await renewUploadLease(recordingId);
903
+ const postCloseLease = await renewUploadLease(recordingId, {
904
+ attemptId,
905
+ generationId: uploadGenerationId,
906
+ });
802
907
  if (!postCloseLease.held) {
803
908
  setResponseStatus(event, 409);
804
909
  return {
@@ -808,10 +913,14 @@ async function handleResumableChunk(
808
913
  "Recording upload has already failed.",
809
914
  };
810
915
  }
811
- await setResumableSession(recordingId, {
812
- ...session,
813
- meta: { ...session.meta, ...closeRes.updatedMeta },
814
- });
916
+ await setResumableSession(
917
+ recordingId,
918
+ {
919
+ ...session,
920
+ meta: { ...session.meta, ...closeRes.updatedMeta },
921
+ },
922
+ uploadGenerationId,
923
+ );
815
924
  }
816
925
  } else {
817
926
  // Idempotent replay guard: a client retry (after a lost response) can
@@ -835,7 +944,10 @@ async function handleResumableChunk(
835
944
  };
836
945
  }
837
946
  } else {
838
- const lease = await renewUploadLease(recordingId);
947
+ const lease = await renewUploadLease(recordingId, {
948
+ attemptId,
949
+ generationId: uploadGenerationId,
950
+ });
839
951
  if (!lease.held) {
840
952
  setResponseStatus(event, 409);
841
953
  return {
@@ -866,14 +978,25 @@ async function handleResumableChunk(
866
978
  ? putResult.ok && putResult.status !== 308
867
979
  : putResult.ok;
868
980
  if (!resultOk) {
869
- setResponseStatus(event, 502);
981
+ const restartRequired =
982
+ putResult.status === 404 || putResult.status === 410;
983
+ if (restartRequired) {
984
+ await deleteResumableSession(recordingId, uploadGenerationId).catch(
985
+ () => {},
986
+ );
987
+ }
988
+ setResponseStatus(event, restartRequired ? 409 : 502);
870
989
  return {
871
990
  ok: false,
872
991
  error: `Chunk upload failed (${putResult.status})`,
992
+ ...(restartRequired ? { restartRequired: true } : {}),
873
993
  };
874
994
  }
875
995
 
876
- const postUploadLease = await renewUploadLease(recordingId);
996
+ const postUploadLease = await renewUploadLease(recordingId, {
997
+ attemptId,
998
+ generationId: uploadGenerationId,
999
+ });
877
1000
  if (!postUploadLease.held) {
878
1001
  setResponseStatus(event, 409);
879
1002
  return {
@@ -883,14 +1006,18 @@ async function handleResumableChunk(
883
1006
  "Recording upload has already failed.",
884
1007
  };
885
1008
  }
886
- await setResumableSession(recordingId, {
887
- ...session,
888
- ...(putResult.updatedMeta
889
- ? { meta: { ...session.meta, ...putResult.updatedMeta } }
890
- : {}),
891
- bytesUploaded: start + bytes.byteLength,
892
- lastCommittedIndex: index,
893
- });
1009
+ await setResumableSession(
1010
+ recordingId,
1011
+ {
1012
+ ...session,
1013
+ ...(putResult.updatedMeta
1014
+ ? { meta: { ...session.meta, ...putResult.updatedMeta } }
1015
+ : {}),
1016
+ bytesUploaded: start + bytes.byteLength,
1017
+ lastCommittedIndex: index,
1018
+ },
1019
+ uploadGenerationId,
1020
+ );
894
1021
  finalizedSourceSizeBytes = start + bytes.byteLength;
895
1022
 
896
1023
  if (!isFinal) {
@@ -901,7 +1028,10 @@ async function handleResumableChunk(
901
1028
 
902
1029
  // isFinal — delegate to finalize-recording, which reads the resumable
903
1030
  // session and calls provider.resumable.completeSession.
904
- const finalLease = await renewUploadLease(recordingId);
1031
+ const finalLease = await renewUploadLease(recordingId, {
1032
+ attemptId,
1033
+ generationId: uploadGenerationId,
1034
+ });
905
1035
  if (!finalLease.held) {
906
1036
  setResponseStatus(event, 409);
907
1037
  return {
@@ -911,7 +1041,7 @@ async function handleResumableChunk(
911
1041
  }
912
1042
  try {
913
1043
  const result = await finalizeRecording.run(
914
- buildFinalizeArgs(recordingId, mimeType, query),
1044
+ buildFinalizeArgs(recordingId, mimeType, query, uploadGenerationId),
915
1045
  );
916
1046
  if ((result as any)?.status === "failed") {
917
1047
  setResponseStatus(event, 409);
@@ -0,0 +1,197 @@
1
+ // guard:allow-api-route — Upload transport interruption preserves resumable state for desktop recovery.
2
+
3
+ /**
4
+ * Mark a live upload as interrupted without discarding its provider session or
5
+ * buffered chunks. Explicit cancellation continues to use /abort.
6
+ *
7
+ * Route: POST /api/uploads/:recordingId/interrupt
8
+ */
9
+
10
+ import {
11
+ compareAndSetAppState,
12
+ readAppState,
13
+ writeAppState,
14
+ } from "@agent-native/core/application-state";
15
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
16
+ import { runWithRequestContext } from "@agent-native/core/server";
17
+ import { and, eq, isNull } from "drizzle-orm";
18
+ import {
19
+ defineEventHandler,
20
+ getRouterParam,
21
+ readBody,
22
+ setResponseStatus,
23
+ type H3Event,
24
+ } from "h3";
25
+
26
+ import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
27
+ import { getDb, schema } from "../../../../db/index.js";
28
+ import {
29
+ getEventOwnerContext,
30
+ ownerEmailMatches,
31
+ } from "../../../../lib/recordings.js";
32
+ import {
33
+ isRetryableUploadInterruption,
34
+ RETRYABLE_UPLOAD_INTERRUPTION_REASON,
35
+ } from "../../../../lib/upload-interruption.js";
36
+ import abortUpload from "./abort.post.js";
37
+
38
+ export default defineEventHandler(async (event: H3Event) => {
39
+ const recordingId = getRouterParam(event, "recordingId");
40
+ if (!recordingId) {
41
+ setResponseStatus(event, 400);
42
+ return { error: "Missing recordingId" };
43
+ }
44
+
45
+ const { userEmail: ownerEmail, orgId } = await getEventOwnerContext(event);
46
+ if (
47
+ !(await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
48
+ userEmail: ownerEmail,
49
+ userKey: ownerEmail,
50
+ orgId,
51
+ }))
52
+ ) {
53
+ return abortUpload(event);
54
+ }
55
+ const body = (await readBody(event).catch(() => null)) as {
56
+ detail?: unknown;
57
+ attemptId?: unknown;
58
+ uploadGenerationId?: unknown;
59
+ } | null;
60
+ const attemptId =
61
+ typeof body?.attemptId === "string" &&
62
+ body.attemptId.length > 0 &&
63
+ body.attemptId.length <= 128
64
+ ? body.attemptId
65
+ : null;
66
+ const interruptionDetail =
67
+ typeof body?.detail === "string" && body.detail.trim()
68
+ ? body.detail.trim().slice(0, 1000)
69
+ : null;
70
+ const uploadGenerationId =
71
+ typeof body?.uploadGenerationId === "string" &&
72
+ body.uploadGenerationId.length > 0 &&
73
+ body.uploadGenerationId.length <= 128
74
+ ? body.uploadGenerationId
75
+ : null;
76
+
77
+ return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
78
+ const db = getDb();
79
+ const [existing] = await db
80
+ .select({
81
+ id: schema.recordings.id,
82
+ status: schema.recordings.status,
83
+ failureReason: schema.recordings.failureReason,
84
+ videoUrl: schema.recordings.videoUrl,
85
+ uploadAttemptId: schema.recordings.uploadAttemptId,
86
+ uploadGenerationId: schema.recordings.uploadGenerationId,
87
+ })
88
+ .from(schema.recordings)
89
+ .where(
90
+ and(
91
+ eq(schema.recordings.id, recordingId),
92
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
93
+ ),
94
+ );
95
+
96
+ if (!existing) {
97
+ setResponseStatus(event, 404);
98
+ return { error: "Recording not found" };
99
+ }
100
+ if (existing.status === "ready" || existing.status === "processing") {
101
+ return {
102
+ ok: true,
103
+ recordingId,
104
+ status: existing.status,
105
+ alreadyAccepted: true,
106
+ };
107
+ }
108
+ if ((existing.uploadAttemptId ?? null) !== attemptId) {
109
+ setResponseStatus(event, 409);
110
+ return {
111
+ error: "A newer upload retry is already active.",
112
+ staleAttempt: true,
113
+ };
114
+ }
115
+ if ((existing.uploadGenerationId ?? null) !== uploadGenerationId) {
116
+ setResponseStatus(event, 409);
117
+ return {
118
+ error: "A newer upload generation is already active.",
119
+ staleAttempt: true,
120
+ };
121
+ }
122
+ const alreadyInterrupted =
123
+ existing.status === "failed" &&
124
+ isRetryableUploadInterruption(existing.failureReason);
125
+ if (existing.status !== "uploading" && !alreadyInterrupted) {
126
+ setResponseStatus(event, 409);
127
+ return { error: "Recording upload is no longer active" };
128
+ }
129
+
130
+ const uploadStateKey = `recording-upload-${recordingId}`;
131
+ const uploadStateRaw = await readAppState(uploadStateKey);
132
+ const uploadState = uploadStateRaw ?? {};
133
+ const interruptedAt = new Date().toISOString();
134
+ if (!alreadyInterrupted) {
135
+ const interrupted = await db
136
+ .update(schema.recordings)
137
+ .set({
138
+ status: "failed",
139
+ failureReason: RETRYABLE_UPLOAD_INTERRUPTION_REASON,
140
+ updatedAt: interruptedAt,
141
+ })
142
+ .where(
143
+ and(
144
+ eq(schema.recordings.id, recordingId),
145
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
146
+ eq(schema.recordings.status, "uploading"),
147
+ attemptId === null
148
+ ? isNull(schema.recordings.uploadAttemptId)
149
+ : eq(schema.recordings.uploadAttemptId, attemptId),
150
+ uploadGenerationId === null
151
+ ? isNull(schema.recordings.uploadGenerationId)
152
+ : eq(schema.recordings.uploadGenerationId, uploadGenerationId),
153
+ ),
154
+ )
155
+ .returning({ id: schema.recordings.id });
156
+
157
+ if (interrupted.length !== 1) {
158
+ setResponseStatus(event, 409);
159
+ return { error: "Recording upload changed while it was interrupted" };
160
+ }
161
+ }
162
+
163
+ const uploadStateUpdated = await compareAndSetAppState(
164
+ uploadStateKey,
165
+ uploadStateRaw,
166
+ {
167
+ ...uploadState,
168
+ recordingId,
169
+ status: "failed",
170
+ failureReason: RETRYABLE_UPLOAD_INTERRUPTION_REASON,
171
+ retryableInterruption: true,
172
+ uploadAttemptId: attemptId,
173
+ uploadGenerationId,
174
+ ...(interruptionDetail ? { interruptionDetail } : {}),
175
+ updatedAt: interruptedAt,
176
+ },
177
+ );
178
+ if (!uploadStateUpdated) {
179
+ console.info(
180
+ `[upload-interrupt] upload state changed after interruption claim; preserving replacement state for ${recordingId}`,
181
+ );
182
+ }
183
+ await writeAppState("refresh-signal", { ts: Date.now() });
184
+
185
+ console.info("[upload-interrupt] resumable state preserved", {
186
+ recordingId,
187
+ hasInterruptionDetail: interruptionDetail !== null,
188
+ });
189
+ return {
190
+ ok: true,
191
+ recordingId,
192
+ status: "failed",
193
+ resumable: true,
194
+ ...(alreadyInterrupted ? { alreadyInterrupted: true } : {}),
195
+ };
196
+ });
197
+ });